Skip to content

Logging System

Blueprint provides a structured logging system built on top of zerolog that offers consistent logging patterns across different components.

Features

  • Structured logging with standardized field names
  • Context-aware logging for tracking requests across multiple services
  • Distributed tracing with trace and request IDs
  • Component-specific logging for HTTP and Kafka operations
  • Log levels for different severity of messages
  • Performance-oriented with minimal allocations

Basic Usage

import "github.com/oddbit-project/blueprint/log"

// Create a logger for a module
logger := log.New("mymodule")

// Log messages at different levels
logger.Info("Application started", log.KV{
    "version": "1.0.0",
})

logger.Debug("Processing item", log.KV{
    "item_id": 123,
})

// Log errors with stack traces
err := someOperation()
if err != nil {
    logger.Error(err, "Failed to process item", log.KV{
        "item_id": 123,
    })
}

Context-Aware Logging

import "github.com/oddbit-project/blueprint/log"

// Create a request context with trace ID
ctx, logger := log.NewRequestContext(context.Background(), "api")

// Add fields to the logger
ctx = log.WithField(ctx, "user_id", userId)

// Log using the context
log.Info(ctx, "Processing user request")

// Pass the context to other functions
processRequest(ctx, request)

// In other functions, retrieve the logger from context
func processRequest(ctx context.Context, req Request) {
    logger := log.FromContext(ctx)
    logger.Info("Processing request details")

    // Or use helper functions
    log.Info(ctx, "Alternative way to log")
}

HTTP Request Logging

import (
    "github.com/gin-gonic/gin"
    "github.com/oddbit-project/blueprint/log"
    "github.com/oddbit-project/provider/httpserver"
)

// Use the HTTP logging middleware
router.Use(httpserver.HTTPLogMiddleware("api"))

// Log within handlers
func handler(c *gin.Context) {
    // Get the request logger
    logger := httpserver.GetRequestLogger(c)

    // Or use helper functions
    httpserver.RequestInfo(c, "Processing API request", log.KV{
        "request_data": someData,
    })

    // Error handling
    if err := someOperation(); err != nil {
        httpserver.RequestError(c, err, "Operation failed")
        return
    }
}

Kafka Message Logging

Kafka consumer example:

import (
    "context"
    "github.com/oddbit-project/blueprint/log"
    "github.com/oddbit-project/blueprint/provider/kafka"
)

ctx := context.Background()

// Producer logging
producer, _ := kafka.NewProducer(cfg, nil)
err := producer.WriteJson(ctx, data)

// Consumer with logging
consumer, _ := kafka.NewConsumer(cfg, nil)
consumer.Subscribe(ctx, func(ctx context.Context, msg kafka.Message) error {
    // use consumer logger
    consumer.Logger.Info("processing message...")

    // log kafka message
    kafka.LogMessageReceived(consumer.Logger, msg, consumer.GetConfig().Group)    

    // Add your processing logic
    // ...

    return nil
})

Configuration

import "github.com/oddbit-project/blueprint/log"

// Create a configuration
cfg := log.NewDefaultConfig()
cfg.Level = "debug"           // log level: debug, info, warn, error
cfg.Format = "pretty"        // output format: pretty or json
cfg.IncludeTimestamp = true   // include timestamp in logs
cfg.IncludeCaller = true      // include caller information
cfg.IncludeHostname = true    // add a "hostname" field (os.Hostname())

// Configure the global logger
err := log.Configure(cfg)
if err != nil {
    panic(err)
}

log.Configure() also applies process-wide zerolog settings (timestamp format and global level), which are not safe to change while other goroutines are logging — call it once, during startup. Building a logger with cfg.Logger() or cfg.ModuleLogger() has no process-wide effect, and such a logger uses zerolog's default timestamp layout rather than cfg.TimeFormat.

With IncludeHostname, every line carries a hostname field (log.LogHostnameKey) holding os.Hostname(). This applies to loggers built with cfg.Logger() or cfg.ModuleLogger(), and to log.New() / log.NewWithComponent() loggers created after log.Configure(); a logger created before Configure() does not carry it. Do not add a hostname field of your own while it is enabled, or JSON lines carry the key twice.

With IncludeCaller, the caller field reports the code that called the logging method. cfg.CallerSkipFrames (default 3) controls that; raise it if you wrap the logger in helpers of your own, one per extra call level.

Formatted Logging Methods

In addition to the standard logging methods, the logger provides formatted variants that support printf-style formatting:

logger := log.New("mymodule")

// Formatted logging methods
logger.Debugf("Processing item %d of %d", current, total)
logger.Infof("User %s logged in from %s", username, ipAddress)
logger.Warnf("Cache hit rate below threshold: %.2f%%", hitRate)
logger.Errorf(err, "Failed to process request %s", requestID)
logger.Fatalf(err, "Critical failure in %s", component)

Context-Based Formatted Logging

When using context-aware logging, formatted variants are also available:

// These functions extract the logger from context and use formatted output
log.Debugf(ctx, "Processing item %d", itemID)
log.Infof(ctx, "Request completed in %dms", duration)
log.Warnf(ctx, "Retry attempt %d of %d", attempt, maxRetries)
log.Errorf(ctx, err, "Failed to connect to %s", hostname)
log.Fatalf(ctx, err, "Unrecoverable error in %s", service)

Additional Utility Functions

Context Utilities

// MergeContextFields combines multiple field sets into one
fields := log.MergeContextFields(
    log.ContextFields{"user_id": userID},
    log.ContextFields{"request_id": reqID},
)

// NewTraceID generates a new unique trace ID
traceID := log.NewTraceID()

// ExtractLoggerFromContext retrieves the logger from context
logger := log.ExtractLoggerFromContext(ctx)

Logger Utility Methods

logger := log.New("mymodule")

// Get module information
info := logger.ModuleInfo()  // Returns the module name

// Get hostname
hostname := logger.Hostname()  // Returns the system hostname

// Set custom output writer
logger.WithOutput(customWriter)

// Access the underlying zerolog logger for advanced usage
zl := logger.GetZerolog()

Best Practices

  1. Include relevant fields: Add meaningful fields to help with debugging and analysis
  2. Be consistent with log levels:
  3. DEBUG: Detailed information for debugging
  4. INFO: General operational information
  5. WARN: Situations that might cause issues
  6. ERROR: Errors that prevent normal operation
  7. FATAL: Critical errors that require shutdown
  8. Sanitize sensitive data: Don't log passwords, tokens, or other sensitive information
  9. Use structured logging: Avoid string concatenation or formatting in log messages