gRPC Logging — Structured Logging Patterns for gRPC Services
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
- Logging sensitive data at info level — never log passwords, tokens, or PII. Use debug level for payload logging and redact sensitive fields before logging.
- Not using structured logging — unstructured log strings can't be parsed by log aggregators. Use JSON structured logging with consistent field names.
- Logging synchronously — synchronous logging blocks the RPC handler. Use async loggers (pino, structlog, slog) that write to a buffer.
- Not including correlation IDs — without correlation IDs, you can't trace a request across services. Generate and propagate them in metadata.
- 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
- What fields should every gRPC log entry contain?
- How do you propagate correlation IDs across service boundaries?
- What is the difference between structured and unstructured logging?
- How do you handle sensitive data in logs?
- 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's Next
Learn about gRPC monitoring and metrics
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro