Audience: operators and developers defining HTTP metrics, labels, dashboards, and custom recorder behavior.
Keep HTTP metrics predictable and low-cardinality so dashboards stay usable and storage costs remain bounded.
The contrib metrics middleware emits:
http_requests_total(counter)http_request_duration_seconds(histogram)
The Prometheus recorder also exposes bounded route-policy and health-state metrics when the relevant hooks are present:
http_route_policy_requests_total(counter)health_status_changes_total(counter)idempotency_outcomes_total(counter)http_hard_timeout_events_total(counter)webhook_delivery_events_total(counter)
Required labels:
method(uppercase standard HTTP method,OTHERfor non-standard methods, orUNKNOWNwhen missing)route(router pattern, not raw path)status(HTTP status code as string, or0when missing or invalid)
Health transition labels:
from(one ofhealthy,degraded,unhealthy,unknown, orother)to(one ofhealthy,degraded,unhealthy,unknown, orother)
Route-policy labels:
method(same bounded method label as HTTP metrics)route(same router-pattern label as HTTP metrics)status_class(1xx,2xx,3xx,4xx,5xx, ornone)auth,tenant,idempotency, andadmin(requiredornone)rate_limit(configuredornone)deprecated(trueorfalse)
Idempotency outcome labels:
method(same bounded method label as HTTP metrics)store_class(memory,redis,sql,custom, orunknown)outcome(stable idempotency outcome enum orunknown)status_class(1xx,2xx,3xx,4xx,5xx, ornone)fail_open(trueorfalse)
Hard-timeout event labels:
method(same bounded method label as HTTP metrics)outcome(timeout,panic,capture_overflow, orunknown)status_class(1xx,2xx,3xx,4xx,5xx, ornone)timed_out,panicked, andcapture_overflow(trueorfalse)
Webhook delivery labels:
event_type(bounded catalog event type orother)outcome(succeeded,failed, or another bounded outcome value)status_class(2xx,3xx,4xx,5xx,transport, orunknown)
Do:
- Use route patterns like
/v1/users/{id}instead of raw paths. - Keep labels to small, bounded sets.
- Prefer stable enums (method, status, route) over user input.
- Use fallbacks like
unknownor0when data is missing.
Avoid:
- Raw paths, query strings, or request bodies.
- User IDs, tenant IDs, API keys, or IP addresses.
- Request IDs, trace IDs, or span IDs.
- User-Agent, Referer, or error messages.
The metrics middleware derives route from chi route patterns or
net/http.ServeMux request patterns and defaults missing routes to unknown.
The Prometheus recorder canonicalizes HTTP methods and status codes before
writing series. When routes are registered through
routecontracts, bounded routepolicy metadata is attached to the request and
recorded by http_route_policy_requests_total. Raw scopes, tenant sources,
rate-limit policy names, admin policy names, and identities are intentionally
not exported as metric labels. This keeps cardinality stable by design. Custom
recorders can call metrics.RoutePolicyLabels to reuse the same bounded label
normalization as the Prometheus recorder.
For periodic health refreshes, wire the scheduler callback to the same Prometheus recorder:
recorder, err := metrics.NewPrometheusRecorderChecked(nil, nil)
if err != nil {
return err
}
scheduler := health.NewScheduler(manager, health.SchedulerConfig{
OnStatusChange: metrics.HealthStatusChangeHook(recorder),
})
go scheduler.Start(ctx)Health status metrics intentionally omit checker names, dependency messages, request data, and tenant data. Keep detailed health information in protected admin endpoints, structured logs, or traces.
The contrib bootstrap helpers keep metrics opt-in:
bootstrap.ProfileStrictAPIandbootstrap.ProfileDevuse a No-op metrics recorder unless you passbootstrap.WithMetricsRecorder(...).bootstrap.MountSystemEndpointspreserves convenience behavior and only mountsspecs.Metricswhen you setSystemEndpoints.Metricsexplicitly.- Generated
saas-apiservices create one Prometheus recorder, pass it to the default router, and run the health scheduler as anAPIServicebackground task withmetrics.HealthStatusChangeHook. They also wire idempotency middleware outcome events tometrics.IdempotencyOutcomeHookandrequestlog.IdempotencyOutcomeLogHook, so write/replay/conflict decisions are observable without raw idempotency keys or tenant labels. - Generated
saas-api-fullservices create one Prometheus recorder, wrap the publicnet/httprouter with the contrib metrics middleware, and mount the standard Prometheus handler behind admin authentication. The generated tests assert HTTP counters/histograms use route patterns and do not expose API keys, admin keys, idempotency keys, actors, or tenants as labels. - Generated
saas-api-fullwebhook delivery workers wire bounded webhook delivery observations through the same recorder. Generated async worker logs use low-cardinality job kind and outcome fields; keep job IDs in logs only and do not promote tenant IDs, object keys, delivery IDs, endpoint URLs, payloads, or provider errors into metric labels. - Prefer
bootstrap.MountSystemEndpointsToWithAdminfor new system endpoint wiring so metrics are mounted behind an explicit admin or internal-network wrapper. bootstrap.PrometheusMetricsHandler()is the convenience helper for the standard Prometheus HTTP handler.
Example:
err := bootstrap.MountSystemEndpointsToWithAdmin(router, bootstrap.SystemEndpoints{
Metrics: bootstrap.PrometheusMetricsHandler(),
}, bootstrap.SystemEndpointAdminOptions{
RequireAdmin: requireAdmin,
})
if err != nil {
return err
}If you add custom metrics, keep label sets small and bounded. If you need higher-cardinality data, emit it to structured logs or tracing instead of metrics.
Normal idempotency request outcomes can be observed with
middleware/idempotency.Options.OnOutcome. OutcomeEvent.MetricLabels() exposes
only:
method: standard HTTP method orOTHERstore_class:memory,redis,sql,custom, orunknownoutcome: a stable idempotency outcome enum such ascompleted_stored,completed_released,replayed,conflict,in_flight,ambiguous,fail_open, orpersistence_failedstatus_class:1xx,2xx,3xx,4xx,5xx, ornone
Contrib services can use metrics.IdempotencyOutcomeHook(recorder) to emit the
same bounded labels to idempotency_outcomes_total; fail_open is added as a
boolean label. Use requestlog.IdempotencyOutcomeLogHook(log) for structured
logs with the same bounded shape.
Outcome events intentionally omit raw paths, query strings, request IDs, tenant IDs, idempotency keys, bodies, and error strings.
The idempotency mixed-version compatibility metric label contract is bounded:
method: standard HTTP method orOTHERstore_class:memory,redis,sql,custom, orunknownoutcome:legacy_in_flight_fallback_entered,legacy_in_flight_fallback_recovered,legacy_in_flight_fallback_rejected,legacy_in_flight_fallback_unknown, orunknown
Do not add raw paths, query strings, request IDs, tenant IDs, idempotency keys,
key hashes, or error strings to metric labels. LegacyInFlightCompatibilityEvent
keeps Path, Key, StoreType, and Error for structured logs or traces where
operators can sample, redact, and restrict access.
LegacyInFlightCompatibilityRawKey remains disabled by default. Enable raw-key
output only for short, access-controlled incident review, and prefer hashed keys
or redacted values for normal operations.
Adapter-level legacy idempotency recovery events follow the same privacy
posture: memory and Redis adapter events hash the Key field by default,
populate KeyHash for correlation, and leave RawKey empty unless the
adapter-specific raw-key opt-in is explicitly enabled for incident review.
Compatibility telemetry delivery is best-effort. Async mode uses a bounded queue and worker pool; when the queue is full, events are dropped and a warning records the cumulative drop count and queue size.
Hard-timeout response cutoffs can be observed with
middleware/timeout.Options.EventHooks. Contrib services can wire the same
bounded event into Prometheus and structured logs:
hardTimeout, err := timeout.NewHard(timeout.Options{
Timeout: 2 * time.Second,
EventHooks: &timeout.HardTimeoutEventHooks{
OnEvent: metrics.HardTimeoutEventHook(recorder),
},
})Use requestlog.HardTimeoutEventLogHook(log) when you also want structured log
entries for timeout, panic, and capture-overflow outcomes. Both helpers omit
panic values, URL paths, query strings, tenant IDs, request IDs, headers,
response bodies, and raw error strings.