Skip to main content

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 choose otel as 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:

samples/ingest/otel_exporter.py
"""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​

SpanBecomes
gen_ai.operation.name is chat, text_completion, completion, generate_content or embeddingsAn llm.call event
gen_ai.operation.name is execute_tool, or the span has gen_ai.tool.nameA tool.invocation event
Any other spanDropped 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.

Content is never stored

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: 429 with Retry-After. Limiter down: 503 with Retry-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.
  • 401 for a bad token, 403 when the token is not bound to an otel source, 415 for a content type that is neither protobuf nor JSON.