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 pinoBuild 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
| Option | Type | Default | Description |
|---|---|---|---|
client | AxonPush | required | The SDK client to publish through. |
channelId | string | required | Channel UUID for log events. |
serviceName | string | OTel service.name. | |
serviceVersion | string | OTel service.version. | |
environment | string | OTel deployment.environment. | |
agentId | string | Agent correlation id. | |
traceId | string | new trace | Seed an existing trace. |
mode | "background" | "sync" | "bullmq" | "background" | Publishing strategy. |
queueSize | number | 1000 | Records buffered before overflow. |
overflowPolicy | "drop-oldest" | "drop-newest" | "block" | "drop-oldest" | What a full queue does. |
shutdownTimeoutMs | number | 2000 | Drain budget on close(). |
concurrency | number | 1 | Parallel in-flight publishes. |
bullmqOptions | BullMQPublisherOptions | Required when mode: "bullmq". |
Flushing and closing
await stream.flush(1000); // drain, giving up after 1s
await stream.close(); // drain and stop the loopOn 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
| Field | Value |
|---|---|
identifier | "pino" |
eventType | "app.log" |
payload.timeUnixNano | pino’s time, converted to nanoseconds |
payload.severityNumber / severityText | mapped from pino’s numeric level |
payload.body | the msg field |
payload.attributes | every remaining field, plus host.name and process.pid |
payload.resource | service.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.