Observability Export (OTLP)
Ship the audit trail — every AI request with its tool calls and governance decisions, plus the server log — to Datadog, Grafana/LGTM, Splunk or any OpenTelemetry collector.
On this page
TL;DR: The otlp_export job pushes the audit trail to any OpenTelemetry collector over OTLP/HTTP. Every completed AI request becomes a span, with its tool calls and governance decisions as child spans. Every server log line becomes a log record. To turn it on, add an observability.otlp block to the profile and restart; a profile without the block exports nothing. Neither profile shipped with this repository declares the block, so no telemetry leaves the instance until a deployment names a collector.
What is exported
| Signal | Source | Shape |
|---|---|---|
traces |
ai_requests (completed rows), tool_call_ledger, governance_decisions |
One span per AI request. Each tool call belonging to it, and each governance decision made under its trace, is a child span. |
logs |
the logs table |
One OTLP log record per row. |
A request span carries the OpenTelemetry GenAI attributes plus the platform's own:
- GenAI:
gen_ai.system,gen_ai.request.model,gen_ai.response.model,gen_ai.response.finish_reason, and the input, output, cache-read and cache-creation token counts. - Identity:
enduser.id,session.id,systemprompt.actor.id,systemprompt.actor.kind,systemprompt.client.kind. - Routing and cost:
systemprompt.route,systemprompt.served_provider(the provider that actually answered, which differs from the route's provider after a failover),systemprompt.wire_protocol,systemprompt.cost.usdandsystemprompt.cost.microdollars. - Timing:
systemprompt.latency_ms(total) andsystemprompt.upstream_latency_ms(time spent waiting on the provider). The difference between the two is the gateway's own overhead, so you can chart it in your own stack. Some older rows have no upstream figure. - Attribution:
systemprompt.scope.<dimension>andsystemprompt.scope.<dimension>.source(header,api_keyordefault) for every scope dimension the request was attributed to (hereproject,cost_centreandpracticeamong them), andsystemprompt.api_key.idfor the key that authenticated it. These are the chargeback keys. - Correlation:
systemprompt.request.id,systemprompt.trace.id,systemprompt.context.id,systemprompt.instance.id,systemprompt.request.kind,systemprompt.request.status.
Prompt and completion bodies are not exported. The spans carry the accounting and the chain, not the conversation.
Configure it
The block lives at the top level of the deployment's profile.yaml:
observability:
otlp:
endpoint: https://otel-collector.example.internal:4318
protocol: http # the only supported value; see below
headers:
Authorization: "Bearer <collector token>"
signals: [traces, logs] # default: both
batch_seconds: 15 # 1–3600; minimum spacing between two exports of a signal
endpointis the collector's base URL. The exporter appends/v1/tracesand/v1/logs, and uses an endpoint that already ends in one of those paths as given. Profile validation requires anhttp://orhttps://URL with a host.protocolmust behttp. The exporter speaks OTLP/HTTP with protobuf bodies.grpcis refused at profile validation, so point the exporter at your collector's HTTP receiver, conventionally port 4318.headersare sent verbatim on every export request. Put your vendor's API key or bearer token here. The production profile is not committed to git, so a token does not land in the repository.- The job runs every 15 seconds.
batch_secondssets a floor on how often each signal ships.
The profile is read once at boot, so adding or changing the block needs a restart. Without the block, the job is not scheduled at all.
Choosing a target
- Datadog: send OTLP/HTTP straight to the Datadog Agent's or OpenTelemetry Collector's OTLP receiver.
- Grafana / LGTM: send to Grafana Alloy or an OpenTelemetry Collector that forwards traces to Tempo and logs to Loki.
- Splunk: put an OpenTelemetry Collector in front, with Splunk HEC as its exporter. The platform does not speak HEC directly.
Google Cloud
Example, untested on GCP. This configuration follows the OpenTelemetry Collector Contrib
googlecloudexporter's documented options. Dynapps has not run it against a Google Cloud project; validate it in a non-production project first.
The gateway speaks OTLP/HTTP only, and Google Cloud's services do not take that push
directly from it, so run an OpenTelemetry Collector (the otelcol-contrib image, or
Google's build of it) beside the instance, for example as a sidecar or a small
Deployment on GKE. Its service account needs roles/cloudtrace.agent and
roles/logging.logWriter.
# otel-collector.yaml
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 75
batch: {}
resourcedetection:
detectors: [gcp]
exporters:
googlecloud:
project: <gcp-project-id> # omit to use the detected project
log:
default_log_name: systemprompt
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, resourcedetection, batch]
exporters: [googlecloud] # -> Cloud Trace
logs:
receivers: [otlp]
processors: [memory_limiter, resourcedetection, batch]
exporters: [googlecloud] # -> Cloud Logging
Then point the profile at the collector's HTTP receiver:
observability:
otlp:
endpoint: http://otel-collector:4318
protocol: http
signals: [traces, logs]
- Cloud Trace receives one trace per AI request, with tool calls and governance
decisions as child spans. Filter on
systemprompt.scope.projectorsystemprompt.served_provider. - Cloud Logging receives the server log under the
systempromptlog name. - BigQuery. A Cloud Logging sink with a BigQuery dataset as destination
(
gcloud logging sinks create <name> bigquery.googleapis.com/projects/<project>/datasets/<dataset> --log-filter='logName:"systemprompt"') lands the log records as tables. Spans go to Cloud Trace, not Cloud Logging, so the sink does not carry them; a chargeback table needs the span attributes (systemprompt.scope.project,systemprompt.scope.cost_centre,systemprompt.scope.practice,systemprompt.api_key.id,systemprompt.cost.microdollars,gen_ai.request.model) routed to the warehouse by an additional collector pipeline, which this example does not include.
Delivery guarantees
- At least once. Each signal has a watermark in
otlp_export_state. The watermark only advances after the collector acknowledges a batch.- A batch that fails is retried three times within the run.
- If it still fails, it keeps its cursor and is sent again at the next tick. Nothing is skipped.
- No duplicates on retry. Span and record ids are digests of the row keys, so a re-sent batch overwrites rather than duplicates.
- Retryable responses are the ones OTLP/HTTP names (429, 502, 503, 504) and connection failures. Any other 4xx is not retried within the run. The watermark still does not move, so the signal stalls until the cause (usually a wrong token or path) is fixed. The console page shows it as the last error, with growing lag.
- No backfill. A signal's watermark starts at "now" the first time it exports. History from before you configured the block is not sent. Use the archives for that.
The console page
/admin/system/observability (console roles) shows:
- The configured block: endpoint, protocol, signals, batch spacing.
- The export ledger: for each signal, the current watermark, lag (now minus watermark), last success, last error, and batch counters.
- Test connection: posts an empty OTLP/HTTP envelope with the exact URL, headers and content type the exporter uses. A 2xx means a real batch will be accepted. Nothing is exported and no watermark moves.
- Export now: runs one export immediately, without waiting for
batch_seconds.
What is not exported
-
Metrics. The Prometheus
/metricslistener carries series such as:gateway_upstream_failovers_total{from,to,reason}(every failover hop);systemprompt_quota_denials_total{subject_kind,dimension,mode}andsystemprompt_usage_anomaly_total{kind,subject_kind}(the alert signals);gateway_overhead_secondsandgateway_upstream_duration_seconds(latency histograms);otlp_export_batches_total{signal,status}.
Metrics are not part of the OTLP push. The listener only starts when the profile sets
server.metrics_port, and neither shipped profile sets it, so scrape it only after a deployment enables it. -
Alerts. The platform sends no email, chat or webhook alerts. Budget-threshold warnings and usage anomalies are recorded as log lines, database rows, metrics and (for
usage_anomaly_scan) ausage_anomalyevent. Once the export is on, those log lines reach your collector, and alert rules belong there. -
Archives and rollups. Archives stay on the instance's storage. See Data Retention & Archives.
Check it yourself
-
/admin/system/observability: use Test connection first, then Export now, then check that lag stays near the batch spacing. -
In your collector, filter for spans whose
systemprompt.instance.idmatches this instance, and compare a span'ssystemprompt.request.idwith/admin/requests/{id}. -
Confirm the job is scheduled (it only appears once the block is present), and that requests are arriving to export:
systemprompt infra jobs show otlp_export systemprompt infra logs request list --since 1h