Observability

Comprehensive observability with logging, metrics, tracing, and health checks

Observability

Forge provides comprehensive observability features including structured logging, metrics collection, distributed tracing, and health checks. These features work together to give you complete visibility into your application's behavior and performance.

Observability Stack

Forge's observability stack includes:

  • Logging: Structured logging with multiple outputs and levels
  • Metrics: Prometheus-compatible metrics collection
  • Tracing: Distributed tracing with OpenTelemetry
  • Health Checks: Comprehensive health monitoring
  • Profiling: Built-in profiling capabilities

Logging

Structured Logging

Forge uses structured logging with contextual information:

// Basic logging
app.Logger().Info("user created", forge.F("user_id", userID))

// Error logging with context
app.Logger().Error("database error", 
    forge.F("error", err),
    forge.F("query", query),
    forge.F("duration", time.Since(start)),
)

// Debug logging
app.Logger().Debug("processing request",
    forge.F("request_id", requestID),
    forge.F("user_id", userID),
    forge.F("path", path),
)

Log Levels

// Different log levels
app.Logger().Debug("debug message")
app.Logger().Info("info message")
app.Logger().Warn("warning message")
app.Logger().Error("error message")
app.Logger().Fatal("fatal message")

Log Configuration

app := forge.NewApp(forge.AppConfig{
    Logger: forge.NewLogger(forge.LoggerConfig{
        Level:  "info",
        Format: "json",
        Output: "stdout",
        Fields: map[string]interface{}{
            "service": "my-app",
            "version": "1.0.0",
        },
    }),
})

Log Outputs

// Multiple outputs
logger := forge.NewLogger(forge.LoggerConfig{
    Outputs: []forge.LogOutput{
        forge.NewStdoutOutput(),
        forge.NewFileOutput("/var/log/app.log"),
        forge.NewSyslogOutput(),
    },
})

Metrics

Built-in Metrics

Forge automatically collects several types of metrics:

// HTTP metrics
http_requests_total{method, path, status}
http_request_duration_seconds{method, path}
http_errors_total{method, path, error_type}

// Application metrics
app_start_time_seconds
app_uptime_seconds
app_memory_usage_bytes

// System metrics (if enabled)
system_cpu_usage_percent
system_memory_usage_bytes
system_disk_usage_bytes

Custom Metrics

// Business metrics
userRegistrations := app.Metrics().Counter("user_registrations_total")
activeUsers := app.Metrics().Gauge("active_users")
sessionDuration := app.Metrics().Histogram("session_duration_seconds")

// Record metrics
userRegistrations.Inc()
activeUsers.Set(float64(userCount))
sessionDuration.Observe(duration.Seconds())

Metrics Configuration

app := forge.NewApp(forge.AppConfig{
    MetricsConfig: forge.MetricsConfig{
        Enabled:              true,
        MetricsPath:          "/_/metrics",
        Namespace:            "forge",
        CollectionInterval:   10 * time.Second,
        EnableSystemMetrics:  false,
        EnableRuntimeMetrics: false,
        EnableHTTPMetrics:    true,
        DefaultTags: map[string]string{
            "environment": "production",
            "service":     "my-app",
        },
    },
})

Distributed Tracing

OpenTelemetry Integration

Forge integrates with OpenTelemetry for distributed tracing:

// Configure tracing
app := forge.NewApp(forge.AppConfig{
    TracingConfig: forge.TracingConfig{
        Enabled:     true,
        ServiceName: "my-app",
        ServiceVersion: "1.0.0",
        Exporter:    "jaeger",
        Endpoint:    "http://localhost:14268/api/traces",
        SampleRate:  1.0,
    },
})

Trace Context

// Create spans
func userHandler(ctx forge.Context) error {
    span := trace.SpanFromContext(ctx.Request().Context())
    span.SetAttributes(
        attribute.String("user.id", userID),
        attribute.String("user.email", email),
    )
    
    // Add events
    span.AddEvent("processing user request")
    
    // Add child span
    childCtx, childSpan := tracer.Start(ctx.Request().Context(), "database.query")
    defer childSpan.End()
    
    // Database operation
    user, err := userService.GetUser(childCtx, userID)
    if err != nil {
        childSpan.RecordError(err)
        return err
    }
    
    childSpan.SetAttributes(attribute.String("user.name", user.Name))
    
    return ctx.JSON(200, user)
}

