Metrics Collection

Collect and expose metrics with Forge's Prometheus-compatible metrics system

Metrics Collection

Forge provides a comprehensive metrics collection system that's compatible with Prometheus and other monitoring systems. The system includes built-in metrics, custom metrics, and automatic instrumentation.

Metrics Interface

The metrics interface provides a clean API for collecting various types of metrics:

type Metrics interface {
    // Counter metrics
    Counter(name string, tags ...string) Counter
    
    // Gauge metrics
    Gauge(name string, tags ...string) Gauge
    
    // Histogram metrics
    Histogram(name string, tags ...string) Histogram
    
    // Timer metrics
    Timer(name string, tags ...string) Timer
    
    // Metric management
    RegisterCounter(name string, help string, tags ...string) error
    RegisterGauge(name string, help string, tags ...string) error
    RegisterHistogram(name string, help string, buckets []float64, tags ...string) error
    
    // Health and status
    Health() error
    Close() error
}

Metric Types

Counters

Counters track monotonically increasing values like request counts, errors, or events.

// Create a counter
requestCounter := app.Metrics().Counter("requests_total")

// Increment counter
requestCounter.Inc()

// Increment by specific value
requestCounter.Add(5)

// Increment with tags
requestCounter.WithTags("method", "GET", "status", "200").Inc()

Example:

func requestCounterMiddleware(next forge.HandlerFunc) forge.HandlerFunc {
    return func(ctx forge.Context) error {
        // Increment request counter
        app.Metrics().Counter("http_requests_total").
            WithTags("method", ctx.Method(), "path", ctx.Path()).
            Inc()
        
        err := next(ctx)
        
        // Increment error counter if request failed
        if err != nil {
            app.Metrics().Counter("http_errors_total").
                WithTags("method", ctx.Method(), "path", ctx.Path()).
                Inc()
        }
        
        return err
    }
}

Gauges

Gauges track values that can go up or down like memory usage, active connections, or queue size.

// Create a gauge
activeConnections := app.Metrics().Gauge("active_connections")

// Set gauge value
activeConnections.Set(42)

// Increment gauge
activeConnections.Inc()

// Decrement gauge
activeConnections.Dec()

// Add/subtract from gauge
activeConnections.Add(10)
activeConnections.Sub(5)

Example:

func connectionGaugeMiddleware(next forge.HandlerFunc) forge.HandlerFunc {
    return func(ctx forge.Context) error {
        // Increment active connections
        app.Metrics().Gauge("active_connections").Inc()
        
        defer func() {
            // Decrement active connections
            app.Metrics().Gauge("active_connections").Dec()
        }()
        
        return next(ctx)
    }
}

Histograms

Histograms track distributions of values like request durations, response sizes, or processing times.

// Create a histogram
requestDuration := app.Metrics().Histogram("request_duration_seconds")

// Observe a value
requestDuration.Observe(0.5)

// Observe with tags
requestDuration.WithTags("method", "GET", "path", "/users").Observe(0.3)

Example:

func requestDurationMiddleware(next forge.HandlerFunc) forge.HandlerFunc {
    return func(ctx forge.Context) error {
        start := time.Now()
        
        err := next(ctx)
        
        duration := time.Since(start).Seconds()
        
        // Record request duration
        app.Metrics().Histogram("http_request_duration_seconds").
            WithTags("method", ctx.Method(), "path", ctx.Path()).
            Observe(duration)
        
        return err
    }
}

Timers

Timers are a convenience wrapper around histograms for measuring durations.

// Create a timer
dbQueryTimer := app.Metrics().Timer("db_query_duration")

// Time a function
dbQueryTimer.Time(func() {
    // Database query
    rows, err := db.Query("SELECT * FROM users")
    // ... process results
})

// Time with tags
dbQueryTimer.WithTags("table", "users", "operation", "select").Time(func() {
    // Database query
})

Example:

func (s *UserService) GetUser(id string) (*User, error) {
    var user *User
    
    // Time the database query
    err := app.Metrics().Timer("db_query_duration").
        WithTags("table", "users", "operation", "select").
        Time(func() {
            user, err = s.db.QueryUser(id)
        })
    
    if err != nil {
        // Increment error counter
        app.Metrics().Counter("db_errors_total").
            WithTags("table", "users", "operation", "select").
            Inc()
        return nil, err
    }
    
    return user, nil
}

Built-in Metrics

Forge automatically collects several built-in metrics:

HTTP Metrics

