OpenTelemetry
AxonPushSpanExporter is a SpanExporter for any OTel tracer provider, add it alongside your existing exporters and spans land in axonpush as app.span events.
If your service is already instrumented with OpenTelemetry, add
AxonPushSpanExporter to its tracer provider. Every span ships to axonpush as
an app.span event with an OTLP-shaped JSON payload, in parallel with whatever
other backends you export to.
Tested against @opentelemetry/api@^1.7 and
@opentelemetry/sdk-trace-base@^1.18, both optional peer dependencies.
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.
Install
npm install @axonpush/sdk @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/sdk-trace-nodeRegister the exporter
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { AxonPush } from "@axonpush/sdk";
import { AxonPushSpanExporter } from "@axonpush/sdk/integrations/otel";
const client = new AxonPush();
const exporter = new AxonPushSpanExporter({
client,
channelId: process.env.AXONPUSH_CHANNEL_ID!,
serviceName: "my-api",
serviceVersion: process.env.npm_package_version,
environment: "production",
});
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();A tracer provider accepts several processors, so this sits alongside an existing OTLP exporter rather than replacing it:
provider.addSpanProcessor(new SimpleSpanProcessor(otlpExporter));
provider.addSpanProcessor(new SimpleSpanProcessor(axonpushExporter));Instrument as usual, spans export when they end:
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("my-api");
await tracer.startActiveSpan("POST /chat", async (span) => {
span.setAttribute("http.method", "POST");
await callModel();
span.end();
});Prefer the native OTLP endpoint for raw throughput
This exporter publishes one client.events.publish(...) call per span. The
axonpush backend also speaks OTLP/HTTP directly at /v1/traces (and
/v1/logs), accepting protobuf or JSON. If you are already running a
collector, point it there instead, you get batching and compression that a
per-span exporter cannot.
Options
| Option | Type | Default | Description |
|---|---|---|---|
client | AxonPush | required | The SDK client to publish through. |
channelId | string | required | Channel UUID for span events. |
serviceName | string | Overrides service.name on the span’s resource. | |
serviceVersion | string | Overrides service.version. | |
environment | string | Overrides deployment.environment. | |
mode | "background" | "sync" | "bullmq" | "background" | Publishing strategy. |
queueSize | number | 1000 | Spans buffered before overflow. |
overflowPolicy | "drop-oldest" | "drop-newest" | "block" | "drop-oldest" | What a full queue does. |
shutdownTimeoutMs | number | 2000 | Drain budget on shutdown(). |
concurrency | number | 1 | Parallel in-flight publishes. |
bullmqOptions | BullMQPublisherOptions | Required when mode: "bullmq". |
The three resource options are merged over the span’s own resource attributes, so they win where they overlap.
Flushing and shutdown
await exporter.forceFlush(); // OTel's hook - drains the queue
await exporter.shutdown(); // drains and stops the loop
await exporter.flush(2000); // same as forceFlush, with a timeoutexport() returns success as soon as spans are queued, not once they are
delivered, the OTel result callback fires immediately. provider.shutdown()
calls exporter.shutdown(), so a graceful exit flushes everything pending. In
a Lambda handler, call forceFlush() at the end of each invocation or wrap the
handler with flushAfterInvocation, re-exported from this module. See the
pino serverless section.
Auto-instrumentation
OTel’s auto-instrumentation packages hook into the provider you already configured, so they need no axonpush-specific wiring:
npm install @opentelemetry/auto-instrumentations-nodeimport { registerInstrumentations } from "@opentelemetry/instrumentation";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
registerInstrumentations({
tracerProvider: provider,
instrumentations: [getNodeAutoInstrumentations()],
});Event shape
| Field | Value |
|---|---|
identifier | the span name |
eventType | "app.span" |
traceId / spanId / parentSpanId | lifted onto the event envelope, not just the payload |
payload.name | span name |
payload.kind | the numeric OTel SpanKind |
payload.startTimeUnixNano / endTimeUnixNano | hrtime pairs converted to nanosecond strings |
payload.flags | traceFlags from the span context |
payload.status | { code, message }, defaulting to code: 0 and an empty message |
payload.attributes | every span attribute |
payload.events / payload.links | present only when non-empty |
payload.resource | span resource merged with the exporter’s overrides |
payload.scope | instrumentation scope { name, version } |
metadata.framework | "opentelemetry" |
payload.kind is the raw enum value the OTel SDK sets, 0 for INTERNAL,
1 SERVER, 2 CLIENT, 3 PRODUCER, 4 CONSUMER, not a string.
Parent span id is read from parentSpanContext.spanId first, falling back to
the older parentSpanId field, so both recent and older SDK versions work.