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-sqlalchemyfrom 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.
| Field | Value |
|---|---|
identifier | The span name |
event_type | app.span |
trace_id | The OTel trace id, 32-hex |
span_id | The OTel span id, 16-hex |
parent_event_id | The parent span id, when the span has a parent |
payload.traceId / payload.spanId / payload.parentSpanId | The same ids inside the payload |
payload.name | Span name |
payload.kind | Span kind as the OTel proto integer |
payload.startTimeUnixNano / payload.endTimeUnixNano | Span timing |
payload.status | code (0 unset, 1 ok, 2 error) and message |
payload.attributes | Every span attribute, values coerced to JSON-safe types |
payload.events | Span events: timeUnixNano, name, attributes |
payload.links | Span links: traceId, spanId, attributes |
payload.resource | The span’s resource, with your overrides applied |
payload.scope | Instrumentation 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 workerexporter.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):
...