85. Logging and Debugging with `grpc-go`
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:
- Many operations happen asynchronously and remotely.
- Error propagation between services is often not specific.
- 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.
| Approach | Pros | Cons |
|---|---|---|
| Default | Simple, out-of-box | Less detail, hard to control |
| Custom | Flexible, detailed | Requires 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
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.
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).
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
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/Chatstream3. 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.
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
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
1grpcurl -plaintext -d '{"id":123}' localhost:50051 proto.OrderService/GetOrderIf 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:
Example JSON Log for ELK:
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:
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:
1[WARN] Slow GRPC call: /proto.OrderService/GetOrder, duration: 712msConclusion
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! 🚀