logger Package
Structured JSON logging with zerolog for LaResto microservices. Logs are output to stdout for collection by Elasticsearch/Kibana.
Features
- Structured JSON Logs: Machine-readable, queryable logs
- Multiple Log Levels: Debug, Info, Warn, Error, Fatal
- Context Propagation: Automatic request ID and user ID extraction
- Type-Safe Fields: Optimized logging for common types
- Zero Allocation: Uses zerolog for high performance
- Permanent Fields: Attach fields to logger instances
Installation
import "github.com/LaRestoOU/laresto-go-common/pkg/logger"
Quick Start
Basic Usage
// Create a logger
log := logger.New(logger.Config{
Level: "info",
ServiceName: "auth-service",
Environment: "production",
})
// Log messages
log.Info("User logged in", "user_id", "123", "email", "user@example.com")
log.Warn("Rate limit approaching", "current", 95, "limit", 100)
log.Error("Database connection failed", err, "retry_count", 3)
Development Logger
// Use default configuration for development
log := logger.NewDefault()
log.Debug("Starting authentication process")
Log Levels
| Level |
Use Case |
Example |
| Debug |
Development, detailed debugging |
log.Debug("SQL query", "query", sql) |
| Info |
Important events, user actions |
log.Info("User logged in", "user_id", id) |
| Warn |
Warning conditions, recoverable issues |
log.Warn("Cache miss", "key", key) |
| Error |
Error conditions, failures |
log.Error("Payment failed", err) |
| Fatal |
Critical errors, process exits |
log.Fatal("Database unreachable", err) |
Configuration
cfg := logger.Config{
// Minimum log level (debug, info, warn, error)
Level: "info",
// Service name (appears in all logs)
ServiceName: "auth-service",
// Environment (development, staging, production)
Environment: "production",
// Output writer (defaults to os.Stdout)
Output: os.Stdout,
}
log := logger.New(cfg)
Context-Aware Logging
Extract request ID and user ID from context automatically:
func HandleRequest(c *gin.Context) {
// Create context-aware logger
log := logger.New(config).WithContext(c.Request.Context())
// All logs include request_id and user_id
log.Info("Processing order")
log.Error("Validation failed", err)
}
// Output:
// {"level":"info","request_id":"abc-123","user_id":"456","message":"Processing order"}
Permanent Fields
Attach fields that appear in all logs:
// Single field
moduleLogger := log.WithField("module", "payment")
moduleLogger.Info("Processing payment") // includes "module":"payment"
// Multiple fields
authLogger := log.WithFields(
"module", "auth",
"version", "v2",
)
authLogger.Info("Login attempt") // includes both fields
Field Types
The logger optimizes for common types:
log.Info("Request completed",
"user_id", "123", // string
"status_code", 200, // int
"duration_ms", int64(150), // int64
"success", true, // bool
"duration", 150*time.Millisecond, // duration
"timestamp", time.Now(), // time
"error", err, // error
"metadata", map[string]string{...}, // any other type
)
Usage Patterns
Service Initialization
func main() {
log := logger.New(logger.Config{
Level: getEnv("LOG_LEVEL", "info"),
ServiceName: "auth-service",
Environment: getEnv("ENVIRONMENT", "production"),
})
log.Info("Service starting",
"port", 8080,
"version", "v1.0.0",
)
// Pass logger to handlers
handler := NewAuthHandler(log)
}
HTTP Request Logging
func (h *Handler) Login(c *gin.Context) {
log := h.logger.WithContext(c.Request.Context())
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
log.Warn("Invalid login request", "error", err)
c.JSON(400, gin.H{"error": "Invalid request"})
return
}
log.Info("Login attempt", "email", req.Email)
user, err := h.authService.Login(req.Email, req.Password)
if err != nil {
log.Error("Login failed", err, "email", req.Email)
c.JSON(401, gin.H{"error": "Unauthorized"})
return
}
log.Info("Login successful", "user_id", user.ID)
c.JSON(200, user)
}
Background Processing
func (w *Worker) ProcessOrder(ctx context.Context, order Order) {
log := w.logger.WithContext(ctx).WithField("order_id", order.ID)
log.Info("Processing order started")
if err := w.validateOrder(order); err != nil {
log.Error("Order validation failed", err)
return
}
log.Info("Order validated")
if err := w.processPayment(order); err != nil {
log.Error("Payment processing failed", err)
return
}
log.Info("Order processing completed",
"duration", time.Since(order.CreatedAt),
)
}
All logs are JSON-formatted:
{
"level": "info",
"service": "auth-service",
"environment": "production",
"time": "2024-12-27T10:30:00Z",
"message": "User logged in",
"request_id": "abc-123",
"user_id": "456",
"email": "user@example.com"
}
Best Practices
DO ✅
// Use structured fields
log.Info("User logged in", "user_id", userID, "ip", ipAddress)
// Include context
log := logger.WithContext(ctx)
// Use appropriate levels
log.Debug("Cache hit", "key", key) // Development only
log.Info("Order created", "order_id", id) // Important events
log.Error("Payment failed", err, "order_id", id) // Errors
// Add permanent fields for modules
paymentLog := log.WithField("module", "payment")
DON'T ❌
// Don't log sensitive data
log.Info("Login", "password", password) // NEVER!
log.Info("Payment", "card_number", card) // NEVER!
log.Info("Token", "jwt", token) // NEVER!
// Don't use string concatenation
log.Info("User " + userID + " logged in") // Use fields instead
// Don't log at wrong level
log.Error("User logged in", nil) // Not an error!
log.Debug("Payment failed", err) // Should be Error!
// Don't log too much in production
log.Debug("Processing...", "step", 1) // Set level to "info" in prod
Sensitive Data Guidelines
NEVER log:
- Passwords (plain or hashed)
- JWT tokens or session IDs
- Credit card numbers
- Personal identification numbers
- API keys or secrets
- Full email addresses in production (hash or mask them)
Safe to log:
- User IDs (numeric or UUID)
- Request IDs
- Timestamps
- Status codes
- Durations
- Error messages (without sensitive context)
Integration with ELK Stack
Logs are written to stdout in JSON format and collected by Docker's logging driver:
# docker-compose.yml
services:
auth-service:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Logs flow: Service → Docker → Filebeat → Elasticsearch → Kibana
Querying Logs in Kibana
# Find all errors for a specific user
level:"error" AND user_id:"123"
# Find slow requests (>1 second)
duration_ms:>1000
# Find logs for a specific request
request_id:"abc-123"
# Find authentication failures
service:"auth-service" AND message:"Login failed"
- Zero Allocation: zerolog doesn't allocate memory during logging
- Lazy Evaluation: Log fields only serialized if level is enabled
- Buffered I/O: Logs are buffered before writing to stdout
- Type Optimization: Common types have optimized serialization paths
Benchmarks (from zerolog):
- ~50ns per log line (with 10 fields)
- Zero allocations for basic types
- 10x faster than standard library log
Testing
func TestHandler_Login(t *testing.T) {
// Use a buffer to capture logs
buf := &bytes.Buffer{}
log := logger.New(logger.Config{
Level: "debug",
Output: buf,
})
handler := NewHandler(log)
// Test handler
handler.Login(ctx, req)
// Verify logs
logs := buf.String()
if !strings.Contains(logs, "Login successful") {
t.Error("Expected login success log")
}
}
iOS Developer Notes
Logger is similar to:
OSLog in iOS - structured logging framework
- Custom logging wrapper around
print()
- Analytics/crash reporting SDKs
Comparison:
// iOS OSLog
os_log(.info, log: logger, "User %{public}@ logged in", userID)
// Go logger
log.Info("User logged in", "user_id", userID)
Key concepts:
- Log levels = OSLog's
.debug, .info, .error, .fault
- Structured fields = Like passing dictionaries to analytics
- Context = Similar to MDC (Mapped Diagnostic Context)
- JSON output = Like serializing to Crashlytics/Firebase
API Reference
Types
type Logger struct {
// Internal zerolog instance
}
type Config struct {
Level string // "debug", "info", "warn", "error"
ServiceName string // Service identifier
Environment string // "development", "staging", "production"
Output io.Writer // Where to write logs (default: os.Stdout)
}
Functions
// New creates a configured logger
func New(cfg Config) *Logger
// NewDefault creates a development logger
func NewDefault() *Logger
Methods
// Logging methods
func (l *Logger) Debug(msg string, fields ...interface{})
func (l *Logger) Info(msg string, fields ...interface{})
func (l *Logger) Warn(msg string, fields ...interface{})
func (l *Logger) Error(msg string, err error, fields ...interface{})
func (l *Logger) Fatal(msg string, err error, fields ...interface{})
// Context and field methods
func (l *Logger) WithContext(ctx context.Context) *Logger
func (l *Logger) WithField(key string, value interface{}) *Logger
func (l *Logger) WithFields(fields ...interface{}) *Logger
Examples
See examples/simple-service for a complete example of using the logger package.
License
MIT License - see LICENSE file for details