Skip to main content

Exporting spans

The SDK exports spans over OTLP/HTTP using the OpenTelemetry SDK, which the hajer[otel] extra installs. This page covers where spans go, which tracer provider they use, and how to make sure they leave a short-lived process.

Without the otel extra, every decorator and wrapped client still runs the code it wraps and records calls locally, but no span is emitted or exported. The hajer logger says so once, and hajer doctor shows the otel line as missing.

Where spans go​

The SDK picks one destination per process, in this order:

  1. Your own collector, when HAJER_OTLP_ENDPOINT is set (or, if it is not, the standard OTEL_EXPORTER_OTLP_ENDPOINT). Spans go to {endpoint}/v1/traces. No credential is sent unless HAJER_OTLP_HEADERS names one.
  2. The Hajer platform, when HAJER_API_KEY and HAJER_TEAM_ID are set, HAJER_DISABLED is off and HAJER_TRACES_ENABLED is on. Spans go to {HAJER_BASE_URL}/api/teams/{HAJER_TEAM_ID}/otel/v1/traces, with the key sent as Authorization: Bearer <key>.
  3. Nowhere, otherwise.

A collector always wins over the platform: if you set an endpoint, your key is not sent with it.

# Send to a local OpenTelemetry Collector instead of Hajer
export HAJER_OTLP_ENDPOINT=http://localhost:4318
export HAJER_OTLP_HEADERS="x-tenant=acme"

HAJER_OTLP_HEADERS uses OpenTelemetry's key=value,key2=value2 format, with values percent-decoded. With the platform as destination, these headers are sent in addition to the Authorization header.

Run hajer doctor to see which destination is in force and, if none, why:

traces https://api.hajer.ai/api/teams/<team-id>/otel/v1/traces (auth: bearer)

Inert mode​

The SDK is inert when HAJER_API_KEY or HAJER_TEAM_ID is missing, or HAJER_DISABLED=1 is set. In inert mode:

  • your code, decorators and wrapped clients run normally,
  • calls are still recorded locally (hajer.wrapped_calls() and hajer.scope() work),
  • nothing is sent to Hajer, no socket is opened for it, and nothing raises.

A missing key is not a configuration error. This lets you merge the integration into a repository whose test suite has no Hajer credentials.

HAJER_DISABLED=1 stops export to Hajer whatever key is set. HAJER_TRACES_ENABLED=0 also stops export to Hajer; with either, spans still go to your application's own tracer provider, if it has one.

note

A collector named by HAJER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT is used even in inert mode, including with HAJER_DISABLED=1. To stop all export from the SDK, unset the endpoint as well.

Which tracer provider is used​

The SDK adds its exporter, as a BatchSpanProcessor, to one tracer provider, chosen in this order:

  1. The provider you pass to hajer.configure(tracer_provider=...).
  2. Your application's global OpenTelemetry TracerProvider, if it is an SDK TracerProvider. Your own exporters, sampler and resource stay in place, and Hajer's exporter is added beside them.
  3. Otherwise, a provider the SDK builds for itself. The SDK never installs it as the global provider, so it does not change where other libraries' spans go.

The exporter is added once per provider, however many times you call configure().

Reusing your OpenTelemetry setup​

If your application already configures OpenTelemetry, set it up before the first Hajer span or model call, and the SDK uses it automatically:

from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

import hajer
from openai import OpenAI

provider = TracerProvider(resource=Resource.create({"service.name": "support-api"}))
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint="https://otel.example.com/v1/traces")))
trace.set_tracer_provider(provider)

# Hajer spans now go to your collector and, with a key, to Hajer as well.
client = hajer.wrap(OpenAI())

To name the provider explicitly, or if it is not the global one:

hajer.configure(tracer_provider=provider)

The SDK chooses the provider the first time it emits a span. If you install a global provider after that, call hajer.configure() again; the next span uses the new choice.

configure​

hajer.configure(settings=None, *, tracer_provider=None, policy=None)

configure sets process-wide options for every span emitted afterwards:

  • settings: a HajerSettings object. None reads the environment.
  • tracer_provider: an OpenTelemetry TracerProvider to put spans on. None uses the rule above.
  • policy: a redaction policy from hajer.build_policy(...). None uses the default catalog. See Content capture and redaction.

Each call replaces the whole configuration; hajer.configure() with no arguments resets to defaults. An open span is not affected. If the SDK built its own provider for the previous configuration, that provider is shut down.

configure(settings=...) applies to span export. wrap, instrument and attach read the environment themselves, or take their own settings= argument.

Resource attributes​

When the SDK builds its own provider, the provider's resource carries:

AttributeFrom
service.nameHAJER_SERVICE_NAME, else OTEL_SERVICE_NAME, else OpenTelemetry's default
deployment.environment.nameHAJER_ENVIRONMENT
hajer.sdk.versionThe SDK version, for example 0.2.0

OpenTelemetry's default telemetry.sdk.* attributes are included too. When the SDK uses your provider, your resource is used unchanged.

Batching​

Export never blocks a model call. Spans are queued and sent in batches on a background thread.

SettingDefaultMeaning
HAJER_TRACE_QUEUE_MAX2048Spans held for export. When the queue is full, new spans are dropped.
HAJER_TRACE_BATCH_MAX128Spans per export request.
HAJER_TRACE_BATCH_DELAY_MS5000How long a partial batch waits before it is sent anyway.
HAJER_TRACE_EXPORT_TIMEOUT_MS5000Deadline for one export request.

Failed exports are logged by OpenTelemetry on its own logger; they never raise into your code.

warning

The platform accepts at most 4 MiB per OTLP request and answers larger ones with 413. A model span can carry up to HAJER_WRAPPED_CALL_MAX_BYTES (32 KiB by default) of content, so a full batch of 128 such spans is already close to that limit. If you raise HAJER_WRAPPED_CALL_MAX_BYTES, lower HAJER_TRACE_BATCH_MAX accordingly.

Flushing​

hajer.flush(timeout_ms=None) -> bool

flush waits for spans emitted so far to be exported. timeout_ms defaults to HAJER_TRACE_FLUSH_TIMEOUT_MS (2000). It returns:

  • True when the tracer provider reports that the spans were exported,
  • False when there is no provider to flush, the flush timed out or failed. It never raises.

At interpreter exit, an atexit hook flushes once and shuts down the exporter the SDK added. An unreachable receiver delays exit by at most one export timeout (HAJER_TRACE_EXPORT_TIMEOUT_MS).

Short-lived scripts, workers and serverless​

atexit hooks do not run in every environment. A serverless function can be frozen between invocations, and a worker can be killed. Spans waiting for the next batch are then lost. Call hajer.flush() at the end of each unit of work:

import hajer
from openai import OpenAI

client = hajer.wrap(OpenAI())


@hajer.workflow("wf_summarize")
def summarize(text: str) -> str:
response = client.chat.completions.create(
model="gpt-5",
messages=[{"role": "user", "content": f"Summarize: {text}"}],
)
return response.choices[0].message.content


def lambda_handler(event, context):
try:
return {"summary": summarize(event["text"])}
finally:
hajer.flush()

Sending from any OpenTelemetry exporter​

The platform endpoint is a standard OTLP/HTTP traces receiver. It accepts protobuf or JSON from any OpenTelemetry exporter, in any language, with your team API key as the bearer token:

export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://api.hajer.ai/api/teams/$HAJER_TEAM_ID/otel/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer%20$HAJER_API_KEY"

Spans that follow the OpenTelemetry GenAI semantic conventions are shown as generations. Add session.id and user.id to group them into sessions.