Skip to content

Logger Package

Structured logging with context support and slog wrapper


Overview

The logger package provides a thin wrapper around Go's standard log/slog with additional convenience methods for context-aware logging. It supports multiple output formats, log levels, and automatic context field extraction.

Status: Production-ready
Tests: 15 tests, 95% coverage
Location: pkg/logger/


Features

  • Structured Logging - JSON or text format output
  • Context Integration - Automatic extraction of request ID, user ID, trace ID
  • Customizable Levels - Debug, Info, Warn, Error
  • Source Location - Optional file/line information
  • Time Formatting - Configurable timestamp format
  • Thread-Safe - Safe for concurrent use
  • Production-Ready - Used in all Promenade services

Quick Start

Basic Initialization

go
package main

import (
    "github.com/basilex/promenade/pkg/logger"
)

func main() {
    // Initialize default logger
    logger.Init(logger.Config{
        Level:      "info",
        Format:     "text",
        AddSource:  true,
        TimeFormat: time.RFC3339,
    })
    
    logger.Info("Application started", "version", "1.0.0")
}

Configuration

go
type Config struct {
    Level      string    // "debug", "info", "warn", "error"
    Format     string    // "json", "text"
    Output     io.Writer // defaults to os.Stdout
    AddSource  bool      // include file:line in logs
    TimeFormat string    // defaults to RFC3339
}

Usage Examples

Basic Logging

go
// Info level
logger.Info("User logged in", "user_id", "123", "ip", "192.168.1.1")

// Debug level
logger.Debug("Processing request", "endpoint", "/api/users")

// Warning
logger.Warn("Rate limit approaching", "current", 95, "max", 100)

// Error
logger.Error("Failed to connect to database", 
    "error", err, 
    "host", "localhost:5432",
)

Output (text format):

time=2025-12-29T10:30:45+02:00 level=INFO msg="User logged in" user_id=123 ip=192.168.1.1
time=2025-12-29T10:30:46+02:00 level=ERROR msg="Failed to connect" error="connection refused"

Output (JSON format):

json
{"time":"2025-12-29T10:30:45+02:00","level":"INFO","msg":"User logged in","user_id":"123","ip":"192.168.1.1"}
{"time":"2025-12-29T10:30:46+02:00","level":"ERROR","msg":"Failed to connect","error":"connection refused"}

Context-Aware Logging

go
// Extract logger from context
func (h *UserHandler) Create(c *gin.Context) {
    ctx := c.Request.Context()
    log := logger.FromContext(ctx)
    
    log.Info("Creating user", 
        slog.String("email", req.Email),
        slog.String("ip", c.ClientIP()),
    )
    
    user, err := h.usecase.CreateUser(ctx, req.Email, req.Password)
    if err != nil {
        log.Error("Failed to create user", 
            slog.Any("error", err),
            slog.String("email", req.Email),
        )
        return
    }
    
    log.Info("User created", 
        slog.String("user_id", user.ID.String()),
    )
}

With Context Fields

go
// Add context fields (request ID, user ID, etc.)
ctx = logger.WithField(ctx, "request_id", requestID)
ctx = logger.WithField(ctx, "user_id", userID)

// Logger automatically includes these fields
logger.FromContext(ctx).Info("Processing request")
// Output: time=... level=INFO msg="Processing request" request_id=abc123 user_id=01JGABC...

API Reference

Initialization

Init

go
func Init(config Config)

Initializes the global logger with configuration.

Example:

go
logger.Init(logger.Config{
    Level:  "debug",
    Format: "json",
})

Logging Functions

Debug

go
func Debug(msg string, args ...any)

Logs at DEBUG level (development only).

Info

go
func Info(msg string, args ...any)

Logs at INFO level (general information).

Warn

go
func Warn(msg string, args ...any)

Logs at WARN level (potential issues).

Error

go
func Error(msg string, args ...any)

Logs at ERROR level (errors that need attention).

Fatal

go
func Fatal(msg string, args ...any)

Logs at ERROR level and calls os.Exit(1).

Context Functions

FromContext

go
func FromContext(ctx context.Context) *slog.Logger

Extracts logger from context with automatic field injection.

Example:

go
log := logger.FromContext(ctx)
log.Info("User action", slog.String("action", "login"))

WithField

go
func WithField(ctx context.Context, key string, value any) context.Context

Adds field to context for automatic inclusion in logs.

Example:

go
ctx = logger.WithField(ctx, "request_id", "abc123")
logger.FromContext(ctx).Info("Request received") // Includes request_id

Configuration Examples

Development (Text Format)

yaml
# config/app.postgres-dev.yaml or config/app.sqlite-dev.yaml
logging:
  level: "debug"
  format: "text"
  add_source: true
go
logger.Init(logger.Config{
    Level:     cfg.Logging.Level,
    Format:    cfg.Logging.Format,
    AddSource: cfg.Logging.AddSource,
})

Production (JSON Format)

yaml
# config/app.postgres-prod.yaml
logging:
  level: "info"
  format: "json"
  add_source: false
go
logger.Init(logger.Config{
    Level:  "info",
    Format: "json",
})

Best Practices

DO

Use structured fields:

go
logger.Info("User created", 
    "user_id", userID.String(),
    "email", email,
)

Use context logger in handlers:

go
log := logger.FromContext(ctx)
log.Info("Processing request")

Log errors with context:

go
if err != nil {
    logger.Error("Database error", 
        "error", err,
        "query", queryName,
    )
}

Use appropriate log levels:

  • DEBUG: Development debugging
  • INFO: Normal operations
  • WARN: Potential issues
  • ERROR: Actual errors

DON'T

Don't use string concatenation:

go
// Bad
logger.Info("User " + userID + " created")

// Good
logger.Info("User created", "user_id", userID)

Don't log sensitive data:

go
// Bad
logger.Info("Login attempt", "password", password)

// Good
logger.Info("Login attempt", "email", email)

Don't use global logger in domain logic:

go
// Bad (in usecase)
logger.Info("Creating user")

// Good (pass context)
logger.FromContext(ctx).Info("Creating user")

Testing

Running Tests

bash
# Run logger tests
go test ./pkg/logger -v

# With coverage
go test ./pkg/logger -cover

Test Example

go
func TestLogger_Info(t *testing.T) {
    var buf bytes.Buffer
    
    logger.Init(logger.Config{
        Level:  "info",
        Format: "json",
        Output: &buf,
    })
    
    logger.Info("test message", "key", "value")
    
    output := buf.String()
    assert.Contains(t, output, "test message")
    assert.Contains(t, output, "key")
}

Integration

Gin Middleware

go
import (
    "github.com/gin-gonic/gin"
    "github.com/basilex/promenade/pkg/logger"
)

func LoggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        requestID := uuid.NewString()
        
        // Add request ID to context
        ctx := logger.WithField(c.Request.Context(), "request_id", requestID)
        c.Request = c.Request.WithContext(ctx)
        
        // Process request
        c.Next()
        
        // Log request completion
        logger.FromContext(ctx).Info("Request completed",
            slog.String("method", c.Request.Method),
            slog.String("path", c.Request.URL.Path),
            slog.Int("status", c.Writer.Status()),
            slog.Duration("duration", time.Since(start)),
        )
    }
}

Built with Domain-Driven Design and Go