While OpenTelemetry is the primary source of observability in Clue, logs still play an important role in certain scenarios. Clue provides a smart logging system that efficiently buffers and formats log messages, helping you maintain visibility while controlling costs and performance.
Smart Buffering: Clue’s smart buffering system helps optimize logging costs and performance. It buffers non-error logs in memory until an error occurs, at which point it flushes the buffer to provide full context around the error. For traced requests, logs are automatically flushed to ensure complete visibility of the request lifecycle. The system also provides manual flush control when you need to force log output in specific scenarios. To maintain flexibility, buffering behavior can be configured based on the context, allowing you to adapt logging patterns to different situations.
Structured Logging: Clue uses structured logging to make logs more useful and maintainable. All log fields are stored as key-value pairs, ensuring they can be easily parsed and analyzed by logging tools. The consistent formatting across all logs improves integration with monitoring and analysis systems. Logs can be output in different formats like JSON or plain text depending on your environment’s needs. Additionally, context-based fields are automatically included to correlate logs with request traces, making it easier to debug issues across your distributed system.
Performance: Clue’s logging system is designed with performance in mind. It uses efficient buffering techniques to minimize I/O overhead and employs smart memory management to keep allocation overhead low. The system can be configured to output logs differently based on your environment’s needs. You can also control log volume through conditional logging, ensuring you only generate the logs you need.
The logger needs to be configured before use. Here’s how to set up a logger with common options:
// Create logger context with options
ctx := log.Context(context.Background(),
// Include span IDs in logs for correlation with traces
log.WithFunc(log.Span),
// Use JSON format for machine readability
log.WithFormat(log.FormatJSON),
// Send logs to standard output
log.WithOutput(os.Stdout),
// Disable buffering when request is being traced
log.WithDisableBuffering(log.IsTracing))
// Add common fields that will be included in all logs
ctx = log.With(ctx,
log.KV{"service", "myservice"}, // Service name for filtering
log.KV{"env", "production"}) // Environment for context
This configuration establishes a robust logging foundation for your service. By including span IDs in the logs, you can easily correlate log entries with distributed traces, giving you a complete picture of request flows through your system. The JSON formatting ensures your logs can be efficiently processed by log aggregation and analysis tools.
For local development convenience, logs are directed to standard output where they can be easily viewed in the terminal. The smart buffering system automatically adjusts based on whether a request is being traced, optimizing for both performance and observability.
Finally, the setup includes common fields that will be added to every log entry, providing consistent context for filtering and analysis. These fields, like service name and environment, make it simple to identify the source and context of each log entry when investigating issues.
Clue supports four severity levels, each serving a specific purpose and following different buffering rules:
// Debug level - for detailed troubleshooting
// Only emitted when debug mode is enabled via WithDebug
log.Debug(ctx, "request details",
log.KV{"headers", req.Headers},
log.KV{"body_size", len(req.Body)})
// Info level - for normal operations
// These logs are buffered by default and flushed on errors
log.Info(ctx, "processing request",
log.KV{"requestID", req.ID},
log.KV{"method", req.Method})
// Warn level - for potential issues
// These logs indicate problems that don't prevent operation
log.Warn(ctx, "resource usage high",
log.KV{"cpu_usage", cpuUsage},
log.KV{"memory_usage", memUsage})
// Error level - for failure conditions
// These logs are written immediately and flush the buffer
log.Error(ctx, err, "request failed",
log.KV{"requestID", req.ID},
log.KV{"status", http.StatusInternalServerError})
// Fatal level - for unrecoverable errors
// These logs cause the program to exit after logging
log.Fatal(ctx, err, "cannot start server",
log.KV{"port", config.Port},
log.KV{"error", err.Error()})
Each level also has a corresponding formatted version that accepts printf-style formatting:
// Debug with formatting
log.Debugf(ctx, "processing item %d of %d", current, total)
// Info with formatting
log.Infof(ctx, "request completed in %dms", duration.Milliseconds())
// Warn with formatting
log.Warnf(ctx, "high latency detected: %dms", latency.Milliseconds())
// Error with formatting and error object
log.Errorf(ctx, err, "failed to process request: %s", req.ID)
// Fatal with formatting and error object
log.Fatalf(ctx, err, "failed to initialize: %s", component)
Best practices for log levels:
DEBUG (SeverityDebug):
INFO (SeverityInfo):
WARN (SeverityWarn):
ERROR (SeverityError):
FATAL (SeverityError + Exit):
Special behaviors:
Color coding (when using terminal format):
Structured logging makes it easier to parse and analyze logs. Here are different ways to structure your logs:
// Using log.KV for ordered key-value pairs
// This is the preferred method as it maintains field order
log.Print(ctx,
log.KV{"action", "user_login"}, // What happened
log.KV{"user_id", user.ID}, // Who it happened to
log.KV{"ip", req.RemoteAddr}, // Additional context
log.KV{"duration_ms", duration.Milliseconds()}) // Performance data
// Using log.Fields for map-style logging
// Useful when working with existing maps of data
log.Print(ctx, log.Fields{
"action": "user_login",
"user_id": user.ID,
"ip": req.RemoteAddr,
"duration_ms": duration.Milliseconds(),
})
// Adding context fields that will be included in all subsequent logs
// Useful for request-scoped information
ctx = log.With(ctx,
log.KV{"tenant", tenant.ID}, // Multi-tenant context
log.KV{"region", "us-west"}, // Geographic context
log.KV{"request_id", reqID}) // Request correlation
Best practices for structured logging:
Choose an output format that matches your environment and tools:
// Plain text format (logfmt)
// Best for local development and human readability
ctx := log.Context(context.Background(),
log.WithFormat(log.FormatText))
// Output: time=2024-02-24T12:34:56Z level=info msg="hello world"
// Terminal format with colors
// Great for local development and debugging
ctx := log.Context(context.Background(),
log.WithFormat(log.FormatTerminal))
// Output: INFO[0000] msg="hello world"
// JSON format
// Best for production and log aggregation systems
ctx := log.Context(context.Background(),
log.WithFormat(log.FormatJSON))
// Output: {"time":"2024-02-24T12:34:56Z","level":"info","msg":"hello world"}
// Custom format
// Use when you need special formatting
ctx := log.Context(context.Background(),
log.WithFormat(func(entry *log.Entry) []byte {
return []byte(fmt.Sprintf("[%s] %s: %s\n",
entry.Time.Format(time.RFC3339),
entry.Level,
entry.Message))
}))
When choosing a log format, consider your environment and requirements carefully. Development environments often benefit from human-readable formats with colors and formatting, while production deployments typically need machine-parseable formats like JSON for log aggregation systems. The format you choose should balance human readability with the needs of your log processing pipeline. Additionally, consider the performance impact of your chosen format - while JSON provides rich structure, it has more processing overhead than simple text formats.
Add logging to HTTP handlers to track requests and responses:
// Basic logging middleware
// Automatically logs request start/end and duration
handler = log.HTTP(ctx)(handler)
// Custom middleware with detailed logging
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Log request details at start
log.Info(ctx, "request started",
log.KV{"method", r.Method},
log.KV{"path", r.URL.Path},
log.KV{"user_agent", r.UserAgent()})
start := time.Now()
sw := &statusWriter{ResponseWriter: w}
next.ServeHTTP(sw, r)
// Log response details and duration
log.Info(ctx, "request completed",
log.KV{"status", sw.status},
log.KV{"duration_ms", time.Since(start).Milliseconds()},
log.KV{"bytes_written", sw.written})
})
}
This middleware provides comprehensive logging capabilities for your HTTP services. It automatically correlates requests with their corresponding responses, allowing you to track the complete lifecycle of each request. Timing information is captured to help identify performance bottlenecks and track response latencies. The middleware also monitors status codes, making it easy to detect and investigate errors or unexpected responses. Through error detection, you can quickly identify and debug issues in your service. Additionally, the performance metrics gathered by the middleware give you valuable insights into your service’s behavior and help you optimize its performance.
Add logging to gRPC services for consistent observability:
// Server-side unary interceptor
// Logs each RPC call with method and duration
svr := grpc.NewServer(
grpc.UnaryInterceptor(log.UnaryServerInterceptor(ctx)))
// Server-side stream interceptor
// Logs stream lifecycle events
svr := grpc.NewServer(
grpc.StreamInterceptor(log.StreamServerInterceptor(ctx)))
// Client-side interceptors
// Log outgoing RPCs for debugging
conn, err := grpc.DialContext(ctx,
"localhost:8080",
grpc.WithUnaryInterceptor(log.UnaryClientInterceptor()))
These interceptors provide comprehensive logging capabilities for your gRPC services. Each RPC method name is automatically logged, allowing you to track which endpoints are being called. The interceptors monitor status codes returned by your services, making it easy to identify successful calls versus failures. They measure the duration of each RPC call, helping you understand performance characteristics and identify slow requests. When errors occur, they are automatically logged with relevant context to aid debugging. The interceptors also capture metadata associated with each call, providing additional context about the RPC invocation such as authentication tokens or correlation IDs.
Use Clue’s logger with standard library compatible code:
// Create a standard logger that uses Clue's logging system
logger := log.AsStdLogger(ctx)
// Use standard log package functions
logger.Print("hello world") // Basic logging
logger.Printf("hello %s", "world") // Format string
logger.Println("hello world") // With newline
// Fatal logging functions
logger.Fatal("fatal error") // Log and exit
logger.Fatalf("fatal: %v", err) // Format and exit
logger.Fatalln("fatal error") // With newline
The compatibility layer provides several important benefits for teams adopting Clue’s logging system. It enables seamless integration with existing codebases that rely on the standard library logger, allowing teams to maintain functionality while transitioning to Clue. This makes it possible to gradually migrate different parts of an application to structured logging without disrupting operations.
The layer ensures consistent log handling across both new and legacy code paths, maintaining a unified logging experience. All logs, whether from modern or legacy components, flow through the same pipeline and receive the same formatting and processing. Additionally, by maintaining compatibility with Go’s standard library logging interface, teams can continue using familiar logging patterns while gaining the advanced features of Clue’s logging system.
Use Clue’s logger with Goa services:
// Create a Goa middleware compatible logger
logger := log.AsGoaMiddlewareLogger(ctx)
// Use logger in Goa service
svc := myservice.New(logger)
// Add logging middleware to all endpoints
endpoints := genmyservice.NewEndpoints(svc)
endpoints.Use(log.Endpoint)
Integrating Clue’s logging with Goa provides several important benefits for your service. The integration ensures consistent logging patterns across all your services, making it easier to monitor and debug your entire system. Through automatic context propagation, logs maintain their relationship with requests as they flow through different service components.
The integration automatically handles logging of requests and responses, giving you visibility into the data flowing through your APIs. When errors occur, they are automatically tracked and logged with appropriate context and stack traces. This makes troubleshooting much more efficient.
Additionally, the integration includes built-in performance monitoring capabilities. It tracks important metrics like request duration and helps you identify performance bottlenecks in your services. This comprehensive approach to logging and monitoring helps teams maintain reliable and performant services.
Log Levels:
Structured Data:
Performance:
Context:
For more information about logging:
Clue Log Package Complete documentation of Clue’s logging capabilities
Go Logger Interface Standard library logging interface documentation