Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
01 Sep 2025 · 5 min read ·Article 85 / 110
Go

85. Logging and Debugging with `grpc-go`

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

85. Logging and Debugging with grpc-go: Keeping Your Microservices Reliable

When developing microservices-based applications in the Go ecosystem, gRPC has become a leading technology for service-to-service communication. However, as architectures grow more complex, the need for monitoring, logging, and debugging within the grpc-go stack becomes critical. Without solid logging and debugging practices, tracing bugs and analyzing performance tends to be difficult and time-consuming.

This article covers how to perform logging and debugging on gRPC services using the grpc-go library. We’ll break down:

  • Enabling logging in grpc-go
  • Integrating logging into your application
  • Leveraging interceptors for tracing
  • Navigating error handling and debugging
  • Debugging tools and tricks

Complete with practical code snippets, simulations, and workflows, this article is aimed at Go backend engineers who want to improve the observability and resilience of their services.


What Is Logging and Debugging in gRPC?

Logging is the process of recording application activity to a log file or console for monitoring, auditing, and troubleshooting purposes. Debugging, on the other hand, is the set of activities involved in identifying and fixing errors in a system.

In a gRPC architecture, logging is especially important because:

  1. Many operations happen asynchronously and remotely.
  2. Error propagation between services is often not specific.
  3. Service security and stability depend heavily on observing the request/response flow.

Logging in grpc-go

By default, the grpc-go package already has internal logging, but it is limited to runtime errors. Real-world requirements often call for custom logging that can be integrated with platforms tailored to your needs (ELK, Sentry, etc.).

Let’s compare two approaches: default logging vs. custom interceptor logging.

ApproachProsCons
DefaultSimple, out-of-boxLess detail, hard to control
CustomFlexible, detailedRequires extra effort

1. Logging with the Built-in Logger (grpclog)

gRPC ships with a grpclog package that writes to stderr by default. However, you can replace its implementation with an external logging library such as zap, logrus, or even a custom logger.

Example Implementation

go
 1import (
 2    "go.uber.org/zap"
 3    "google.golang.org/grpc/grpclog"
 4)
 5
 6func init() {
 7    // Replace default grpc logger with zap
 8    logger, _ := zap.NewProduction()
 9    grpclog.SetLoggerV2(
10        zapLogger{logger},
11    )
12}
13
14type zapLogger struct {
15    l *zap.Logger
16}
17
18func (z zapLogger) Info(args ...interface{})  { z.l.Sugar().Info(args...) }
19func (z zapLogger) Infoln(args ...interface{}) { z.Info(args...) }
20func (z zapLogger) Infof(format string, args ...interface{}) { z.l.Sugar().Infof(format, args...) }
21
22func (z zapLogger) Warning(args ...interface{})  { z.l.Sugar().Warn(args...) }
23// and so on...

Note: gRPC only uses certain log levels.


2. Logging Through Middleware: Unary and Stream Interceptors

One of the powerful features in grpc-go is the interceptor: a wrapper function we can use to inject logging, tracing, and even rate limiting.

a. Unary Interceptor

Well suited for simple request-response calls.

go
 1import "google.golang.org/grpc"
 2
 3func LoggingUnaryInterceptor(
 4    ctx context.Context,
 5    req interface{},
 6    info *grpc.UnaryServerInfo,
 7    handler grpc.UnaryHandler,
 8) (interface{}, error) {
 9    log.Printf("[Unary] Request: %s at %v", info.FullMethod, time.Now())
10    resp, err := handler(ctx, req)
11    if err != nil {
12        log.Printf("[Unary] Error: %v", err)
13    }
14    return resp, err
15}
16
17// In grpc.NewServer:
18server := grpc.NewServer(
19    grpc.UnaryInterceptor(LoggingUnaryInterceptor),
20)

b. Stream Interceptor

For stream communication (client/server/bidirectional).

go
1func LoggingStreamInterceptor(
2    srv interface{},
3    ss grpc.ServerStream,
4    info *grpc.StreamServerInfo,
5    handler grpc.StreamHandler,
6) error {
7    log.Printf("[Stream] Started method: %s", info.FullMethod)
8    return handler(srv, ss)
9}

Simulated Logging Output

text
1[Unary] Request: /proto.OrderService/GetOrder at 2024-06-15T09:00:00Z
2[Unary] Error: rpc error: code = NotFound desc = Order not found
3[Stream] Started method: /proto.ChatService/Chatstream

3. Error Propagation and Contextual Logging

Logging without request context is sometimes not very informative. Use a unique request ID/trace ID for each call.

Example: adding metadata to the context.

go
 1import (
 2    "github.com/google/uuid"
 3    "google.golang.org/grpc/metadata"
 4)
 5
 6func addRequestID(ctx context.Context) context.Context {
 7    id := uuid.New().String()
 8    md := metadata.Pairs("x-request-id", id)
 9    return metadata.NewOutgoingContext(ctx, md)
10}

In the interceptor, log the request ID along with the method and error.


4. Debugging the gRPC API: Practices and Tools

Logging Flow Diagram in the Interceptor

MERMAID
flowchart TD
    Start[Start Request] --> Interceptor{Interceptor?}
    Interceptor -- Yes --> Log[Log Data]
    Log --> Handler[Handler Execution]
    Handler --> Resp[Response/Error]
    Resp --> Interceptor
    Interceptor -- No --> Handler

The diagram above illustrates the simple flow of intercept request–logging–handler–response.


Debugging with grpctools & Reflection

  • grpccurl: a CLI tool for probing gRPC endpoints, allowing you to inspect request/response details and errors in real time.
  • gRPC Reflection: use the reflection plugin (reflection.Register(server)) so endpoints can be explored during testing.

Error Debugging Example

sh
1grpcurl -plaintext -d '{"id":123}' localhost:50051 proto.OrderService/GetOrder

If an error occurs, the stack trace in the log (thanks to the logging interceptor) will be a great help in pinpointing the source of the problem.


5. Production Logging Integration (Stackdriver, ELK, Sentry)

In production, logs need to be exported to an observability system.

Recommended Libraries:

  • zap : high-performance structured logging
  • logrus : flexible and easy to customize

Example JSON Log for ELK:

json
1{
2  "level": "info",
3  "service": "order-service",
4  "method": "/proto.OrderService/GetOrder",
5  "request_id": "123e4567-e89b-12d3-a456-426614174000",
6  "status": "error",
7  "error": "Order not found",
8  "timestamp": "2024-06-15T09:00:01Z"
9}

Case Study: Handling Slow Requests and Error Traces

Suppose your order service suddenly becomes slow.

With the following logging interceptor, you can track down the delay:

go
 1func MonitoringInterceptor(
 2    ctx context.Context,
 3    req interface{},
 4    info *grpc.UnaryServerInfo,
 5    handler grpc.UnaryHandler,
 6) (interface{}, error) {
 7    start := time.Now()
 8    resp, err := handler(ctx, req)
 9    duration := time.Since(start)
10    if duration > 500*time.Millisecond {
11        log.Printf("[WARN] Slow GRPC call: %s, duration: %s", info.FullMethod, duration)
12    }
13    return resp, err
14}

Simulated Output:

text
1[WARN] Slow GRPC call: /proto.OrderService/GetOrder, duration: 712ms

Conclusion

Logging and debugging tooling and practices in grpc-go are a key investment in maintaining the reliability of your Go backend services. By placing logging interceptors, using context-aware traces, and integrating with an observability stack, you not only make troubleshooting easier but also lay the foundation for SRE and DevOps down the road.

Always review your logging needs: make sure they aren’t excessive, stay compliant with security/compliance requirements, and remain easy to trace.


The source code and experiments can be accessed at https://github.com/namapengguna/grpc-go-logging-playground .

Happy experimenting–and may your debugging never be frustrating again! 🚀

Related Articles

💬 Comments