axonpush
Python SDK

Telemetry

OpenTelemetry-native GenAI tracing, reuse your own TracerProvider and ship standard gen_ai.* spans to axonpush over OTLP.

pip install "axonpush[otel]"

axonpush.telemetry is the OpenTelemetry-native path. It reuses your application’s own TracerProvider, attaches a BatchSpanProcessor, and ships spans as real OTLP over HTTP to POST {base}/v1/traces. The spans follow the GenAI semantic conventions (gen_ai.operation.name, gen_ai.request.model, gen_ai.usage.*), so they are portable to any OTLP backend, not just axonpush, and there is no proprietary event format to translate.

Recommended over the legacy exporter

This is the recommended path for new code. The event-model OpenTelemetry span exporter still works and is supported, but it converts each span into a proprietary app.span event through the events API. This module emits standard OTLP instead and is now the preferred way to trace GenAI calls.

Quickstart

Set AXONPUSH_BASE_URL, AXONPUSH_API_KEY and AXONPUSH_CHANNEL_ID in the environment, then configure once at startup, wrap each model call in a GenAI span, and record usage when the response returns.

from axonpush.telemetry import (
    configure_telemetry,
    genai_span,
    record_genai_response,
    record_genai_content,
)

handle = configure_telemetry(
    service_name="my-agent",
    environment="prod",
)
tracer = handle.tracer()

with genai_span(
    tracer,
    operation="chat",
    request_model="gpt-4o",
    system="openai",
    agent_name="research-agent",
) as span:
    response = call_model(...)

    record_genai_response(
        span,
        response_model="gpt-4o",
        input_tokens=1200,
        output_tokens=350,
        reasoning_tokens=64,
        cache_read_tokens=900,
        cache_write_tokens=300,
    )
    record_genai_content(span, prompt=prompt, completion=text, handle=handle)

handle.flush()  # serverless: call at the end of each invocation

configure_telemetry resolves base_url, api_key and channel_id from the argument first, then the matching AXONPUSH_* variable. The exporter posts to {base}/v1/traces with X-API-Key for auth and X-Axonpush-Channel for routing. Every keyword is optional; a zero-argument call works once the environment is set.

Reuse the app’s TracerProvider

configure_telemetry never replaces a provider you already own:

  • Pass tracer_provider= and it attaches to that.
  • If the global provider is already a real SDK TracerProvider, it attaches to it.
  • Otherwise it creates one with a resource describing the service (service.name, deployment.environment.name, service.version) and installs it as the global provider.

So an app that already has OpenTelemetry keeps its own instrumentation and just gains the axonpush exporter. Attaching for the same (endpoint, channel) twice is a no-op, so calling configure_telemetry from more than one module is safe.

Do not double-instrument a call. If a framework integration (LangChain, OpenAI Agents, the OTel span exporter, …) already produces a span or event for the same model call, do not also wrap it with genai_span, you would emit the operation twice. Pick one plane: native OTLP through this module, or the events plane through the integrations.

Spanning a GenAI call

with genai_span(
    tracer,
    operation="chat",
    request_model="gpt-4o",
    system="openai",
    agent_name="research-agent",
) as span:
    ...

genai_span is a context manager that starts a CLIENT span named "{operation} {request_model}" (override with name=). It sets gen_ai.operation.name and gen_ai.request.model, and when supplied gen_ai.system / gen_ai.provider.name from system, and gen_ai.agent.name from agent_name.

record_genai_response records the response and usage attributes, including the cache-token counts that map to the semconv gen_ai.usage.cache_read_input_tokens and gen_ai.usage.cache_write_input_tokens:

record_genai_response(
    span,
    response_model="gpt-4o",
    finish_reasons=["stop"],
    input_tokens=1200,
    output_tokens=350,
    reasoning_tokens=64,
    cache_read_tokens=900,
    cache_write_tokens=300,
)

Content capture and redaction

Prompts and completions are off by default. record_genai_content emits them as span events (gen_ai.content.prompt / gen_ai.content.completion) rather than attributes, so large payloads do not inflate the span, and it is gated by the capture policy:

ModeBehaviour
metadata_only (default)Content is dropped; only models and token counts are kept.
redactedShort previews are kept, enough to recognise a run but not reconstruct it.
fullContent is kept verbatim.

Credential-shaped keys are always stripped regardless of mode, and any keys you name in redact_keys are removed too. Set the policy once on the handle:

handle = configure_telemetry(
    service_name="my-agent",
    content_capture="redacted",
    redact_keys=["email", "ssn"],
)

record_genai_content(span, prompt=prompt, completion=text, handle=handle)

Or pass the policy per call with content_capture= and redact_keys= instead of handle=.

Flush on shutdown

The BatchSpanProcessor exports in the background. Drain it on a graceful exit, and on serverless, where the container is frozen between invocations, so the atexit hook is unreliable, call flush() at the end of each invocation. configure_telemetry logs a hint when it detects a serverless host.

handle.flush(timeout_ms=2000)  # returns True if it drained in time
handle.shutdown()              # flush and stop the processor; idempotent