// Request metrics
http_requests_total{method, path, status}
http_request_duration_seconds{method, path}
http_request_size_bytes{method, path}
http_response_size_bytes{method, path}

// Error metrics
http_errors_total{method, path, error_type}

Application Metrics

// Application metrics
app_start_time_seconds
app_uptime_seconds
app_memory_usage_bytes
app_goroutine_count

System Metrics

// System metrics (if enabled)
system_cpu_usage_percent
system_memory_usage_bytes
system_disk_usage_bytes
system_network_bytes_sent
system_network_bytes_received

Metrics Configuration

Basic 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,
        MaxMetrics:           10000,
        BufferSize:           1000,
        DefaultTags: map[string]string{
            "environment": "production",
            "service":     "my-app",
        },
    },
})

Advanced Configuration

metricsConfig := forge.MetricsConfig{
    // Basic settings
    Enabled:             true,
    MetricsPath:         "/_/metrics",
    Namespace:           "forge",
    
    // Collection settings
    CollectionInterval:  10 * time.Second,
    
    // Metric types
    EnableSystemMetrics:  false,
    EnableRuntimeMetrics: false,
    EnableHTTPMetrics:    true,
    
    // Limits
    MaxMetrics: 10000,
    BufferSize: 1000,
    
    // Default tags
    DefaultTags: map[string]string{
        "environment": "production",
        "service":     "my-app",
        "version":     "1.0.0",
    },
    
    // Custom settings
    CustomSettings: map[string]interface{}{
        "histogram_buckets": []float64{0.1, 0.5, 1.0, 2.5, 5.0, 10.0},
        "enable_histograms": true,
        "enable_counters":   true,
        "enable_gauges":     true,
    },
}

Custom Metrics

Business Metrics

// User metrics
type UserMetrics struct {
    userRegistrations forge.Counter
    userLogins        forge.Counter
    activeUsers       forge.Gauge
    userSessions      forge.Histogram
}

func NewUserMetrics(metrics forge.Metrics) *UserMetrics {
    return &UserMetrics{
        userRegistrations: metrics.Counter("user_registrations_total"),
        userLogins:        metrics.Counter("user_logins_total"),
        activeUsers:       metrics.Gauge("active_users"),
        userSessions:      metrics.Histogram("user_session_duration_seconds"),
    }
}

func (um *UserMetrics) RecordRegistration() {
    um.userRegistrations.Inc()
}

func (um *UserMetrics) RecordLogin() {
    um.userLogins.Inc()
}

func (um *UserMetrics) SetActiveUsers(count int) {
    um.activeUsers.Set(float64(count))
}

func (um *UserMetrics) RecordSessionDuration(duration time.Duration) {
    um.userSessions.Observe(duration.Seconds())
}

Service Metrics

// Database metrics
type DatabaseMetrics struct {
    queryDuration    forge.Histogram
    queryErrors      forge.Counter
    activeConnections forge.Gauge
    connectionPool   forge.Gauge
}

func NewDatabaseMetrics(metrics forge.Metrics) *DatabaseMetrics {
    return &DatabaseMetrics{
        queryDuration:     metrics.Histogram("db_query_duration_seconds"),
        queryErrors:       metrics.Counter("db_query_errors_total"),
        activeConnections: metrics.Gauge("db_active_connections"),
        connectionPool:    metrics.Gauge("db_connection_pool_size"),
    }
}

func (dm *DatabaseMetrics) RecordQuery(duration time.Duration, table, operation string) {
    dm.queryDuration.WithTags("table", table, "operation", operation).
        Observe(duration.Seconds())
}

func (dm *DatabaseMetrics) RecordQueryError(table, operation string) {
    dm.queryErrors.WithTags("table", table, "operation", operation).Inc()
}

func (dm *DatabaseMetrics) SetActiveConnections(count int) {
    dm.activeConnections.Set(float64(count))
}

func (dm *DatabaseMetrics) SetConnectionPoolSize(size int) {
    dm.connectionPool.Set(float64(size))
}

Metrics Middleware

Request Metrics Middleware

