ProvenPin Install

pino logging with redaction, request context and a custom transport: working versions (tested 2026-10-15)

A pino 10 logger with path and wildcard redaction, error serialisers, string levels and an AsyncLocalStorage mixin for request ids, plus a pino-abstract-transport alert sink combined via multistream, tested offline against in-memory streams.

passed5 passed · 0 failed2026-10-15 15:02 UTC
Status
passed
Last passed
2026-10-15 15:02 UTC
Tests
5 / 5
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
pino@10.4.0pino-abstract-transport@3.0.0tsx@4.23.15@types/node@26.6.5
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38755782594

The code

Exactly the files that passed. Your agent can pull them with get_recipe("pino-redaction-custom-transport") (install), or see them on GitHub.

package.json
{
  "name": "pino-redaction-custom-transport",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "pino": "10.4.0",
    "pino-abstract-transport": "3.0.0"
  },
  "devDependencies": {
    "tsx": "4.23.15",
    "@types/node": "26.6.5"
  }
}
src/logger.ts
import { AsyncLocalStorage } from "node:async_hooks";
import pino, { type DestinationStream } from "pino";
import build from "pino-abstract-transport";

export const requestContext = new AsyncLocalStorage<{ requestId: string; userId?: string }>();

/**
 * App logger: redacts secrets, serialises errors, and stamps every line with the current
 * request id from AsyncLocalStorage through a mixin.
 */
export function createLogger(destination: DestinationStream, level = "info") {
  return pino({
    level,
    base: { service: "billing" },
    timestamp: pino.stdTimeFunctions.isoTime,
    formatters: { level: (label) => ({ level: label }) }, // "level":"info" instead of 30
    redact: {
      paths: ["password", "*.password", "headers.authorization", "card.number", "users[*].email"],
      censor: "[REDACTED]",
    },
    serializers: { err: pino.stdSerializers.err },
    mixin: () => requestContext.getStore() ?? {},
  }, destination);
}

/**
 * A custom transport built with pino-abstract-transport. It receives parsed log objects and
 * forwards only warn and above to a sink (a stand-in for Slack, Sentry, a queue...).
 * Used here in-process as a destination; the same module can run in a worker via pino.transport.
 */
export async function alertTransport(opts: { sink: (line: { level: string; msg: string; requestId?: string }) => void; minLevel?: number }) {
  const min = opts.minLevel ?? 40;
  const levelNum: Record<string, number> = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60 };
  return build(async (source) => {
    for await (const obj of source) {
      if ((levelNum[obj.level] ?? obj.level) >= min) opts.sink({ level: obj.level, msg: obj.msg, requestId: obj.requestId });
    }
  });
}
test/logger.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { Writable } from "node:stream";
import pino from "pino";
import { createLogger, requestContext, alertTransport } from "../src/logger.ts";

/** Collects JSON lines written by pino. */
function memory() {
  const lines: any[] = [];
  const stream = new Writable({ write(chunk, _enc, cb) { for (const l of chunk.toString().split("\n").filter(Boolean)) lines.push(JSON.parse(l)); cb(); } });
  return { stream, lines };
}

test("redacts secrets at top level, nested, and in arrays", () => {
  const m = memory();
  const log = createLogger(m.stream);
  log.info({ password: "hunter2", headers: { authorization: "Bearer abc", host: "x" }, card: { number: "4242424242424242", last4: "4242" }, users: [{ email: "[email protected]", id: 1 }] }, "signup");
  const [l] = m.lines;
  assert.equal(l.password, "[REDACTED]");
  assert.equal(l.headers.authorization, "[REDACTED]");
  assert.equal(l.headers.host, "x");
  assert.equal(l.card.number, "[REDACTED]");
  assert.equal(l.card.last4, "4242");
  assert.equal(l.users[0].email, "[REDACTED]");
  assert.equal(l.level, "info");
  assert.equal(l.service, "billing");
  assert.match(l.time, /^\d{4}-\d{2}-\d{2}T/);
});

test("child loggers add bindings and wildcard redaction still applies", () => {
  const m = memory();
  const child = createLogger(m.stream).child({ module: "invoices" });
  child.warn({ customer: { password: "pw", id: "c1" } }, "odd login");
  assert.equal(m.lines[0].module, "invoices");
  assert.equal(m.lines[0].customer.password, "[REDACTED]");
  assert.equal(m.lines[0].customer.id, "c1");
});

test("errors are serialised with type, message and stack", () => {
  const m = memory();
  createLogger(m.stream).error({ err: new TypeError("bad input") }, "failed");
  assert.equal(m.lines[0].err.type, "TypeError");
  assert.equal(m.lines[0].err.message, "bad input");
  assert.match(m.lines[0].err.stack, /TypeError: bad input/);
});

test("mixin stamps the AsyncLocalStorage request context on every line", async () => {
  const m = memory();
  const log = createLogger(m.stream, "debug");
  await requestContext.run({ requestId: "req-42", userId: "u7" }, async () => {
    log.debug("start");
    await new Promise((r) => setTimeout(r, 1));
    log.info("after await");
  });
  log.info("outside");
  assert.deepEqual(m.lines.map((l) => [l.msg, l.requestId ?? null]), [["start", "req-42"], ["after await", "req-42"], ["outside", null]]);
  assert.equal(m.lines[0].userId, "u7");
});

test("custom pino-abstract-transport forwards only warn and above", async () => {
  const alerts: { level: string; msg: string; requestId?: string }[] = [];
  const transport = await alertTransport({ sink: (a) => alerts.push(a) });
  const m = memory();
  const log = createLogger(pino.multistream([{ stream: m.stream, level: "debug" }, { stream: transport, level: "debug" }]), "debug");
  requestContext.run({ requestId: "req-9" }, () => {
    log.info("fine");
    log.warn("disk 91%");
    log.error("payment provider down");
  });
  transport.end();
  await new Promise((r) => transport.on("close", r));
  assert.equal(m.lines.length, 3);
  assert.deepEqual(alerts, [
    { level: "warn", msg: "disk 91%", requestId: "req-9" },
    { level: "error", msg: "payment provider down", requestId: "req-9" },
  ]);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.