Skip to content

Gateway OTEL Lifecycle

The LLM gateway emits a lifecycle trace and matching OTEL log events for LLM and relay proxy requests. The contract is intentionally request-centric: the root span is the full gateway request, child spans represent lifecycle stages, and lifecycle log events carry event.sequence so log backends can reconstruct in-request order even when async plugin work is involved.

Instrumentation name for lifecycle traces and logs:

guardian.gateway.lifecycle
gateway.request
gateway.request.prepare
gateway.plugin.invocation # request.start, when matching plugins run
gateway.verification
gateway.proxy
gateway.plugin.invocation # proxy.pre, when matching plugins run
gateway.upstream
gateway.plugin.invocation # proxy.post, when matching plugins run

Relay uses the same verify → hooks → forward path, but does not emit gateway.proxy / gateway.upstream child spans in the current release (C07). Request lifecycle + structured error logs are sufficient for operators to filter relay traffic.

gateway.request
gateway.request.prepare
gateway.plugin.invocation # request.start, when matching plugins run
gateway.verification
gateway.plugin.invocation # proxy.pre / proxy.post when hooks match /v1/relay

The root gateway.request span includes:

  • viper.state_cid when the inbound request provides X-Viper-State-CID
  • gateway.traffic_kind = llm | relay
  • http.route = provider template (/v1/anthropic_api/*, …) or /v1/relay
  • for relay after destination resolve: relay.destination_host (hostname only)
  • for LLM only: llm.provider, llm.operation (omitted on relay — not emitted empty)

request.end plugins are asynchronous. Their queued work carries W3C trace propagation and a reserved event.sequence value into the worker so worker plugin logs continue after the synchronous queue/request-end events. Worker plugin invocation spans are emitted as gateway.plugin.invocation spans with hook.name=request.end.

Every lifecycle log has correlation_id. Logs also include event.name as a durable attribute because some local Collector/file-exporter versions omit the OTEL log record’s top-level event name field. Logs emitted on the synchronous request path also carry a monotonic event.sequence for that request. Async request.end plugin worker logs continue from the reserved sequence captured when the queue item was written.

gateway.request.start
attrs (shared): correlation_id, viper.state_cid, http.method, gateway.uri, gateway.traffic_kind
attrs (LLM): llm.provider, llm.operation
attrs (relay): (no llm.* attrs)
gateway.request.prepare.start
attrs: correlation_id
gateway.request.prepare.end
attrs: correlation_id, llm.request_model, llm.stream_requested, gateway.route_supported, upstream.uri
(llm.* prepare fields only when an LLM body was parsed)
gateway.verification.start
attrs: correlation_id, viper.state_cid
gateway.verification.end
attrs: correlation_id, agent.id, signer.key_id, verification.outcome, verification.decision_code
gateway.proxy.start # LLM only
attrs: correlation_id, agent.id, llm.provider, llm.operation, upstream.uri
gateway.plugin.invocation.start
attrs: correlation_id, hook.name, plugin.id, plugin.mode, plugin.runtime, configuration.version, configuration.layer, plugin.order_index
gateway.plugin.invocation.end
attrs: correlation_id, hook.name, plugin.id, plugin.outcome, plugin.decision_code, plugin.status_code, plugin.error_code, plugin.latency_ms
gateway.plugin.invocation.queued
attrs: correlation_id, hook.name=request.end, configuration.version
gateway.upstream.start # LLM only
attrs: correlation_id, llm.provider, llm.operation, upstream.uri
gateway.upstream.end # LLM only
attrs: correlation_id, http.status_code, llm.response_model, llm.stream_observed
gateway.proxy.end # LLM only
attrs: correlation_id, gateway.route_type, policy.decision_code, http.status_code
gateway.request.end
attrs (shared): correlation_id, agent.id, gateway.route_type, policy.decision_code,
http.status_code, gateway.duration_ms, gateway.traffic_kind
attrs (relay): relay.destination_host (when destination was resolved)

Verification denials use verification.outcome=denied. Internal verification failures use verification.outcome=error.

Plugin denials are policy outcomes, not span errors. gateway.plugin.invocation spans are marked with OTEL error status only for plugin execution or context failures, such as timeouts, runtime errors, and invalid context writes.

Proxy and request completion severities follow the response outcome:

  • INFO for successful proxy responses.
  • WARN for local denials and other 4xx outcomes.
  • ERROR for upstream failures and 5xx outcomes.

Relay upstream transport failures and SSRF-floor destination_blocked denials emit gateway.proxy.upstream.error from guardian.gateway.proxy with gateway.traffic_kind=relay, gateway.route_type, policy.decision_code, and relay.destination_host when known. Full destination URLs are not logged on this path (path/query may carry secrets). Keep filtering on existing event name; discriminate traffic kind via attrs.

Lifecycle OTEL logs do not capture request bodies, response bodies, or raw headers. Payload inspection remains in the audit subsystem and plugin previews, where retention and redaction are controlled separately.