Trace Propagation

// Propagate trace context
func httpClientMiddleware(next forge.HandlerFunc) forge.HandlerFunc {
    return func(ctx forge.Context) error {
        // Extract trace context
        traceCtx := trace.SpanFromContext(ctx.Request().Context())
        
        // Propagate to downstream services
        headers := make(map[string]string)
        propagator := otel.GetTextMapPropagator()
        propagator.Inject(traceCtx.SpanContext(), otel.MapCarrier(headers))
        
        // Add headers to request
        for key, value := range headers {
            ctx.SetHeader(key, value)
        }
        
        return next(ctx)
    }
}

Health Checks

Health Monitoring

Forge provides comprehensive health monitoring:

// Register health checks
app.HealthManager().Register("database", func(ctx context.Context) HealthResult {
    if err := db.PingContext(ctx); err != nil {
        return HealthResult{
            Status:  HealthStatusUnhealthy,
            Message: "database connection failed",
            Error:   err,
        }
    }
    return HealthResult{
        Status:  HealthStatusHealthy,
        Message: "database is healthy",
    }
})

// Check overall health
status := app.HealthManager().HealthStatus()
report := app.HealthManager().Check(ctx)

Health Endpoints

# Overall health
GET /_/health

# Detailed health report
GET /_/health/detailed

# Specific service health
GET /_/health/service/{name}

# Health check history
GET /_/health/history

Profiling

Built-in Profiling

Forge includes built-in profiling capabilities:

// Enable profiling
app := forge.NewApp(forge.AppConfig{
    ProfilingConfig: forge.ProfilingConfig{
        Enabled:     true,
        ProfilePath: "/_/profile",
        CPUProfile:  true,
        MemProfile:  true,
        BlockProfile: true,
        MutexProfile: true,
    },
})

Profile Endpoints

# CPU profile
GET /_/profile/cpu

# Memory profile
GET /_/profile/mem

# Block profile
GET /_/profile/block

# Mutex profile
GET /_/profile/mutex

Observability Middleware

Request Observability

func ObservabilityMiddleware(app App) forge.Middleware {
    return func(next forge.HandlerFunc) forge.HandlerFunc {
        return func(ctx forge.Context) error {
            start := time.Now()
            requestID := generateRequestID()
            
            // Add request ID to context
            ctx.Set("request_id", requestID)
            
            // Create span
            spanCtx, span := tracer.Start(ctx.Request().Context(), "http.request")
            defer span.End()
            
            // Set span attributes
            span.SetAttributes(
                attribute.String("http.method", ctx.Method()),
                attribute.String("http.path", ctx.Path()),
                attribute.String("request.id", requestID),
            )
            
            // Log request start
            app.Logger().Info("request started",
                forge.F("request_id", requestID),
                forge.F("method", ctx.Method()),
                forge.F("path", ctx.Path()),
            )
            
            // Increment request counter
            app.Metrics().Counter("http_requests_total").
                WithTags("method", ctx.Method(), "path", ctx.Path()).
                Inc()
            
            // Process request
            err := next(ctx)
            
            // Record metrics
            duration := time.Since(start)
            app.Metrics().Histogram("http_request_duration_seconds").
                WithTags("method", ctx.Method(), "path", ctx.Path()).
                Observe(duration.Seconds())
            
            // Log request completion
            app.Logger().Info("request completed",
                forge.F("request_id", requestID),
                forge.F("method", ctx.Method()),
                forge.F("path", ctx.Path()),
                forge.F("duration", duration),
                forge.F("error", err),
            )
            
            // Record error if request failed
            if err != nil {
                span.RecordError(err)
                app.Metrics().Counter("http_errors_total").
                    WithTags("method", ctx.Method(), "path", ctx.Path()).
                    Inc()
            }
            
            return err
        }
    }
}

Database Observability

