Send OpenTelemetry traces
If your agents are already instrumented with OpenTelemetry or OpenLIT, export their GenAI spans to Bairav and you get LLM-call and tool-call events, token usage and agent attribution without changing application code.
What you need
- An ingest token bound to a telemetry source of kind
otel. See Security events, and chooseotelas the source kind.
Point your exporter at Bairav
The receiver URL is:
https://<host>/api/ingest/otlp/v1/traces
Set it as the traces endpoint so the exporter does not append anything, and send the token in a header. With environment variables:
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://your-tenant.example.com/api/ingest/otlp/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="X-Ingest-Token=obt_xxxxxxxx.xxxxxxxx"
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="http/protobuf"
Or in code:
"""Export OpenTelemetry GenAI spans to Bairav (opentelemetry-sdk and opentelemetry-exporter-otlp-proto-http).
export BAIRAV_HOST=your-tenant.example.com
export BAIRAV_INGEST_TOKEN=obt_xxxxxxxx.xxxxxxxx # bound to a telemetry source of kind otel
"""
import os
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
exporter = OTLPSpanExporter(
# The full receiver URL. Pass it as the traces endpoint so the exporter does not append a path.
endpoint=f"https://{os.environ['BAIRAV_HOST']}/api/ingest/otlp/v1/traces",
headers={"X-Ingest-Token": os.environ["BAIRAV_INGEST_TOKEN"]},
)
provider = TracerProvider(resource=Resource.create({"service.name": "support-bot"})) # used as the agent name
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("example")
with tracer.start_as_current_span("chat gpt-4o-mini") as span:
# Only these GenAI attributes are read. Prompts, completions and tool arguments are never stored.
span.set_attribute("gen_ai.operation.name", "chat")
span.set_attribute("gen_ai.provider.name", "openai")
span.set_attribute("gen_ai.request.model", "gpt-4o-mini")
span.set_attribute("gen_ai.usage.input_tokens", 42)
span.set_attribute("gen_ai.usage.output_tokens", 17)
provider.shutdown() # flush before exit
Authorization: Bearer obt_... is accepted as an alternative to X-Ingest-Token.
What Bairav reads
| Span | Becomes |
|---|---|
gen_ai.operation.name is chat, text_completion, completion, generate_content or embeddings | An llm.call event |
gen_ai.operation.name is execute_tool, or the span has gen_ai.tool.name | A tool.invocation event |
| Any other span | Dropped silently. That is not an error. |
Only these attributes are read: provider (gen_ai.provider.name or gen_ai.system), model (gen_ai.response.model or gen_ai.request.model), tokens (gen_ai.usage.input_tokens or gen_ai.usage.prompt_tokens, and gen_ai.usage.output_tokens or gen_ai.usage.completion_tokens), error status, agent (gen_ai.agent.name, else service.name), tool (gen_ai.tool.name) and bairav.trace_id.
Prompts, completions, messages, system instructions and tool arguments are never read or stored, even if your exporter sends them. A sender's own cost estimate is ignored; Bairav prices usage from its own catalogue.
Limits and responses
- 1 MB per request, compressed and decompressed. Gzip is accepted with
Content-Encoding: gzip. - 600 spans a minute per token. Over the limit:
429withRetry-After. Limiter down:503withRetry-After. - Protobuf in, protobuf out; JSON in, JSON out, as the OTLP specification requires.
- A bad span is counted in
partialSuccess.rejectedSpans. It never fails the whole request. - The same span sent twice is one event.
401for a bad token,403when the token is not bound to anotelsource,415for a content type that is neither protobuf nor JSON.