OpenTelemetry integration
AuditTrail ingests OTLP/JSON natively: any app instrumented with
OpenTelemetry GenAI semantic conventions (OpenLLMetry, Langfuse's
OTel-based SDKs, hand-rolled OTel) can send spans with zero AuditTrail
SDK installed — traces, the DAG, cost accounting and constitutional
rule evaluation all work from gen_ai.* attributes alone.
Endpoint
POST /api/v1/ingest/otlp
Content-Type: application/json
Authorization: Bearer sk-at-... (an AuditTrail API key)
The body is a standard OTLP ExportTraceServiceRequest in JSON
encoding. Protobuf is not accepted — configure your exporter for
http/json (a protobuf body gets a clear 415 telling you the same).
Batches are capped at 2,000 spans per request.
Exporter configuration
Whether you can point an SDK straight at AuditTrail depends on whether it can emit OTLP/JSON:
JS / Node — direct. The JS SDK implements the http/json protocol
natively, so env vars are the whole setup:
# Signal-specific endpoint — used verbatim (the generic
# OTEL_EXPORTER_OTLP_ENDPOINT would append /v1/traces per the OTel spec).
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://your-audittrail-host/api/v1/ingest/otlp"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/json"
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer sk-at-..."Python — bridge through an OpenTelemetry Collector. The Python SDK
implements only grpc and http/protobuf — setting
OTEL_EXPORTER_OTLP_PROTOCOL=http/json fails exporter selection (the
upstream JSON exporter package is still unreleased). Ship the default
protobuf to a local Collector and let it re-encode:
# otel-collector.yaml
receivers:
otlp:
protocols:
http: # listens on :4318
exporters:
otlphttp/audittrail:
traces_endpoint: https://your-audittrail-host/api/v1/ingest/otlp
encoding: json # re-encode protobuf -> OTLP/JSON
headers:
Authorization: Bearer sk-at-...
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlphttp/audittrail]# App side: default http/protobuf, pointed at the Collector.
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"(For Python apps you control, the first-party Python SDK is the native, Collector-free path.)
With OpenLLMetry (Python) the app side is then just:
pip install traceloop-sdkfrom traceloop.sdk import Traceloop
Traceloop.init(app_name="support-bot") # exports to the Collector above
# ... run your LangChain / LlamaIndex / raw-OpenAI app unchangedAttribute mapping
otlp_mapper.py translates GenAI semantic conventions to native span
fields; anything unmapped is preserved verbatim under span.attributes.
| OTel attribute | AuditTrail field |
|---|---|
gen_ai.operation.name (else OTel span kind) | span_type (llm / tool / agent / …) |
gen_ai.usage.input_tokens or …prompt_tokens | tokens_in |
gen_ai.usage.output_tokens or …completion_tokens | tokens_out |
gen_ai.response.model or gen_ai.request.model | model (drives cost accounting) |
gen_ai.content.prompt / gen_ai.content.completion | span input / output |
resource attrs (service.name, gen_ai.agent.*) | merged into every span |
Spans arriving through OTLP are first-class: the governor evaluates your YAML rules on them at ingest, they stream to the live dashboard over WebSocket, and they feed the causal-attribution pipeline like SDK spans.
Gotchas
- JSON only. A protobuf body 415s. JS/Node: set
OTEL_EXPORTER_OTLP_PROTOCOL=http/json. Python: the SDK cannot emithttp/json(grpc + http/protobuf only) — bridge through the Collector config above. - Spans missing
span_id,trace_idorstart_timeare skipped and counted in the response'srejectedtally with per-span error strings — the endpoint tells you exactly what it dropped. - Cost is only computed when a model attribute is present; content capture follows your instrumentation's privacy settings (OpenLLMetry gates prompt/completion capture behind its own flag).