Telemetry
OpenTelemetry-native GenAI tracing, reuse your own TracerProvider and ship standard gen_ai.* spans to axonpush over OTLP.
@axonpush/sdk/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 (AxonPushSpanExporter)
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.
Install
The OpenTelemetry SDK ships as optional peers, loaded lazily so the base SDK runs without them:
npm install @axonpush/sdk @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/exporter-trace-otlp-http @opentelemetry/resourcesQuickstart
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.
import {
configureTelemetry,
genaiSpan,
recordGenaiResponse,
recordGenaiContent,
} from "@axonpush/sdk/telemetry";
const handle = await configureTelemetry({
serviceName: "my-agent",
environment: "prod",
});
const tracer = handle.tracer();
const span = genaiSpan(tracer, {
operation: "chat",
requestModel: "gpt-4o",
system: "openai",
agentName: "research-agent",
});
try {
const response = await callModel();
recordGenaiResponse(span, {
responseModel: "gpt-4o",
inputTokens: 1200,
outputTokens: 350,
reasoningTokens: 64,
cacheReadTokens: 900,
cacheWriteTokens: 300,
});
recordGenaiContent(span, { prompt, completion: response.text });
} finally {
span.end();
}
await handle.flush(); // serverless: call at the end of each invocationconfigureTelemetry resolves baseUrl, apiKey and channelId from the
option 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. It returns { flush, shutdown, tracer }. Unlike Python’s context
manager, you own the span lifecycle, call span.end(), typically in a
finally.
Reuse the app’s TracerProvider
configureTelemetry never replaces a provider you already own:
- Pass
tracerProviderand it attaches to that. - If the global provider is already a real SDK provider, it attaches to it.
- Otherwise it creates a
NodeTracerProviderwith a resource describing the service (service.name,deployment.environment.name,service.version) and registers 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.
Do not double-instrument a call. If a framework integration (LangChain, Mastra,
the Vercel AI middleware, AxonPushSpanExporter, …) already produces a span
or event for the same model call, do not also wrap it with genaiSpan, 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
genaiSpan starts a CLIENT span named "{operation} {requestModel}"
(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 agentName.
recordGenaiResponse 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:
recordGenaiResponse(span, {
responseModel: "gpt-4o",
finishReasons: ["stop"],
inputTokens: 1200,
outputTokens: 350,
reasoningTokens: 64,
cacheReadTokens: 900,
cacheWriteTokens: 300,
});Content capture and redaction
Prompts and completions are off by default. recordGenaiContent 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:
| Mode | Behaviour |
|---|---|
metadata_only (default) | Content is dropped; only models and token counts are kept. |
redacted | Long strings are truncated to short previews. |
full | Content is kept verbatim. |
Credential-shaped keys are always stripped regardless of mode, and any keys you
name in redactKeys are removed too:
recordGenaiContent(span, {
prompt,
completion: response.text,
contentCapture: "redacted",
redactKeys: ["email", "ssn"],
});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, call
flush() at the end of each invocation. configureTelemetry logs a hint when
it detects a serverless host, and flushAfterInvocation is re-exported to wrap
a Lambda handler for you.
import { flushAfterInvocation } from "@axonpush/sdk/telemetry";
await handle.flush(2000); // resolves true if it drained in time
await handle.shutdown(); // flush and stop the processor; idempotent
export const handler = flushAfterInvocation(handle, async (event) => {
// ...
});TypeScript SDK
Publish and trace agent events from Node.js and Bun with @axonpush/sdk, a typed REST client with framework integrations for LangChain, Vercel AI, Mastra, OpenAI Agents and more.
Integrations
Drop-in handlers for the agent frameworks, logging libraries and tracing stacks you already use, LangChain, LangGraph, OpenAI Agents, Anthropic, Vercel AI SDK, LlamaIndex, Mastra, Google ADK, plus pino, winston, console capture, OpenTelemetry, Sentry and BullMQ.