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:
- Your own collector, when
HAJER_OTLP_ENDPOINTis set (or, if it is not, the standardOTEL_EXPORTER_OTLP_ENDPOINT). Spans go to{endpoint}/v1/traces. No credential is sent unlessHAJER_OTLP_HEADERSnames one. - The Hajer platform, when
HAJER_API_KEYandHAJER_TEAM_IDare set,HAJER_DISABLEDis off andHAJER_TRACES_ENABLEDis on. Spans go to{HAJER_BASE_URL}/api/teams/{HAJER_TEAM_ID}/otel/v1/traces, with the key sent asAuthorization: Bearer <key>. - 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()andhajer.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.
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:
- The provider you pass to
hajer.configure(tracer_provider=...). - Your application's global OpenTelemetry
TracerProvider, if it is an SDKTracerProvider. Your own exporters, sampler and resource stay in place, and Hajer's exporter is added beside them. - 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: aHajerSettingsobject.Nonereads the environment.tracer_provider: an OpenTelemetryTracerProviderto put spans on.Noneuses the rule above.policy: a redaction policy fromhajer.build_policy(...).Noneuses 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:
| Attribute | From |
|---|---|
service.name | HAJER_SERVICE_NAME, else OTEL_SERVICE_NAME, else OpenTelemetry's default |
deployment.environment.name | HAJER_ENVIRONMENT |
hajer.sdk.version | The 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.
| Setting | Default | Meaning |
|---|---|---|
HAJER_TRACE_QUEUE_MAX | 2048 | Spans held for export. When the queue is full, new spans are dropped. |
HAJER_TRACE_BATCH_MAX | 128 | Spans per export request. |
HAJER_TRACE_BATCH_DELAY_MS | 5000 | How long a partial batch waits before it is sent anyway. |
HAJER_TRACE_EXPORT_TIMEOUT_MS | 5000 | Deadline for one export request. |
Failed exports are logged by OpenTelemetry on its own logger; they never raise into your code.
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:
Truewhen the tracer provider reports that the spans were exported,Falsewhen 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.