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

79. gRPC Structured Logging with Zap

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

79. gRPC Structured Logging with Zap

Logging is an essential aspect of building gRPC-based microservices. Without structured logging, troubleshooting, monitoring, and alerting become extremely challenging. In modern practice, professional engineers have moved away from plain logging (for example, using fmt.Println) toward structured logging with libraries such as Uber Zap : a high-performance, structured, leveled logging library for Golang.

In this article, I will cover best practices for implementing structured logging with Zap in gRPC services, complete with code examples, diagrams, and simulated log output relevant to production observability needs.


Why Structured Logging?

Before we dive into the implementation, let’s understand why structured logging is so crucial:

  • Easier Searching: Log data in JSON or key-value format is easy to process with tools like ElasticSearch, Grafana Loki, or Datadog.
  • Richer Context: We can add contextual metadata such as traceID, userID, and status code, which is extremely helpful for tracing and audit trails.
  • Powerful Log Queries: The nature of structured logs opens the door to filtering and aggregating based on specific fields without having to parse them manually.

gRPC & Logging Context

gRPC services, unlike conventional HTTP, handle requests as a binary protocol and typically rely on context propagation more intensively. As a result, error handling and logging context are critical.

Unfortunately, unstructured logs cannot consistently capture important details such as latency, request metadata, or status.


Getting Started with Zap in gRPC Services

Let’s move on to an implementation example. We will build a gRPC interceptor that integrates the Zap Logger in a structured way into every request lifecycle. Interceptors in gRPC are similar to middleware in HTTP: they execute functions before and after the request handler runs.

Installation

sh
1go get go.uber.org/zap
2go get google.golang.org/grpc

Setting Up the Zap Logger

go
 1package main
 2
 3import (
 4    "go.uber.org/zap"
 5)
 6
 7func NewZapLogger() *zap.Logger {
 8    logger, err := zap.NewProduction() // Output: JSON, suitable for production
 9    if err != nil {
10        panic(err)
11    }
12    return logger
13}

Zap supports two main modes:

ModeDescription
ProductionJSON output, suitable for deployment
DevelopmentFriendly output for local development

Zap Interceptor for gRPC

Here is an example implementation of a unary interceptor that performs structured logging on every request:

go
 1package main
 2
 3import (
 4    "context"
 5    "time"
 6
 7    "go.uber.org/zap"
 8    "google.golang.org/grpc"
 9    "google.golang.org/grpc/status"
10)
11
12func UnaryZapLogger(logger *zap.Logger) grpc.UnaryServerInterceptor {
13    return func(
14        ctx context.Context,
15        req interface{},
16        info *grpc.UnaryServerInfo,
17        handler grpc.UnaryHandler,
18    ) (resp interface{}, err error) {
19
20        start := time.Now()
21        resp, err = handler(ctx, req)
22        duration := time.Since(start)
23
24        grpcStatus, _ := status.FromError(err)
25        logger.Info("gRPC Request",
26            zap.String("method", info.FullMethod),
27            zap.Any("request", req),
28            zap.Duration("latency_ms", duration),
29            zap.String("code", grpcStatus.Code().String()),
30            zap.Error(err),
31        )
32
33        return resp, err
34    }
35}

Explanation:

  • info.FullMethod: The name of the method being accessed (e.g., /helloworld.Greeter/SayHello)
  • zap.Any("request", req): Logs the request in serializable form (be careful with sensitive info)
  • zap.Duration("latency_ms", duration): Processing latency
  • zap.String("code", grpcStatus.Code().String()): The gRPC status code

Integrating the Interceptor into the gRPC Server

go
1func main() {
2    logger := NewZapLogger()
3    grpcServer := grpc.NewServer(
4        grpc.UnaryInterceptor(UnaryZapLogger(logger)),
5    )
6    // Register service & listen...
7}

Simulated Log Output

With the setup above, your logs will be recorded with neat JSON output like this:

json
 1{
 2  "level": "info",
 3  "ts": 1719491811.966,
 4  "caller": "main.go:24",
 5  "msg": "gRPC Request",
 6  "method": "/helloworld.Greeter/SayHello",
 7  "request": {"name": "Bob"},
 8  "latency_ms": 7.424,
 9  "code": "OK",
10  "error": ""
11}

A log structure like this is very easy to search and analyze in an observability backend.


Adding Context Metadata (TraceID, etc.)

The gRPC Context can carry metadata such as traceID, userID, etc., which can be forwarded to the Zap log.

Here is an example of accessing the traceID from the context and injecting it into the log:

go
 1func UnaryZapLogger(logger *zap.Logger) grpc.UnaryServerInterceptor {
 2    return func(ctx context.Context, req interface{},
 3        info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
 4
 5        // Simulate extracting the traceID from the context/metadata
 6        traceID, _ := ctx.Value("trace_id").(string)
 7
 8        start := time.Now()
 9        resp, err := handler(ctx, req)
10        duration := time.Since(start)
11        grpcStatus, _ := status.FromError(err)
12        logger.Info("gRPC Request",
13            zap.String("trace_id", traceID),
14            zap.String("method", info.FullMethod),
15            zap.Duration("latency_ms", duration),
16            zap.String("code", grpcStatus.Code().String()),
17            zap.Error(err),
18        )
19        return resp, err
20    }
21}

Tip: For automation, use grpc-metadata or a tracing library such as OpenTelemetry to inject context metadata.


Logging Flow Diagram with the Interceptor (Mermaid)

MERMAID
sequenceDiagram
    participant Client as gRPC Client
    participant Server as gRPC Server
    participant Logger as Zap Logger

    Client->>Server: Send gRPC Request
    Server->>Logger: Log context, request, dsb
    Server->>Server: Handler process
    Server->>Logger: Log status, duration, error
    Server-->>Client: Return gRPC Response

Good Practices

  1. Don’t Log Sensitive Data
    Avoid logging confidential data such as passwords.

  2. Log Levels
    Use logger.Error for errors and logger.Info for normal events.

  3. Standard Fields
    Get into the habit of logging with consistent fields: method, userID, code, duration.
    Here is a table of recommended essential fields:

    FieldPurpose
    methodAPI/method name
    codegRPC status code
    latency_msExecution duration (ms)
    trace_idTrace for tracing
    errorError details (if any)
    user_idUser (if user-facing)
  4. Integrate the Logger into the Context
    For complex handlers, you can apply the pattern of storing the logger in the context, so that each sub-function can access the same logger along with contextual info.


Conclusion

Structured logging with Zap in gRPC services makes monitoring, troubleshooting, and post-mortem analysis much easier. The availability of context-rich logs will reduce MTTR (Mean Time To Recovery) and significantly improve observability.

If you are serious about building a gRPC-based product, don’t overlook structured logging—make Zap and interceptors the default template for your projects.

Happy experimenting, and don’t forget to refactor your logs toward a more structured approach!


References:

Related Articles

💬 Comments