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_countSystem Metrics
// System metrics (if enabled)
system_cpu_usage_percent
system_memory_usage_bytes
system_disk_usage_bytes
system_network_bytes_sent
system_network_bytes_receivedMetrics 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/infoCustom 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: 15sGrafana 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
- Meaningful Names: Use descriptive metric names
- Consistent Tags: Use consistent tag names and values
- Appropriate Granularity: Don't over-instrument
- Performance: Keep metrics collection lightweight
- Cardinality: Avoid high-cardinality tags
- Documentation: Document custom metrics
- Testing: Test metrics collection
- 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