func RequestMetricsMiddleware(metrics forge.Metrics) forge.Middleware {
    return func(next forge.HandlerFunc) forge.HandlerFunc {
        return func(ctx forge.Context) error {
            start := time.Now()
            
            // Increment request counter
            metrics.Counter("http_requests_total").
                WithTags("method", ctx.Method(), "path", ctx.Path()).
                Inc()
            
            err := next(ctx)
            
            // Record request duration
            duration := time.Since(start)
            metrics.Histogram("http_request_duration_seconds").
                WithTags("method", ctx.Method(), "path", ctx.Path()).
                Observe(duration.Seconds())
            
            // Record error if request failed
            if err != nil {
                metrics.Counter("http_errors_total").
                    WithTags("method", ctx.Method(), "path", ctx.Path()).
                    Inc()
            }
            
            return err
        }
    }
}

Database Metrics Middleware

func DatabaseMetricsMiddleware(metrics forge.Metrics) 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()
                
                err := next(ctx)
                
                duration := time.Since(start)
                metrics.Histogram("db_operation_duration_seconds").
                    WithTags("operation", getDatabaseOperation(ctx.Path())).
                    Observe(duration.Seconds())
                
                if err != nil {
                    metrics.Counter("db_operation_errors_total").
                        WithTags("operation", getDatabaseOperation(ctx.Path())).
                        Inc()
                }
                
                return err
            }
            
            return next(ctx)
        }
    }
}

Metrics Endpoints

Built-in Endpoints

Forge automatically provides metrics endpoints:

# Prometheus metrics
GET /_/metrics

# Metrics health check
GET /_/metrics/health

# Metrics information
GET /_/metrics/info

Custom Metrics Endpoints

// Custom metrics endpoint
app.Router().GET("/metrics/custom", func(ctx forge.Context) error {
    // Get custom metrics
    metrics := map[string]interface{}{
        "user_registrations": app.Metrics().Counter("user_registrations_total").Value(),
        "active_users":       app.Metrics().Gauge("active_users").Value(),
        "avg_session_time":   app.Metrics().Histogram("user_session_duration_seconds").Mean(),
    }
    
    return ctx.JSON(200, metrics)
})

Metrics Integration

Prometheus Integration

// Prometheus metrics are automatically available at /_/metrics
// Configure Prometheus to scrape this endpoint

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

Grafana Dashboards

{
  "dashboard": {
    "title": "Forge Application Metrics",
    "panels": [
      {
        "title": "Request Rate",
        "type": "graph",
        "targets": [
          {
            "expr": "rate(http_requests_total[5m])",
            "legendFormat": "{{method}} {{path}}"
          }
        ]
      },
      {
        "title": "Request Duration",
        "type": "graph",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
            "legendFormat": "95th percentile"
          }
        ]
      }
    ]
  }
}

Metrics Testing

Unit Testing

func TestMetrics(t *testing.T) {
    app := forge.NewTestApp(forge.TestAppConfig{
        Name: "test-app",
    })
    
    // Test counter
    counter := app.Metrics().Counter("test_counter")
    counter.Inc()
    counter.Add(5)
    
    assert.Equal(t, float64(6), counter.Value())
    
    // Test gauge
    gauge := app.Metrics().Gauge("test_gauge")
    gauge.Set(42)
    gauge.Inc()
    
    assert.Equal(t, float64(43), gauge.Value())
    
    // Test histogram
    histogram := app.Metrics().Histogram("test_histogram")
    histogram.Observe(0.5)
    histogram.Observe(1.0)
    
    assert.Equal(t, float64(0.75), histogram.Mean())
}

Integration Testing

func TestMetricsEndpoint(t *testing.T) {
    app := forge.NewTestApp(forge.TestAppConfig{
        Name: "test-app",
    })
    
    // Start application
    go func() {
        app.Run()
    }()
    defer app.Stop(context.Background())
    
    // Test metrics endpoint
    resp, err := http.Get("http://localhost:8080/_/metrics")
    require.NoError(t, err)
    defer resp.Body.Close()
    
    assert.Equal(t, 200, resp.StatusCode)
    
    // Check metrics format
    body, err := io.ReadAll(resp.Body)
    require.NoError(t, err)
    
    assert.Contains(t, string(body), "http_requests_total")
    assert.Contains(t, string(body), "http_request_duration_seconds")
}

Best Practices

  1. Meaningful Names: Use descriptive metric names
  2. Consistent Tags: Use consistent tag names and values
  3. Appropriate Granularity: Don't over-instrument
  4. Performance: Keep metrics collection lightweight
  5. Cardinality: Avoid high-cardinality tags
  6. Documentation: Document custom metrics
  7. Testing: Test metrics collection
  8. Monitoring: Set up alerts for critical metrics

For more information about observability and monitoring, see the Observability documentation.

How is this guide?

Last updated on