Observability Cookbook
Implement logging, metrics, and tracing using rocco's event system.
Event System Overview
Rocco emits lifecycle events via capitan. You hook into these events to build observability.
import (
"context"
"github.com/zoobz-io/capitan"
"github.com/zoobz-io/rocco"
)
// Hook specific events
capitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {
// Handle request completed event
})
// Observe all events
capitan.Observe(func(ctx context.Context, e *capitan.Event) {
// Handle any event
})
Structured Logging
Request Logging
import (
"log/slog"
"os"
)
var logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
func setupRequestLogging() {
// Log successful requests
capitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {
method, _ := rocco.MethodKey.From(e)
path, _ := rocco.PathKey.From(e)
status, _ := rocco.StatusCodeKey.From(e)
duration, _ := rocco.DurationMsKey.From(e)
handler, _ := rocco.HandlerNameKey.From(e)
logger.Info("request",
"method", method,
"path", path,
"status", status,
"duration_ms", duration,
"handler", handler,
)
})
// Log failed requests with error details
capitan.Hook(rocco.RequestFailed, func(ctx context.Context, e *capitan.Event) {
method, _ := rocco.MethodKey.From(e)
path, _ := rocco.PathKey.From(e)
status, _ := rocco.StatusCodeKey.From(e)
duration, _ := rocco.DurationMsKey.From(e)
handler, _ := rocco.HandlerNameKey.From(e)
errMsg, _ := rocco.ErrorKey.From(e)
logger.Error("request_failed",
"method", method,
"path", path,
"status", status,
"duration_ms", duration,
"handler", handler,
"error", errMsg,
)
})
}
Security Logging
func setupSecurityLogging() {
securityLogger := slog.New(slog.NewJSONHandler(
os.Stdout,
&slog.HandlerOptions{Level: slog.LevelWarn},
))
// Authentication failures
capitan.Hook(rocco.AuthenticationFailed, func(ctx context.Context, e *capitan.Event) {
method, _ := rocco.MethodKey.From(e)
path, _ := rocco.PathKey.From(e)
errMsg, _ := rocco.ErrorKey.From(e)
securityLogger.Warn("auth_failed",
"method", method,
"path", path,
"error", errMsg,
)
})
// Authorization denials
capitan.Hook(rocco.AuthorizationScopeDenied, func(ctx context.Context, e *capitan.Event) {
method, _ := rocco.MethodKey.From(e)
path, _ := rocco.PathKey.From(e)
identityID, _ := rocco.IdentityIDKey.From(e)
required, _ := rocco.RequiredScopesKey.From(e)
securityLogger.Warn("authz_denied",
"method", method,
"path", path,
"identity", identityID,
"required_scopes", required,
)
})
// Rate limit exceeded
capitan.Hook(rocco.RateLimitExceeded, func(ctx context.Context, e *capitan.Event) {
method, _ := rocco.MethodKey.From(e)
path, _ := rocco.PathKey.From(e)
identityID, _ := rocco.IdentityIDKey.From(e)
limitKey, _ := rocco.LimitKeyKey.From(e)
current, _ := rocco.CurrentValueKey.From(e)
threshold, _ := rocco.ThresholdKey.From(e)
securityLogger.Warn("rate_limit_exceeded",
"method", method,
"path", path,
"identity", identityID,
"limit_key", limitKey,
"current", current,
"threshold", threshold,
)
})
}
Error Logging
func setupErrorLogging() {
// Handler errors (unexpected)
capitan.Hook(rocco.HandlerError, func(ctx context.Context, e *capitan.Event) {
handler, _ := rocco.HandlerNameKey.From(e)
errMsg, _ := rocco.ErrorKey.From(e)
logger.Error("handler_error",
"handler", handler,
"error", errMsg,
)
})
// Undeclared sentinel errors (programming error)
capitan.Hook(rocco.HandlerUndeclaredSentinel, func(ctx context.Context, e *capitan.Event) {
handler, _ := rocco.HandlerNameKey.From(e)
errMsg, _ := rocco.ErrorKey.From(e)
status, _ := rocco.StatusCodeKey.From(e)
logger.Error("undeclared_sentinel",
"handler", handler,
"error", errMsg,
"would_be_status", status,
)
})
// Validation failures
capitan.Hook(rocco.RequestValidationInputFailed, func(ctx context.Context, e *capitan.Event) {
handler, _ := rocco.HandlerNameKey.From(e)
errMsg, _ := rocco.ErrorKey.From(e)
logger.Debug("validation_failed",
"handler", handler,
"error", errMsg,
)
})
}
Metrics
Prometheus Metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
requestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status", "handler"},
)
requestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
},
[]string{"method", "path", "handler"},
)
errorsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_errors_total",
Help: "Total number of HTTP errors",
},
[]string{"method", "path", "handler", "error_code"},
)
)
func setupPrometheusMetrics() {
// Request metrics
capitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {
method, _ := rocco.MethodKey.From(e)
path, _ := rocco.PathKey.From(e)
status, _ := rocco.StatusCodeKey.From(e)
duration, _ := rocco.DurationMsKey.From(e)
handler, _ := rocco.HandlerNameKey.From(e)
requestsTotal.WithLabelValues(method, path, fmt.Sprint(status), handler).Inc()
requestDuration.WithLabelValues(method, path, handler).Observe(float64(duration) / 1000)
})
capitan.Hook(rocco.RequestFailed, func(ctx context.Context, e *capitan.Event) {
method, _ := rocco.MethodKey.From(e)
path, _ := rocco.PathKey.From(e)
status, _ := rocco.StatusCodeKey.From(e)
duration, _ := rocco.DurationMsKey.From(e)
handler, _ := rocco.HandlerNameKey.From(e)
requestsTotal.WithLabelValues(method, path, fmt.Sprint(status), handler).Inc()
requestDuration.WithLabelValues(method, path, handler).Observe(float64(duration) / 1000)
errorsTotal.WithLabelValues(method, path, handler, fmt.Sprint(status)).Inc()
})
}
Custom Metrics
var (
authFailures = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "auth_failures_total",
Help: "Total authentication failures",
},
[]string{"path"},
)
rateLimitHits = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "rate_limit_hits_total",
Help: "Total rate limit hits",
},
[]string{"identity", "limit_key"},
)
)
func setupCustomMetrics() {
capitan.Hook(rocco.AuthenticationFailed, func(ctx context.Context, e *capitan.Event) {
path, _ := rocco.PathKey.From(e)
authFailures.WithLabelValues(path).Inc()
})
capitan.Hook(rocco.RateLimitExceeded, func(ctx context.Context, e *capitan.Event) {
identityID, _ := rocco.IdentityIDKey.From(e)
limitKey, _ := rocco.LimitKeyKey.From(e)
rateLimitHits.WithLabelValues(identityID, limitKey).Inc()
})
}
OpenTelemetry Tracing
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var tracer = otel.Tracer("rocco")
func setupTracing() {
capitan.Hook(rocco.RequestReceived, func(ctx context.Context, e *capitan.Event) {
method, _ := rocco.MethodKey.From(e)
path, _ := rocco.PathKey.From(e)
handler, _ := rocco.HandlerNameKey.From(e)
_, span := tracer.Start(ctx, handler,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithAttributes(
attribute.String("http.method", method),
attribute.String("http.route", path),
),
)
defer span.End()
})
capitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {
span := trace.SpanFromContext(ctx)
status, _ := rocco.StatusCodeKey.From(e)
span.SetAttributes(attribute.Int("http.status_code", status))
})
capitan.Hook(rocco.RequestFailed, func(ctx context.Context, e *capitan.Event) {
span := trace.SpanFromContext(ctx)
status, _ := rocco.StatusCodeKey.From(e)
errMsg, _ := rocco.ErrorKey.From(e)
span.SetAttributes(
attribute.Int("http.status_code", status),
attribute.String("error", errMsg),
)
span.RecordError(errors.New(errMsg))
})
}
Global Observer
Handle all events with a single observer:
func setupGlobalObserver() {
capitan.Observe(func(ctx context.Context, e *capitan.Event) {
switch e.Signal {
// Request lifecycle
case rocco.RequestReceived:
handleRequestReceived(ctx, e)
case rocco.RequestCompleted:
handleRequestCompleted(ctx, e)
case rocco.RequestFailed:
handleRequestFailed(ctx, e)
// Errors
case rocco.HandlerError:
handleHandlerError(ctx, e)
case rocco.HandlerUndeclaredSentinel:
handleUndeclaredSentinel(ctx, e)
// Security
case rocco.AuthenticationFailed:
handleAuthFailed(ctx, e)
case rocco.AuthorizationScopeDenied, rocco.AuthorizationRoleDenied:
handleAuthzDenied(ctx, e)
case rocco.RateLimitExceeded:
handleRateLimit(ctx, e)
// Server lifecycle
case rocco.EngineStarting:
handleEngineStarting(ctx, e)
case rocco.EngineShutdownComplete:
handleEngineShutdown(ctx, e)
}
})
}
Complete Setup
package main
import (
"github.com/zoobz-io/capitan"
"github.com/zoobz-io/rocco"
)
func main() {
// Setup observability before creating engine
setupRequestLogging()
setupSecurityLogging()
setupErrorLogging()
setupPrometheusMetrics()
// Create engine
engine := rocco.NewEngine().WithAuthenticator(extractIdentity)
// Register handlers
engine.WithHandlers(handlers...)
// Start server
engine.Start(rocco.HostAll, 8080)
}
Testing Observability
import "github.com/zoobz-io/capitan"
func TestMetricsEmission(t *testing.T) {
// Configure sync mode for testing
capitan.Configure(capitan.WithSyncMode())
var requestReceived bool
listener := capitan.Hook(rocco.RequestReceived, func(_ context.Context, e *capitan.Event) {
requestReceived = true
})
defer listener.Close()
// Make request
engine := rtesting.TestEngine()
engine.WithHandlers(handler)
rtesting.ServeRequest(engine, "GET", "/test", nil)
if !requestReceived {
t.Error("RequestReceived event not emitted")
}
}
See Also
- Events Reference - Complete event list
- Best Practices - Production patterns
- capitan Documentation - Event system docs