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
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
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
// 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):
{"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
// 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
// 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
func Init(config Config)Initializes the global logger with configuration.
Example:
logger.Init(logger.Config{
Level: "debug",
Format: "json",
})Logging Functions
Debug
func Debug(msg string, args ...any)Logs at DEBUG level (development only).
Info
func Info(msg string, args ...any)Logs at INFO level (general information).
Warn
func Warn(msg string, args ...any)Logs at WARN level (potential issues).
Error
func Error(msg string, args ...any)Logs at ERROR level (errors that need attention).
Fatal
func Fatal(msg string, args ...any)Logs at ERROR level and calls os.Exit(1).
Context Functions
FromContext
func FromContext(ctx context.Context) *slog.LoggerExtracts logger from context with automatic field injection.
Example:
log := logger.FromContext(ctx)
log.Info("User action", slog.String("action", "login"))WithField
func WithField(ctx context.Context, key string, value any) context.ContextAdds field to context for automatic inclusion in logs.
Example:
ctx = logger.WithField(ctx, "request_id", "abc123")
logger.FromContext(ctx).Info("Request received") // Includes request_idConfiguration Examples
Development (Text Format)
# config/app.postgres-dev.yaml or config/app.sqlite-dev.yaml
logging:
level: "debug"
format: "text"
add_source: truelogger.Init(logger.Config{
Level: cfg.Logging.Level,
Format: cfg.Logging.Format,
AddSource: cfg.Logging.AddSource,
})Production (JSON Format)
# config/app.postgres-prod.yaml
logging:
level: "info"
format: "json"
add_source: falselogger.Init(logger.Config{
Level: "info",
Format: "json",
})Best Practices
DO
Use structured fields:
logger.Info("User created",
"user_id", userID.String(),
"email", email,
)Use context logger in handlers:
log := logger.FromContext(ctx)
log.Info("Processing request")Log errors with context:
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:
// Bad
logger.Info("User " + userID + " created")
// Good
logger.Info("User created", "user_id", userID)Don't log sensitive data:
// Bad
logger.Info("Login attempt", "password", password)
// Good
logger.Info("Login attempt", "email", email)Don't use global logger in domain logic:
// Bad (in usecase)
logger.Info("Creating user")
// Good (pass context)
logger.FromContext(ctx).Info("Creating user")Testing
Running Tests
# Run logger tests
go test ./pkg/logger -v
# With coverage
go test ./pkg/logger -coverTest Example
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
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)),
)
}
}Related Documentation
- Quick Start Guide - Get started with Promenade
- Testing Guide - Testing patterns
- Contributing Guide - Development workflow
- GitHub Repository - Source code