stacklok / stacklok/toolhive

Add OpenTelemetry instrumentation to authserver

Open
#3,922 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
2.2k
Forks
300
Avg merge
1d 15h
Merged PRs (30d)
184

Description

Context

The authserver has zero OpenTelemetry instrumentation — no traces, no metrics. The MCP proxy (pkg/telemetry/middleware.go) and vMCP server (pkg/vmcp/server/telemetry.go) both have comprehensive OTel coverage. The authserver needs parity.

Since the authserver is embedded in the proxy runner, it will receive TracerProvider and MeterProvider from the runner (plumbed through NewEmbeddedAuthServer / NewHandler).

Scope

Standard HTTP metrics layer

Add the stable OTel HTTP server metric using otelhttp middleware or adapted telemetry middleware:

  • http.server.request.duration (Histogram, unit s) with stable semconv attributes:
    • http.request.method, url.scheme, http.response.status_code, http.route, error.type
    • Routes: /oauth/authorize, /oauth/callback, /oauth/token, /oauth/register

This gives RED metrics on every endpoint for free, usable with any standard dashboard.

Custom auth-specific metrics

Follow the decorator pattern from pkg/vmcp/server/telemetry.go. Use dot-separated names (the Prometheus bridge handles conversion).

Counters:

  • toolhive.authserver.token.issued — labels: grant_type (authorization_code, refresh_token), error.type (omit on success; set to invalid_grant, invalid_client, etc. on failure). This replaces both the "issued" and "failures" counters — failures are token issuance attempts with error.type set, following the OTel error pattern.
  • toolhive.authserver.client.registrations — labels: error.type
  • toolhive.authserver.authorize.requests — labels: upstream (IDP type), error.type
  • toolhive.authserver.upstream.callbacks — labels: upstream, outcome (success, error, upstream_error)

Histograms:

  • toolhive.authserver.upstream.exchange.duration (unit s) — upstream IDP code exchange latency
  • toolhive.authserver.upstream.refresh.duration (unit s) — upstream IDP token refresh latency

Note: no token_request_duration_seconds — this is already captured by http.server.request.duration with http.route=/oauth/token.

UpDownCounters (not Gauges — sessions are countable increments/decrements):

  • toolhive.authserver.active_sessions — increment on session creation, decrement on revocation
  • toolhive.authserver.registered_clients — increment on DCR, decrement on deletion

Do NOT manually append _total to counter names — the OTLP→Prometheus bridge does this automatically.

Tracing

Top-level spans follow HTTP server semconv naming{METHOD} {route}:

  • GET /oauth/authorize
  • GET /oauth/callback
  • POST /oauth/token
  • POST /oauth/register

Set http.route as a span attribute. Do NOT use custom names like authserver.authorize at the top level.

Child spans for domain operations:

  • authserver.upstream.exchange_code — wraps ExchangeCode() call
  • authserver.upstream.resolve_identity — wraps ResolveIdentity() call
  • authserver.upstream.refresh_tokens — wraps upstream token refresh

Skip in-memory storage spans — nanosecond operations produce noise without value. Add storage-level tracing later if/when a persistent backend (PostgreSQL) is introduced.

Authorize → Callback correlation via span links:
The authorize and callback are two separate HTTP requests correlated by the state parameter. Use an OTel span link (not parent-child):

  1. In AuthorizeHandler: serialize the span context (trace ID + span ID) into the PendingAuthorization struct stored to storage
  2. In CallbackHandler: reconstruct the span context and attach as a trace.Link on the callback span

This is the canonical OTel pattern for causally-related spans across separate traces.

Upstream HTTP client instrumentation:
Wrap the HTTP client in upstream.OAuth2Provider with otelhttp.NewTransport:

  • Automatically creates client spans for upstream IDP calls
  • Propagates traceparent/tracestate headers into outgoing requests
  • Pairs with the upstream.exchange.duration and upstream.refresh.duration histograms

Span attributes:

  • oauth.client_id, oauth.grant_type, oauth.upstream_provider
  • Standard HTTP attributes using stable semconv names (http.request.method, http.response.status_code — note: the existing pkg/telemetry/middleware.go uses pre-stable names)
  • Sensitive data (tokens, codes, PKCE) NEVER included
Provider plumbing
  • Extend NewEmbeddedAuthServer / NewHandler to accept trace.TracerProvider and metric.MeterProvider
  • Runner passes its existing providers when constructing the embedded authserver

Acceptance criteria

  • Standard http.server.request.duration metric emitted with stable semconv attributes
  • All custom counters, histograms, and UpDownCounters implemented as described
  • Top-level HTTP spans follow {METHOD} {route} naming convention
  • Child spans created for upstream IDP calls
  • Authorize→callback correlation implemented via span links
  • Upstream HTTP client wrapped with otelhttp.NewTransport
  • No in-memory storage spans
  • Errors recorded on spans via span.RecordError() and span.SetStatus(codes.Error, ...)
  • TracerProvider and MeterProvider injected as dependencies (not global access)
  • Runner plumbs its providers to the embedded authserver
  • Sensitive data never appears in span attributes or metric labels
  • Unit tests verify metrics recorded and spans created for success/failure paths

Dependencies

  • Depends on #3918 (HTTP middleware — request IDs useful for trace correlation)
  • Can be developed in parallel with the audit logging issue (#3919)

References

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with pkg/telemetry/middleware.go and pkg/vmcp/server/telemetry.go, then trace NewEmbeddedAuthServer/NewHandler through the runner and inspect upstream.OAuth2Provider. Implement the specified metrics, spans, provider injection, span-link correlation, and otelhttp transport without sensitive attributes or storage spans. Add unit tests covering success and failure paths for metrics and spans.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend-api-design, observability-sre
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.