Skip to content

Use case — Prometheus metrics

You want OIDC-business metrics (tokens issued, refresh rotations, audit event counts) on your existing Prometheus stack — but you don't want the library mounting /metrics for you, because that's your router's job.

Source: examples/52-prometheus-metrics

The contract

go
import (
  "github.com/prometheus/client_golang/prometheus"
  "github.com/prometheus/client_golang/prometheus/promhttp"
  "github.com/libraz/go-oidc-provider/op"
)

reg := prometheus.NewRegistry()

provider, err := op.New(
  /* required options */
  op.WithPrometheus(reg), // <-- the library registers its collectors here
)

// You mount /metrics on your router, on the same registry:
mux := http.NewServeMux()
mux.Handle("/", provider)
mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))

Lib is business-only, not HTTP-lifecycle

The library emits OIDC business counters — token issuance, refresh rotation, audit event counts, authentication outcomes. It does not emit:

  • HTTP request duration histograms
  • HTTP request / response sizes
  • Status-code distribution
  • Panic recovery counters

These are the embedder's responsibility because they're not OIDC domain — they're HTTP-server domain. Wrap your router with promhttp.InstrumentHandler* middleware (or otelhttp.NewMiddleware for tracing) according to your SRE conventions.

What the library exports

The exported counter set is curated and stable. Every metric carries a constant issuer label sourced from op.WithIssuer, so multiple issuers may share one registry while retaining separate series. Two providers with the same issuer on one registry still collide because they register the same metric descriptors. Registration is all-or-nothing: if any collector is refused, the collectors already accepted by that call are unregistered before op.New returns the error.

The actual names are prefixed oidc_* — see the example for the live list. Categories:

CategoryCounters / labels
Token endpointoidc_token_issued_total{issuer,grant_type,client_id}, oidc_tokens_refreshed_total{issuer,client_id}, refresh / authorization-code replay detections, client-auth failures by method and reason
Authenticationoidc_login_attempts_total{issuer,factor,result}; the factor label covers primary login and MFA attempts
Extension flowsDCR, Device Authorization, Device Code, CIBA, and Token Exchange event counters, labelled by the audit event sub-name
Logout / revocationback-channel delivery outcomes, session-bearing logout notices with zero eligible RP targets derived from grants (bcl.no_sessions_for_subject), token / refresh-chain / grant revocation side-effect failures
Operational signalsintrospection authentication errors, DPoP loose-method-case bridge admissions, retired JWKS kid presentation

The metrics bridge is fed from the audit emitter. A single audit event updates the slog stream and the matching counter, so embedders do not need to emit metrics separately.

Dynamic clients are deliberately not exposed as raw client_id label values. Only statically seeded client IDs are labelled; DCR-created or unknown clients collapse into the empty client_id bucket to keep cardinality bounded.

For oidc_token_issued_total, the grant_type value is derived from the persisted refresh-chain origin, not copied from an arbitrary request label: authorization_code, device_code, ciba, or custom_grant, with unknown when the origin is absent or outside that closed set. Refresh rotation has its own oidc_tokens_refreshed_total counter.

Why on-the-side, not bundled

Two reasons:

  1. Registry ownership — embedders frequently maintain a single prometheus.Registry for their whole process so cardinality and collector-list audits stay in one place. A second registry created by the library would split that.
  2. Path / auth ownership/metrics is often gated by auth, or exposed only on a separate listener. The library cannot predict that choice, so mounting the path stays with the embedder.

The same separation holds for tracing: op.WithLogger / op.WithAuditLogger take a *slog.Logger, and OpenTelemetry spans for HTTP server semantics belong to the embedder's otelhttp.NewMiddleware. The OP emits no built-in tracing spans, business or request/response lifecycle; instrument additional business stages in the embedder if needed.