Skip to content

gRPC Logging — Structured Logging Patterns for gRPC Services

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about grpc logging. We cover key concepts, practical examples, and best practices to help you master this topic.

gRPC logging captures structured information about every RPC call including method name, duration, status code, metadata, and error details, enabling debugging, auditing, and performance analysis.

What You'll Learn

  • Interceptor-based structured logging
  • Correlation IDs for request tracking
  • Log levels for development vs production
  • Request and response payload logging
  • Centralized log aggregation
  • Log sampling for high-throughput services

Why It Matters

Without structured logging, debugging a distributed system means grepping through millions of unstructured log lines. Structured logs with correlation IDs let you trace a single request across all services. DodaTech's Durga Antivirus Pro logs every gRPC call with correlation IDs and structured JSON, storing 30 days of logs in Elasticsearch for analysis.

Real-World Use

A threat alert is not showing up on a user's dashboard. The support team searches for the user's correlation ID in the logs, finds the gRPC call that processed the alert, and discovers it failed with a database timeout. The timeout is fixed, and the alert is reprocessed.

flowchart LR
    A["gRPC Request"] --> B["Log Interceptor"]
    B --> C["Generate\nCorrelation ID"]
    C --> D["Log: Request Start\n{method, metadata, id}"]
    D --> E["Call Handler"]
    E --> F["Log: Request Complete\n{duration, status, id}"]
    F --> G["Log Aggregator\n(Elasticsearch)"]
    G --> H["Grafana/Kibana\nSearch by correlation ID"]
    style G fill:#fef3c7,stroke:#d97706

Code Examples

Example 1: Structured Logging Interceptor in Go

package main

import (
    "log/slog"
    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
)

func loggingInterceptor(logger *slog.Logger) grpc.UnaryServerInterceptor {
    return func(ctx context.Context,
        req interface{},
        info *grpc.UnaryServerInfo,
        handler grpc.UnaryHandler,
    ) (interface{}, error) {
        
        // Extract or generate correlation ID
        md, _ := metadata.FromIncomingContext(ctx)
        correlationID := "unknown"
        if ids := md.Get("x-correlation-id"); len(ids) > 0 {
            correlationID = ids[0]
        } else {
            correlationID = generateUUID()
        }
        
        // Log request
        logger.Info("gRPC request started",
            "method", info.FullMethod,
            "correlation_id", correlationID,
            "request_size", proto.Size(req.(proto.Message)),
        )
        
        start := time.Now()
        resp, err := handler(ctx, req)
        duration := time.Since(start)
        
        // Log completion
        attrs := []slog.Attr{
            slog.String("method", info.FullMethod),
            slog.String("correlation_id", correlationID),
            slog.Duration("duration", duration),
            slog.Int("response_size",
                proto.Size(resp.(proto.Message))),
        }
        
        if err != nil {
            attrs = append(attrs,
                slog.String("error", err.Error()),
                slog.String("status",
                    status.Code(err).String()),
            )
            logger.Error("gRPC request failed", attrs...)
        } else {
            logger.Info("gRPC request completed", attrs...)
        }
        
        return resp, err
    }
}

// Stream logging
func streamLoggingInterceptor(logger *slog.Logger) grpc.StreamServerInterceptor {
    return func(srv interface{},
        ss grpc.ServerStream,
        info *grpc.StreamServerInfo,
        handler grpc.StreamHandler,
    ) error {
        logger.Info("stream started",
            "method", info.FullMethod,
            "is_server_stream", info.IsServerStream,
            "is_client_stream", info.IsClientStream,
        )
        
        start := time.Now()
        err := handler(srv, ss)
        
        logger.Info("stream ended",
            "method", info.FullMethod,
            "duration", time.Since(start),
            "error", err,
        )
        
        return err
    }
}

Example 2: Python Structured Logging

import structlog
import uuid
from grpc_interceptor import ServerInterceptor

logger = structlog.get_logger()

class StructuredLoggingInterceptor(ServerInterceptor):
    def intercept(self, method, request, context, method_name):
        # Get or create correlation ID
        metadata = dict(context.invocation_metadata())
        correlation_id = metadata.get(
            "x-correlation-id", str(uuid.uuid4()))
        
        # Add correlation ID to response metadata
        context.send_initial_metadata([
            ("x-correlation-id", correlation_id),
        ])
        
        log = logger.bind(
            method=method_name,
            correlation_id=correlation_id,
        )
        
        log.info("request_started",
            request=str(request)[:200],
        )
        
        start = time.time()
        try:
            response = method(request, context)
            duration = time.time() - start
            
            log.info("request_completed",
                duration_ms=round(duration * 1000, 2),
                status="success",
            )
            return response
            
        except Exception as e:
            duration = time.time() - start
            
            log.error("request_failed",
                duration_ms=round(duration * 1000, 2),
                error=str(e),
                status="error",
            )
            raise

