name: logging-standards description: Structured logging best practices including levels, context propagation, and monitoring version: 1.0.0 category: development tags: [logging, monitoring, debugging, observability] author: ZGO Team updated: 2026-01-24
Logging Standards
๐ Purpose
This skill provides comprehensive logging standards for the ZGO Go backend project, ensuring consistent, structured, and production-ready logging across all modules.
๐ฏ When to Use
- Setting up logging for a new module
- Debugging production issues
- Implementing monitoring and observability
- Code review for logging practices
- Troubleshooting application behavior
โ๏ธ Prerequisites
- Understanding of Go and structured logging
- Familiarity with
logrusorzaplibraries - Knowledge of log levels and their meanings
๐ Core Principles
1. Wide Events (CRITICAL)
Principle: Emit comprehensive log entries with all relevant context in a single event.
Why: Simplifies log querying and analysis. One log event contains everything needed.
// โ
CORRECT - Wide event with full context
logger.WithFields(logrus.Fields{
"user_id": userID,
"request_id": requestID,
"action": "user_login",
"ip_address": ipAddr,
"user_agent": userAgent,
"login_time": time.Now().Unix(),
"status": "success",
}).Info("User logged in successfully")
// โ WRONG - Multiple narrow events
logger.Info("User login")
logger.Infof("User ID: %d", userID)
logger.Infof("IP: %s", ipAddr)
logger.Info("Login successful")
Benefits:
- One query finds all related information
- Easier correlation in log aggregation tools
- Better performance (fewer log calls)
- Cleaner log streams
2. High Cardinality & Dimensionality (CRITICAL)
Principle: Include high-cardinality identifiers (request_id, user_id, session_id) in every log.
Why: Enables precise filtering and correlation across distributed systems.
// โ
CORRECT - High cardinality fields
logger.WithFields(logrus.Fields{
"request_id": ctx.Value("request_id"), // Unique per request
"user_id": user.ID, // Unique per user
"tenant_id": tenant.ID, // Unique per tenant
"correlation_id": ctx.Value("correlation_id"), // Trace across services
"session_id": sessionID, // Unique per session
}).Info("Processing payment")
// โ WRONG - Low cardinality only
logger.WithFields(logrus.Fields{
"module": "payment",
"status": "processing",
}).Info("Processing payment")
Key Identifiers to Always Include:
request_id- Trace a single HTTP requestuser_id- Track user actionstenant_id- Multi-tenancy supportcorrelation_id- Distributed tracingsession_id- User session tracking
3. Business Context (CRITICAL)
Principle: Log business-relevant information, not just technical details.
Why: Helps non-technical stakeholders understand system behavior.
// โ
CORRECT - Business context
logger.WithFields(logrus.Fields{
"user_id": user.ID,
"order_id": order.ID,
"order_total": order.Total,
"payment_method": "credit_card",
"subscription": "premium",
"conversion": "trial_to_paid",
}).Info("User completed purchase")
// โ WRONG - Only technical details
logger.WithFields(logrus.Fields{
"table": "orders",
"action": "insert",
"rows": 1,
}).Info("Database insert")
Business Events to Log:
- User registration, login, logout
- Purchases, subscriptions, upgrades
- Important state changes
- Feature usage
- Errors affecting business operations
4. Environment Characteristics (CRITICAL)
Principle: Include environment information in every log (via global fields).
Why: Distinguishes logs from different environments and deployment contexts.
// โ
CORRECT - Set global fields at startup
logger := logrus.New()
logger.SetFormatter(&logrus.JSONFormatter{})
// Global context for all logs
logger = logger.WithFields(logrus.Fields{
"environment": os.Getenv("ENV"), // dev/staging/production
"service": "zgo-api",
"version": version.Version,
"hostname": hostname,
"region": os.Getenv("AWS_REGION"),
"pod_name": os.Getenv("POD_NAME"), // Kubernetes
}).Logger
Environment Fields:
environment- dev, staging, productionservice- Service nameversion- Application versionhostname- Server hostnameregion- Cloud regionpod_name- Container/pod identifier
5. Single Logger Instance (HIGH)
Principle: Use a single, globally configured logger throughout the application.
Why: Ensures consistent formatting, levels, and configuration.
// โ
CORRECT - Single logger (pkg/logger/logger.go)
package logger
import (
"os"
"github.com/sirupsen/logrus"
)
var Log *logrus.Logger
func Init() {
Log = logrus.New()
Log.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02T15:04:05.000Z",
FieldMap: logrus.FieldMap{
logrus.FieldKeyTime: "timestamp",
logrus.FieldKeyLevel: "level",
logrus.FieldKeyMsg: "message",
},
})
// Set log level from environment
if level, err := logrus.ParseLevel(os.Getenv("LOG_LEVEL")); err == nil {
Log.SetLevel(level)
} else {
Log.SetLevel(logrus.InfoLevel)
}
// Add global fields
Log = Log.WithFields(logrus.Fields{
"service": "zgo-api",
"environment": os.Getenv("ENV"),
"version": os.Getenv("VERSION"),
}).Logger
}
// Usage in modules
import "github.com/zgiai/zgo/pkg/logger"
func someFunction() {
logger.Log.WithFields(logrus.Fields{
"user_id": 123,
}).Info("User action")
}
6. Middleware Pattern (HIGH)
Principle: Use middleware to log all HTTP requests automatically.
Why: Consistent request logging, automatic request_id propagation.
// โ
CORRECT - Logging middleware
func LoggingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
requestID := uuid.New().String()
// Store request_id in context
c.Set("request_id", requestID)
// Log request start
logger.Log.WithFields(logrus.Fields{
"request_id": requestID,
"method": c.Request.Method,
"path": c.Request.URL.Path,
"ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
}).Info("Request started")
// Process request
c.Next()
// Log request completion
duration := time.Since(start)
logger.Log.WithFields(logrus.Fields{
"request_id": requestID,
"method": c.Request.Method,
"path": c.Request.URL.Path,
"status": c.Writer.Status(),
"duration": duration.Milliseconds(),
"size": c.Writer.Size(),
}).Info("Request completed")
}
}
// Register in main.go
r := gin.New()
r.Use(LoggingMiddleware())
7. Structure & Consistency (HIGH)
Principle: Use structured logging (JSON) with consistent field names.
Why: Machine-readable, queryable, works with log aggregation tools.
// โ
CORRECT - Structured JSON logging
logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02T15:04:05.000Z",
})
logger.WithFields(logrus.Fields{
"user_id": 123,
"action": "login",
"ip_address": "192.168.1.1",
}).Info("User logged in")
// Output:
// {"timestamp":"2026-01-24T18:20:01.000Z","level":"info","message":"User logged in","user_id":123,"action":"login","ip_address":"192.168.1.1"}
// โ WRONG - Unstructured text logging
logger.Infof("User %d logged in from %s", userID, ipAddr)
// Output:
// 2026-01-24 18:20:01 INFO User 123 logged in from 192.168.1.1
Standard Field Names:
- Use snake_case for consistency
- Use descriptive names:
user_idnotuid - Use common names:
timestampnottimeort
๐ Log Levels
Level Definitions
| Level | Usage | Example | Production |
|---|---|---|---|
| DEBUG | Detailed info for debugging | Variable values, flow trace | โ Disabled |
| INFO | General information | Request/response, state changes | โ Enabled |
| WARN | Warning conditions | Deprecated API usage, retries | โ Enabled |
| ERROR | Error conditions | Failed operations, exceptions | โ Enabled |
| FATAL | Critical errors | Startup failures, panics | โ Enabled |
When to Use Each Level
DEBUG
// โ
Use for development debugging
logger.WithFields(logrus.Fields{
"user_id": user.ID,
"query": query,
"result_count": len(results),
}).Debug("Database query executed")
// โ
Trace execution flow
logger.Debug("Entering function ProcessPayment")
logger.Debug("Validation passed")
logger.Debug("Exiting function ProcessPayment")
INFO
// โ
Normal application events
logger.WithFields(logrus.Fields{
"user_id": user.ID,
"action": "login",
}).Info("User logged in")
// โ
Important state changes
logger.WithFields(logrus.Fields{
"order_id": order.ID,
"status": "completed",
}).Info("Order completed")
WARN
// โ
Recoverable issues
logger.WithFields(logrus.Fields{
"user_id": user.ID,
"attempt": 3,
}).Warn("Password attempt failed, user locked")
// โ
Deprecated features
logger.Warn("Using deprecated API endpoint: /api/v1/users")
// โ
Resource limits approaching
logger.WithFields(logrus.Fields{
"disk_usage": "85%",
}).Warn("Disk usage approaching limit")
ERROR
// โ
Operation failures
logger.WithFields(logrus.Fields{
"user_id": user.ID,
"error": err.Error(),
}).Error("Failed to create user")
// โ
External service failures
logger.WithFields(logrus.Fields{
"service": "payment_gateway",
"error": err.Error(),
}).Error("Payment gateway unavailable")
FATAL
// โ
Unrecoverable errors (terminates app)
logger.WithFields(logrus.Fields{
"error": err.Error(),
}).Fatal("Failed to connect to database")
// โ ๏ธ Use sparingly - only for startup failures
๐ง Implementation Patterns
Pattern 1: Request-Scoped Logger
Create a logger with request context for each HTTP request.
// middleware/context_logger.go
func ContextLogger() gin.HandlerFunc {
return func(c *gin.Context) {
requestID := uuid.New().String()
c.Set("request_id", requestID)
// Create request-scoped logger
requestLogger := logger.Log.WithFields(logrus.Fields{
"request_id": requestID,
"path": c.Request.URL.Path,
"method": c.Request.Method,
})
c.Set("logger", requestLogger)
c.Next()
}
}
// Usage in handler
func (h *Handler) Get(c *gin.Context) {
log := c.MustGet("logger").(*logrus.Entry)
log.WithField("user_id", userID).Info("Fetching user")
user, err := h.service.GetByID(ctx, userID)
if err != nil {
log.WithError(err).Error("Failed to fetch user")
return
}
log.Info("User fetched successfully")
}
Pattern 2: Context Propagation
Pass logger through context for service/repository layers.
// pkg/logger/context.go
type loggerKey struct{}
func WithLogger(ctx context.Context, log *logrus.Entry) context.Context {
return context.WithValue(ctx, loggerKey{}, log)
}
func FromContext(ctx context.Context) *logrus.Entry {
if log, ok := ctx.Value(loggerKey{}).(*logrus.Entry); ok {
return log
}
return logger.Log.WithField("source", "unknown")
}
// Usage in service
func (s *service) Create(ctx context.Context, req *CreateRequest) error {
log := logger.FromContext(ctx)
log.WithField("username", req.Username).Info("Creating user")
user, err := s.repo.Create(ctx, req)
if err != nil {
log.WithError(err).Error("Failed to create user in database")
return err
}
log.WithField("user_id", user.ID).Info("User created successfully")
return nil
}
Pattern 3: Error Logging
Always log errors with full context.
// โ
CORRECT - Log with context and error
func (s *service) ProcessPayment(ctx context.Context, orderID uint, amount float64) error {
log := logger.FromContext(ctx)
log.WithFields(logrus.Fields{
"order_id": orderID,
"amount": amount,
}).Info("Processing payment")
if err := s.paymentGateway.Charge(amount); err != nil {
log.WithFields(logrus.Fields{
"order_id": orderID,
"amount": amount,
"error": err.Error(),
"gateway": "stripe",
}).Error("Payment gateway charge failed")
return fmt.Errorf("charge failed: %w", err)
}
log.WithField("order_id", orderID).Info("Payment processed successfully")
return nil
}
Pattern 4: Sensitive Data Handling
Never log sensitive information.
// โ WRONG - Logging sensitive data
logger.WithFields(logrus.Fields{
"password": user.Password,
"credit_card": payment.CardNumber,
"ssn": user.SSN,
}).Info("User created")
// โ
CORRECT - Mask or omit sensitive data
logger.WithFields(logrus.Fields{
"user_id": user.ID,
"email": user.Email,
"card_last4": payment.CardLast4, // Only last 4 digits
}).Info("User created")
// โ
CORRECT - Helper function
func sanitizeUser(user *User) logrus.Fields {
return logrus.Fields{
"user_id": user.ID,
"email": user.Email,
"username": user.Username,
// Password excluded
}
}
logger.WithFields(sanitizeUser(user)).Info("User logged in")
๐ซ Anti-Patterns to Avoid
1. String Interpolation in Messages
// โ WRONG - Variables in message string
logger.Infof("User %d logged in from %s", userID, ipAddr)
// โ
CORRECT - Variables in structured fields
logger.WithFields(logrus.Fields{
"user_id": userID,
"ip_address": ipAddr,
}).Info("User logged in")
2. Multiple Logs for Single Event
// โ WRONG - Multiple narrow logs
logger.Info("Starting payment")
logger.Infof("Amount: %.2f", amount)
logger.Infof("Order: %d", orderID)
logger.Info("Payment completed")
// โ
CORRECT - Single wide log
logger.WithFields(logrus.Fields{
"order_id": orderID,
"amount": amount,
"status": "completed",
}).Info("Payment processed")
3. Logging Without Context
// โ WRONG - No context
logger.Info("Error occurred")
// โ
CORRECT - With full context
logger.WithFields(logrus.Fields{
"user_id": userID,
"operation": "create_order",
"error": err.Error(),
}).Error("Failed to create order")
4. Excessive DEBUG Logging
// โ WRONG - Too verbose
logger.Debug("Entering function")
logger.Debug("x = 1")
logger.Debug("y = 2")
logger.Debug("Calling helper")
logger.Debug("Helper returned")
logger.Debug("Exiting function")
// โ
CORRECT - Meaningful DEBUG logs
logger.WithField("input", input).Debug("Processing input")
logger.WithField("result", result).Debug("Function completed")
โ Verification Checklist
- Using structured logging (JSON) not text
- Single logger instance configured at startup
- Log levels correctly set (INFO in production)
- Global fields included (service, environment, version)
- Request middleware logs all HTTP requests
- Request_id propagated through context
- High-cardinality fields included (user_id, request_id)
- Business context logged for important events
- Errors logged with full context
- No sensitive data in logs
- Consistent field naming (snake_case)
- Wide events (not multiple narrow logs)
๐ Complete Example
See .agent/skills/logging-standards/examples/logging-setup.go
๐ง Validation Script
Run the logging standards validation:
.agent/skills/logging-standards/scripts/validate-logging.sh <module_name>
๐ Related Skills
api-development: API logging patternscoding-standards: General code qualitymodule-creation: Module structure
๐ Quick Reference
// Setup (main.go or init)
logger.Init()
// HTTP middleware
r.Use(middleware.ContextLogger())
// In handlers
log := c.MustGet("logger").(*logrus.Entry)
log.WithField("user_id", userID).Info("Action performed")
// In services
log := logger.FromContext(ctx)
log.WithError(err).Error("Operation failed")
// Common patterns
log.Info("Normal event")
log.Warn("Warning condition")
log.WithError(err).Error("Error occurred")
log.WithFields(logrus.Fields{
"user_id": 123,
"action": "login",
}).Info("User action")
Version: 1.0.0
Last Updated: 2026-01-24
Maintainer: ZGO Team