Skip to content

feat(otel): OTLP/HTTP trace ingress - #128

Merged
prashar32 merged 2 commits into
mainfrom
feat/otlp-ingress
Jun 14, 2026
Merged

feat(otel): OTLP/HTTP trace ingress#128
prashar32 merged 2 commits into
mainfrom
feat/otlp-ingress

Conversation

@prashar32

Copy link
Copy Markdown
Owner

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/traces and meter GenAI spans from
apps 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:

RISKKERNEL_OTEL_INGRESS_ENABLED=true riskkernel serve
# on the app:
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:7070
export OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer $RISKKERNEL_API_TOKEN

What it does

  • POST /v1/traces, the standard OTLP path, accepting both encodings —
    protobuf (application/x-protobuf, the SDK default) and JSON — and replying with
    an OTLP ExportTraceServiceResponse in the same encoding.
  • Meters each span carrying gen_ai.usage.* token counts: reads the model
    (gen_ai.response.model, falling back to gen_ai.request.model) and
    gen_ai.system, prices via the existing table, and records through the ledger.
  • Correlates to a run by riskkernel.run.id (on the span, falling back to the
    resource), 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.
  • Off by default (RISKKERNEL_OTEL_INGRESS_ENABLED) and authenticated like the
    rest 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) and
google.golang.org/protobuf, both already in the module graph transitively via
the OTLP exporter we use for egress — promoted from indirect to direct, so this
adds no new module to go.sum. Hand-rolling protobuf parsing of
ExportTraceServiceRequest would 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.
  • Verified live end-to-end: started the daemon with ingress enabled, POSTed an
    OTLP JSON span with riskkernel.run.id, and confirmed the run appeared with the
    metered tokens and a priced cost.
  • go test -race ./... green; go vet ./... clean; gofmt clean.

Pinned attributes are documented in api/v1/otel-genai.md; user-facing usage in
docs/OTLP_INGRESS.md.

Closes #90

…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.
@prashar32
prashar32 merged commit 6fa7f5d into main Jun 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OTLP ingress: consume GenAI spans to govern apps RiskKernel didn't instrument

1 participant