# Configure structlog for JSON output
structlog.configure(
    processors=[
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.JSONRenderer(),
    ],
)

Example 3: Request/Response Logging in Node.js

const grpc = require('@grpc/grpc-js');
const pino = require('pino');
const { v4: uuidv4 } = require('uuid');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
});

class LoggingInterceptor {
  constructor(options = {}) {
    this.logPayloads = options.logPayloads || false;
    this.maxPayloadSize = options.maxPayloadSize || 500;
  }
  
  intercept(method, request, callback, metadata, config) {
    const correlationId = metadata.get('x-correlation-id')[0] || uuidv4();
    const start = Date.now();
    
    logger.info({
      msg: 'request started',
      method: method.name,
      correlationId,
      requestSize: request.length,
    });
    
    if (this.logPayloads && request) {
      const payload = JSON.stringify(request).substring(0, this.maxPayloadSize);
      logger.debug({ msg: 'request payload', method: method.name, payload });
    }
    
    const wrappedCallback = (error, response) => {
      const duration = Date.now() - start;
      
      if (error) {
        logger.error({
          msg: 'request failed',
          method: method.name,
          correlationId,
          duration,
          error: error.details,
          code: error.code,
        });
      } else {
        logger.info({
          msg: 'request completed',
          method: method.name,
          correlationId,
          duration,
          responseSize: JSON.stringify(response).length,
        });
      }
      
      callback(error, response);
    };
    
    return wrappedCallback;
  }
}

// Configure client with logging
const client = new ThreatServiceClient(
  'localhost:50051',
  grpc.credentials.createInsecure(),
  {
    interceptors: [new LoggingInterceptor({ logPayloads: true })],
  },
);

Common Mistakes

  1. Logging sensitive data at info level — never log passwords, tokens, or PII. Use debug level for payload logging and redact sensitive fields before logging.
  2. Not using structured logging — unstructured log strings can't be parsed by log aggregators. Use JSON structured logging with consistent field names.
  3. Logging synchronously — synchronous logging blocks the RPC handler. Use async loggers (pino, structlog, slog) that write to a buffer.
  4. Not including correlation IDs — without correlation IDs, you can't trace a request across services. Generate and propagate them in metadata.
  5. Logging too much in production — debug-level logging in production generates terabytes of data per day. Use info for operational logs, debug for development.

Practice Questions

  1. What fields should every gRPC log entry contain?
  2. How do you propagate correlation IDs across service boundaries?
  3. What is the difference between structured and unstructured logging?
  4. How do you handle sensitive data in logs?
  5. Why should logging be asynchronous?

Challenge: Design a logging Strategy for a gRPC microservice architecture that includes: structured JSON logging, correlation ID propagation, log levels per environment, sensitive data redaction, async logging with batching, and centralized log aggregation with Elasticsearch.

Mini Project

Build a comprehensive logging system for gRPC services with: interceptor-based structured JSON logging, correlation ID generation and propagation, configurable log levels, sensitive data redaction, async logging with pino/structlog, and integration with Elasticsearch/Kibana for log search.

FAQ

What log format is best for gRPC services?

JSON structured logging with fields: timestamp, level, method, correlation_id, duration_ms, status_code, error, service_name, request_size, response_size.

How do I prevent logging sensitive data?

Use a redaction filter that masks fields matching patterns (token, password, secret, ssn, credit_card). Log at debug level for payloads, not info.

Should I log every gRPC call in production?

Log every call at info level for operational awareness. Use debug for detailed payload logging, which should be sampled (1 in 1000) in production.

How do I correlate logs across services?

Generate a UUID at the edge service and pass it as x-correlation-id in gRPC metadata. Each service includes this ID in its log entries.

What logging library should I use?

Go: slog (stdlib) or zap. Python: structlog or structlog. Node.js: pino (fastest) or winston. All support structured JSON and async logging.

What's Next

Learn about gRPC monitoring and metrics

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro