Logging Integration¶
When errors occur in your application, having detailed, structured logs is crucial for understanding what went wrong and why. The ewrap package provides a flexible logging system that integrates seamlessly with popular logging frameworks while maintaining a clean, consistent interface for error logging.
Understanding Logging in ewrap¶
The logging system in ewrap is built around a simple yet powerful interface that can adapt to any logging framework. When an error occurs, ewrap can automatically log not just the error message, but also the stack trace, metadata, and contextual information that helps tell the complete story of what happened.
The Logger Interface¶
Let's start by understanding the core logging interface:
type Logger interface {
Error(msg string, keysAndValues ...interface{})
Debug(msg string, keysAndValues ...interface{})
Info(msg string, keysAndValues ...interface{})
}
This interface is intentionally simple to make it easy to adapt any logging framework to work with ewrap. The variadic keysAndValues
parameter allows for structured logging where key-value pairs provide additional context.
Built-in Logging Adapters¶
ewrap provides adapters for popular logging frameworks. Here's how to use them:
Zap Logger Integration¶
import (
"go.uber.org/zap"
"github.com/hyp3rd/ewrap/adapters"
)
func setupZapLogger() error {
// Create a production-ready Zap logger
zapLogger, err := zap.NewProduction()
if err != nil {
return err
}
// Create the adapter
logger := adapters.NewZapAdapter(zapLogger)
// Create an error with logging enabled
err = ewrap.New("operation failed",
ewrap.WithLogger(logger),
ewrap.WithContext(ctx, ewrap.ErrorTypeInternal, ewrap.SeverityError)).
WithMetadata("operation", "user_update").
WithMetadata("user_id", userID)
// The error will be automatically logged with all context
return err
}
Logrus Integration¶
import (
"github.com/sirupsen/logrus"
"github.com/hyp3rd/ewrap/adapters"
)
func setupLogrusLogger() error {
// Configure Logrus
logrusLogger := logrus.New()
logrusLogger.SetFormatter(&logrus.JSONFormatter{})
// Create the adapter
logger := adapters.NewLogrusAdapter(logrusLogger)
// Use the logger with ewrap
return ewrap.New("database connection failed",
ewrap.WithLogger(logger),
ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityCritical))
}
Zerolog Integration¶
import (
"github.com/rs/zerolog"
"github.com/hyp3rd/ewrap/adapters"
)
func setupZerolog() error {
// Configure Zerolog
zerologLogger := zerolog.New(os.Stdout).With().Timestamp().Logger()
// Create the adapter
logger := adapters.NewZerologAdapter(zerologLogger)
// Use with ewrap
return ewrap.New("request validation failed",
ewrap.WithLogger(logger),
ewrap.WithContext(ctx, ewrap.ErrorTypeValidation, ewrap.SeverityWarning))
}
Advanced Logging Patterns¶
Contextual Logging¶
Here's how to create rich, contextual logs that capture the full story of an error:
func processOrder(ctx context.Context, order Order) error {
logger := getContextLogger(ctx)
// Create an operation logger that will track the entire process
opLogger := &OperationLogger{
Logger: logger,
Operation: "process_order",
StartTime: time.Now(),
Context: map[string]interface{}{
"order_id": order.ID,
"user_id": order.UserID,
},
}
// Log operation start
opLogger.Info("starting order processing")
if err := validateOrder(order); err != nil {
return ewrap.Wrap(err, "order validation failed",
ewrap.WithLogger(opLogger),
ewrap.WithContext(ctx, ewrap.ErrorTypeValidation, ewrap.SeverityError)).
WithMetadata("validation_time", time.Since(opLogger.StartTime))
}
if err := processPayment(order); err != nil {
return ewrap.Wrap(err, "payment processing failed",
ewrap.WithLogger(opLogger),
ewrap.WithContext(ctx, ewrap.ErrorTypePayment, ewrap.SeverityCritical)).
WithMetadata("processing_time", time.Since(opLogger.StartTime))
}
// Log successful completion
opLogger.Info("order processed successfully",
"duration", time.Since(opLogger.StartTime))
return nil
}
Creating a Custom Logger¶
You might want to create a custom logger that adds specific functionality:
type CustomLogger struct {
logger Logger
component string
env string
}
func NewCustomLogger(baseLogger Logger, component string) *CustomLogger {
return &CustomLogger{
logger: baseLogger,
component: component,
env: os.Getenv("APP_ENV"),
}
}
func (l *CustomLogger) Error(msg string, keysAndValues ...interface{}) {
// Add standard context to all error logs
enrichedKV := append([]interface{}{
"component", l.component,
"environment", l.env,
"timestamp", time.Now().UTC(),
}, keysAndValues...)
l.logger.Error(msg, enrichedKV...)
}
func (l *CustomLogger) Debug(msg string, keysAndValues ...interface{}) {
enrichedKV := append([]interface{}{
"component", l.component,
"environment", l.env,
}, keysAndValues...)
l.logger.Debug(msg, enrichedKV...)
}
func (l *CustomLogger) Info(msg string, keysAndValues ...interface{}) {
enrichedKV := append([]interface{}{
"component", l.component,
"environment", l.env,
}, keysAndValues...)
l.logger.Info(msg, enrichedKV...)
}
Logging with Circuit Breakers¶
Combining logging with circuit breakers provides insight into system health:
type MonitoredCircuitBreaker struct {
*ewrap.CircuitBreaker
logger Logger
name string
}
func NewMonitoredCircuitBreaker(name string, maxFailures int, timeout time.Duration, logger Logger) *MonitoredCircuitBreaker {
cb := ewrap.NewCircuitBreaker(name, maxFailures, timeout)
return &MonitoredCircuitBreaker{
CircuitBreaker: cb,
logger: logger,
name: name,
}
}
func (m *MonitoredCircuitBreaker) RecordFailure() {
m.CircuitBreaker.RecordFailure()
m.logger.Error("circuit breaker failure recorded",
"breaker_name", m.name,
"current_state", "open",
"timestamp", time.Now())
}
func (m *MonitoredCircuitBreaker) RecordSuccess() {
m.CircuitBreaker.RecordSuccess()
m.logger.Info("circuit breaker success recorded",
"breaker_name", m.name,
"current_state", "closed",
"timestamp", time.Now())
}
Best Practices¶
1. Structured Logging¶
Always use structured logging for better searchability:
// Good - structured logging
logger.Error("database query failed",
"query", queryString,
"duration_ms", duration.Milliseconds(),
"affected_rows", 0)
// Avoid - unstructured logging
logger.Error(fmt.Sprintf("database query failed: %s (took %v)",
queryString, duration))
2. Consistent Log Levels¶
Use appropriate log levels consistently:
// Error - for actual errors
logger.Error("failed to process payment",
"error", err,
"user_id", userID)
// Debug - for detailed troubleshooting information
logger.Debug("attempting payment processing",
"payment_provider", provider,
"amount", amount)
// Info - for tracking normal operations
logger.Info("payment processed successfully",
"transaction_id", txID,
"amount", amount)
3. Context Preservation¶
Ensure context is preserved through the logging chain:
func processWithContext(ctx context.Context) error {
logger := getContextLogger(ctx)
// Add request-specific context
requestLogger := enrichLoggerWithContext(logger, ctx)
err := performOperation()
if err != nil {
return ewrap.Wrap(err, "operation failed",
ewrap.WithLogger(requestLogger),
ewrap.WithContext(ctx, ewrap.ErrorTypeInternal, ewrap.SeverityError))
}
return nil
}
func enrichLoggerWithContext(logger Logger, ctx context.Context) Logger {
// Extract common context values
requestID := ctx.Value("request_id")
userID := ctx.Value("user_id")
return &ContextLogger{
base: logger,
requestID: requestID.(string),
userID: userID.(string),
}
}