func DatabaseObservabilityMiddleware(app App) forge.Middleware {
    return func(next forge.HandlerFunc) forge.HandlerFunc {
        return func(ctx forge.Context) error {
            // Check if this is a database operation
            if isDatabaseOperation(ctx.Path()) {
                start := time.Now()
                
                // Create database span
                spanCtx, span := tracer.Start(ctx.Request().Context(), "database.operation")
                defer span.End()
                
                // Set span attributes
                span.SetAttributes(
                    attribute.String("db.operation", getDatabaseOperation(ctx.Path())),
                    attribute.String("db.table", getDatabaseTable(ctx.Path())),
                )
                
                // Record database metrics
                dbCounter := app.Metrics().Counter("db_operations_total").
                    WithTags("operation", getDatabaseOperation(ctx.Path()))
                dbCounter.Inc()
                
                err := next(ctx)
                
                // Record duration
                duration := time.Since(start)
                app.Metrics().Histogram("db_operation_duration_seconds").
                    WithTags("operation", getDatabaseOperation(ctx.Path())).
                    Observe(duration.Seconds())
                
                if err != nil {
                    span.RecordError(err)
                    app.Metrics().Counter("db_errors_total").
                        WithTags("operation", getDatabaseOperation(ctx.Path())).
                        Inc()
                }
                
                return err
            }
            
            return next(ctx)
        }
    }
}

Observability Configuration

Complete Configuration

app := forge.NewApp(forge.AppConfig{
    // Logging configuration
    Logger: forge.NewLogger(forge.LoggerConfig{
        Level:  "info",
        Format: "json",
        Output: "stdout",
        Fields: map[string]interface{}{
            "service": "my-app",
            "version": "1.0.0",
        },
    }),
    
    // Metrics configuration
    MetricsConfig: forge.MetricsConfig{
        Enabled:              true,
        MetricsPath:          "/_/metrics",
        Namespace:            "forge",
        CollectionInterval:   10 * time.Second,
        EnableSystemMetrics:  false,
        EnableRuntimeMetrics: false,
        EnableHTTPMetrics:    true,
        DefaultTags: map[string]string{
            "environment": "production",
            "service":     "my-app",
        },
    },
    
    // Health configuration
    HealthConfig: forge.HealthConfig{
        Enabled:                true,
        CheckInterval:          30 * time.Second,
        ReportInterval:         60 * time.Second,
        EnableAutoDiscovery:    true,
        MaxConcurrentChecks:    10,
        DefaultTimeout:         5 * time.Second,
        EnableSmartAggregation: true,
        HistorySize:            100,
    },
    
    // Tracing configuration
    TracingConfig: forge.TracingConfig{
        Enabled:       true,
        ServiceName:   "my-app",
        ServiceVersion: "1.0.0",
        Exporter:     "jaeger",
        Endpoint:     "http://localhost:14268/api/traces",
        SampleRate:   1.0,
    },
    
    // Profiling configuration
    ProfilingConfig: forge.ProfilingConfig{
        Enabled:     true,
        ProfilePath: "/_/profile",
        CPUProfile:  true,
        MemProfile:  true,
        BlockProfile: true,
        MutexProfile: true,
    },
})

Observability Tools Integration

Prometheus + Grafana

# prometheus.yml
scrape_configs:
  - job_name: 'forge-app'
    static_configs:
      - targets: ['localhost:8080']
    metrics_path: '/_/metrics'
    scrape_interval: 15s

Jaeger Tracing

# docker-compose.yml
version: '3'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"
      - "14268:14268"
    environment:
      - COLLECTOR_OTLP_ENABLED=true

ELK Stack

# docker-compose.yml
version: '3'
services:
  elasticsearch:
    image: elasticsearch:7.14.0
    ports:
      - "9200:9200"
  
  logstash:
    image: logstash:7.14.0
    ports:
      - "5044:5044"
  
  kibana:
    image: kibana:7.14.0
    ports:
      - "5601:5601"

Best Practices

  1. Structured Logging: Use structured logging with consistent fields
  2. Meaningful Metrics: Collect metrics that provide business value
  3. Trace Context: Propagate trace context across service boundaries
  4. Health Checks: Monitor all critical dependencies
  5. Performance: Keep observability overhead minimal
  6. Security: Don't log sensitive information
  7. Retention: Set appropriate retention policies
  8. Alerting: Set up alerts for critical metrics

For more detailed information about specific observability components, see the Logging, Metrics, and Health Checks documentation.

How is this guide?

Last updated on