axonpush
TypeScript SDKIntegrations

pino

Turn pino records into OpenTelemetry-shaped app.log events on a non-blocking background queue.

createAxonPushPinoStream returns a pino destination stream. Point a logger at it and every record becomes an axonpush app.log event with an OpenTelemetry-shaped payload, severity, body, attributes, resource.

Tested against pino@^9, which is an optional peer dependency.

Install

npm install @axonpush/sdk pino

Build the stream

import pino from "pino";
import { AxonPush } from "@axonpush/sdk";
import { createAxonPushPinoStream } from "@axonpush/sdk/integrations/pino";

const client = new AxonPush();
const stream = createAxonPushPinoStream({
  client,
  channelId: process.env.AXONPUSH_CHANNEL_ID!,
  serviceName: "my-api",
  serviceVersion: process.env.npm_package_version,
  environment: "production",
});

const log = pino({ level: "info" }, stream);

Then log normally. pino’s structured fields become OTel attributes:

log.info({ userId: 42, method: "oauth" }, "user signed in");
log.warn({ endpoint: "/api/search", remaining: 3 }, "rate limit approaching");
log.error({ endpoint: "/api/search", elapsedMs: 5000 }, "downstream timeout");

Non-blocking by default

write() parses the record and pushes it onto a bounded in-memory queue, then returns. A background loop drains the queue and awaits client.events.publish(...), so log.info(...) stays O(microseconds) on the caller’s path. Records are held in memory until drained, see Durability if losing them on a hard crash is unacceptable.

Options

OptionTypeDefaultDescription
clientAxonPushrequiredThe SDK client to publish through.
channelIdstringrequiredChannel UUID for log events.
serviceNamestringOTel service.name.
serviceVersionstringOTel service.version.
environmentstringOTel deployment.environment.
agentIdstringAgent correlation id.
traceIdstringnew traceSeed an existing trace.
mode"background" | "sync" | "bullmq""background"Publishing strategy.
queueSizenumber1000Records buffered before overflow.
overflowPolicy"drop-oldest" | "drop-newest" | "block""drop-oldest"What a full queue does.
shutdownTimeoutMsnumber2000Drain budget on close().
concurrencynumber1Parallel in-flight publishes.
bullmqOptionsBullMQPublisherOptionsRequired when mode: "bullmq".

Flushing and closing

await stream.flush(1000); // drain, giving up after 1s
await stream.close();     // drain and stop the loop

On a normal process exit, beforeExit, SIGTERM, or SIGINT, every live publisher is closed automatically, so long-running servers do not need to wire this up by hand.

Serverless

Frozen containers do not run drain loops. When the SDK detects AWS Lambda, Google Cloud Functions, or Azure Functions it logs a one-off reminder at startup. Wrap the handler so the queue drains before the container freezes:

import { flushAfterInvocation } from "@axonpush/sdk/integrations/pino";

export const handler = flushAfterInvocation(stream, async (event, _ctx) => {
  log.info({ event }, "processing event");
  return { statusCode: 200 };
});

flushAfterInvocation flushes in a finally block, so a throwing handler still ships its logs. It accepts an array if you have several flushables, and takes a { timeoutMs } option that defaults to 5000.

Durability

The default queue is in-memory

Under drop-oldest, the default, a full queue evicts the oldest record to make room for the newest, and the publisher warns at most once every 10 seconds with a running drop count. drop-newest preserves the backlog instead; block spins until a slot frees and makes submission effectively async. All three lose whatever is queued if the process is killed.

For logs you cannot afford to lose, set mode: "bullmq" and hand the records to Redis instead, see BullMQ.

Re-entrancy

The SDK’s own diagnostics run through consola. If the publisher logs a warning while it is mid-publish, that record would otherwise come straight back through this stream and loop. Records produced inside the publisher’s async scope are detected and skipped, so the loop cannot form.

Event shape

FieldValue
identifier"pino"
eventType"app.log"
payload.timeUnixNanopino’s time, converted to nanoseconds
payload.severityNumber / severityTextmapped from pino’s numeric level
payload.bodythe msg field
payload.attributesevery remaining field, plus host.name and process.pid
payload.resourceservice.name, service.version, deployment.environment
metadata.framework"pino"

Level mapping: 10 → TRACE (1), 20 → DEBUG (5), 30 → INFO (9), 40 → WARN (13), 50 → ERROR (17), 60 → FATAL (21). An unrecognised level falls back to INFO.

A chunk that is not valid JSON, pretty-printed output, or a stray write, is published verbatim as an INFO record rather than dropped.