feat(otel): OTLP/HTTP trace ingress - #128
Merged
Merged
Conversation
…roxy Add the consume side of the OpenTelemetry surface: an OTLP/HTTP trace receiver at POST /v1/traces. Point any OTel exporter at the daemon (OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:7070) and the GenAI model calls an app already traces — OpenLLMetry, the OpenAI Agents SDK, the Vercel AI SDK — show up against governed runs with tokens and cost metered into the same ledger the proxy uses. This makes spend visible for agents that don't route through the proxy or SDK. The receiver accepts both OTLP encodings (protobuf, the default, and JSON) using the canonical OTLP proto types (promoted from an existing transitive dep — no new module), walks the spans, and meters each one carrying gen_ai.usage.* token counts. It correlates to a run by riskkernel.run.id (on the span, falling back to the resource), pricing via the existing table and recording through the ledger. A GenAI span with no run id is observed and reported as a rejected span in the OTLP partial-success response; spans without usage are ignored. Token counts emitted as a double or numeric string are accepted rather than dropped. Scope is observe + meter: a consumed call is recorded (and marks the run halted if it crosses the budget) but not blocked after the fact, since it already happened — governing consumed spans is a separate step. The receiver is off by default (RISKKERNEL_OTEL_INGRESS_ENABLED) and authenticated like the rest of the API, so an exporter carries the bearer token via OTEL_EXPORTER_OTLP_HEADERS. Tests: protobuf and JSON over HTTP (verbatim decode + metering + partial-success reply), run-id from span and from resource, pricing of a consumed call, lenient numeric attribute types, and skipping of non-GenAI and unattributed spans. Pinned attributes documented in api/v1/otel-genai.md; usage in docs/OTLP_INGRESS.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RiskKernel already emits OpenTelemetry GenAI spans to your backend (the export
side of Surface 3). This adds the other half — ingress: RiskKernel can act as
an OTLP/HTTP trace endpoint at
POST /v1/tracesand meter GenAI spans fromapps already instrumented (OpenLLMetry, the OpenAI Agents SDK, the Vercel AI SDK),
so spend is visible for agents that never routed through the proxy or SDK.
Point an existing app's OTLP exporter at the daemon and the model calls it already
traces show up against governed runs, with tokens and cost metered into the same
ledger the proxy uses — the usual one env var:
What it does
POST /v1/traces, the standard OTLP path, accepting both encodings —protobuf (
application/x-protobuf, the SDK default) and JSON — and replying withan OTLP
ExportTraceServiceResponsein the same encoding.gen_ai.usage.*token counts: reads the model(
gen_ai.response.model, falling back togen_ai.request.model) andgen_ai.system, prices via the existing table, and records through the ledger.riskkernel.run.id(on the span, falling back to theresource), creating the run lazily under the default budget like the proxy's
run-id header. A GenAI span with no run id is observed and reported as a
rejected span in the OTLP partial-success response. Spans without usage (tool
calls, retrieval, framework spans) are ignored. Token counts emitted as a double
or numeric string are accepted rather than dropped to zero.
RISKKERNEL_OTEL_INGRESS_ENABLED) and authenticated like therest of the API.
Scope
Observe + meter: an ingested call already happened in the other app, so it's
recorded against the run's ledger (and marks the run halted if it crosses the
budget) but not blocked after the fact. Governing consumed spans — refusing the
next call on an externally-driven run — is a separate, future step. For
before-the-fact enforcement, route through the proxy (Surface 1) or SDK (Surface 2).
Dependency note
Uses the canonical OTLP proto types (
go.opentelemetry.io/proto/otlp) andgoogle.golang.org/protobuf, both already in the module graph transitively viathe OTLP exporter we use for egress — promoted from indirect to direct, so this
adds no new module to
go.sum. Hand-rolling protobuf parsing ofExportTraceServiceRequestwould be far more error-prone.Tests
internal/otel/ingress_test.go: protobuf and JSON over HTTP (verbatim decode +metering + the partial-success reply), run id from the span and from the
resource, pricing of a consumed call, lenient numeric attribute types, and
skipping of non-GenAI and unattributed spans.
OTLP JSON span with
riskkernel.run.id, and confirmed the run appeared with themetered tokens and a priced cost.
go test -race ./...green;go vet ./...clean;gofmtclean.Pinned attributes are documented in
api/v1/otel-genai.md; user-facing usage indocs/OTLP_INGRESS.md.Closes #90