zoobzio December 15, 2025 Edit this page

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