Structured logging and observability

tanGO uses the standard library's log/slog for structured logs and a small backend-neutral observability.Recorder interface for metrics. It does not require Prometheus, OpenTelemetry, or a particular logging backend.

Server configuration

Pass the framework logger and metric recorder to ServeContext:

err := tango.ServeContext(ctx, config, sqlDB, dialect,
    tango.WithLogger(logger),
    tango.WithRecorder(recorder),
)

An omitted logger uses slog.Default(). WithLogger(nil) is a configuration error. Metrics are disabled unless a non-no-op recorder is configured. Recorder implementations must be safe for concurrent use.

WithRecorder covers matched HTTP routes and scheduler jobs. Realtime is an independent package and receives its recorder through realtime.Options{Recorder: recorder}. Both host/bot Dispatch and transport-facing DispatchPeer calls contribute to the bounded dispatch outcome metric.

Request middleware

The recommended order is:

Middleware: []tango.Middleware{
    tango.RequestID(),
    tango.Recoverer(),
    tango.AccessLogger(),
}

RequestID generates a cryptographically random ID, exposes it through RequestIDFromContext, and writes X-Request-ID. It never trusts an inbound header by default. If a trusted proxy supplies IDs, opt in explicitly:

tango.RequestID(tango.WithTrustedHeader("X-Gateway-Request-ID"))

The header name is validated when middleware is constructed. Invalid inbound values are ignored and replaced with a generated ID.

Recoverer converts a panic into tanGO's generic JSON 500 response and emits tango.EventPanic. AccessLogger emits one tango.EventAccessLog after the downstream matched route completes. Each middleware can receive its own logger through WithRequestIDLogger, WithRecoveryLogger, or WithAccessLogger; otherwise it uses slog.Default(). AccessLogger does no work for a request when its logger discards Info events (for example a handler set to Warn), so you can keep it in the middleware list and raise the level to turn the access log off.

Inside a View, ctx.Logger() returns the configured logger enriched with the route pattern, HTTP method, and request ID when present.

The route value is the pattern the route was registered with, trailing slash included (/items/{id}/). It's the same in ctx.Logger(), View-error, access, panic and request-ID logs and in HTTP metrics, for every request.

Stable events and metrics

The stable log event names are EventViewError, EventPanic, EventAccessLog, EventJobFailed, and EventRequestIDGenerationFailed, plus accounts' accounts.EventMailFailed (tango.accounts.mail_failed, with purpose and a redacted error) and accounts.EventMailDropped (tango.accounts.mail_dropped, with purpose); purpose is password_reset or email_verification. The stable metric names are MetricHTTPRequestDuration, MetricSchedulerJobInvocations, and MetricRealtimeRoomEvents in the observability package.

Framework metric labels are intentionally bounded. HTTP metrics use route patterns rather than raw URLs. Realtime metrics never include room IDs, user IDs, or error text. Durations are recorded in seconds.

Host-supplied log handlers and recorders are best-effort dependencies: a panic from one is isolated and cannot replace the application error or operation being reported.

Known boundary

Under the default middleware scope, HTTP instrumentation wraps compiled, matched routes: router-generated 404 and 405 responses do not pass through AccessLogger and do not produce automatic HTTP metrics. With Config.MiddlewareScope: tango.MiddlewareScopeAll, which tango newproject sets, they do, reported with the route "(unmatched)". That value is part of the stable observability names, like the event and metric names above.

For a quick, in-memory view of recent activity inside the admin, see the admin dashboard guide.

View this page's source on GitHub