axonpush
Python SDKIntegrations

OpenTelemetry

A SpanExporter that ships every span your service already produces to axonpush as app.span events.

pip install "axonpush[otel]"

Tested against opentelemetry-sdk>=1.20,<2.

For tracing GenAI model calls, the recommended path is now OpenTelemetry-native telemetry, which emits standard gen_ai.* spans over OTLP. This exporter (which ships spans as proprietary app.span events) still works and is supported, but is superseded for new code.

If your service is already instrumented with the OpenTelemetry SDK, this is the shortest path in: add one span processor and every span you produce lands in axonpush alongside your agent events, joined by the OTel trace id.

Plug it into the tracer provider

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

from axonpush import AxonPush
from axonpush.integrations.otel import AxonPushSpanExporter

client = AxonPush()

provider = TracerProvider()
provider.add_span_processor(
    BatchSpanProcessor(
        AxonPushSpanExporter(
            client=client,
            channel_id=channel_id,
            service_name="my-api",
            environment="production",
        )
    )
)
trace.set_tracer_provider(provider)

Every argument to the exporter is keyword-only.

Then instrument as you normally would, spans export when they close:

tracer = trace.get_tracer(__name__)

with tracer.start_as_current_span("POST /chat") as req:
    req.set_attribute("http.method", "POST")

    with tracer.start_as_current_span("llm.call") as llm:
        llm.set_attribute("gen_ai.request.model", "gpt-4o-mini")
        response = call_llm(...)

A TracerProvider accepts several processors, so this sits alongside whatever you already export to:

provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(...)))
provider.add_span_processor(BatchSpanProcessor(AxonPushSpanExporter(...)))

Constructor

AxonPushSpanExporter(
    *,
    client,                  # AxonPush or AsyncAxonPush - required
    channel_id,              # str UUID - required
    service_name=None,
    service_version=None,
    environment=None,
    mode=None,               # "background" (default) | "sync"
    queue_size=1000,
    shutdown_timeout=2.0,
)

service_name, service_version and environment are overlaid on top of the span’s own resource attributes, so they win over whatever the SDK’s Resource carried.

In "background" mode the exporter queues each span and a daemon thread publishes, keeping export() off the network. Pass a sync AxonPush, an AsyncAxonPush gets no worker and publishes inline.

Auto-instrumentation

The OTel auto-instrumentation packages hook into the provider you configured above, so they need no axonpush-specific wiring:

pip install opentelemetry-instrumentation-fastapi
pip install opentelemetry-instrumentation-sqlalchemy
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor

FastAPIInstrumentor().instrument_app(app)
SQLAlchemyInstrumentor().instrument(engine=engine)

Instrumenting HTTPX alongside this exporter used to feed itself: each publish made an HTTPX call, which produced a span, which was published, and so on. Since v0.0.12 every SDK request runs inside an OTel context flagged suppress_instrumentation and suppress_http_instrumentation, so HTTP instrumentors skip the SDK’s own calls. The OTEL_PYTHON_HTTPX_EXCLUDED_URLS workaround is no longer needed.

What each span becomes

One app.span event per span, with the OTel ids preserved end-to-end.

FieldValue
identifierThe span name
event_typeapp.span
trace_idThe OTel trace id, 32-hex
span_idThe OTel span id, 16-hex
parent_event_idThe parent span id, when the span has a parent
payload.traceId / payload.spanId / payload.parentSpanIdThe same ids inside the payload
payload.nameSpan name
payload.kindSpan kind as the OTel proto integer
payload.startTimeUnixNano / payload.endTimeUnixNanoSpan timing
payload.statuscode (0 unset, 1 ok, 2 error) and message
payload.attributesEvery span attribute, values coerced to JSON-safe types
payload.eventsSpan events: timeUnixNano, name, attributes
payload.linksSpan links: traceId, spanId, attributes
payload.resourceThe span’s resource, with your overrides applied
payload.scopeInstrumentation scope name and version

Every event carries framework: "opentelemetry" in metadata.

Spans go through the events API as app.span, not through the backend’s OTLP endpoint. That endpoint exists and accepts protobuf and JSON if you would rather point a collector at it, see the OTLP concept page.

Flushing

provider.force_flush()             # drains the BatchSpanProcessor
exporter.flush(timeout=2.0)        # drains the axonpush queue
exporter.shutdown()                # stop the worker

exporter.force_flush(timeout_millis=30000) satisfies the SpanExporter interface and calls flush() for you.

For Lambda and other freeze-between-invocations runtimes, flush_after_invocation is re-exported from this module and behaves as it does for the stdlib handler:

from axonpush.integrations.otel import AxonPushSpanExporter, flush_after_invocation

@flush_after_invocation(exporter)
def lambda_handler(event, context):
    ...