winston custom formats and an in-memory custom transport: working versions (tested 2026-10-15)
Build a winston logger with custom redact and filter formats, error stacks, child metadata, a fixed timestamp and a custom winston-transport that captures JSON entries for assertions.
passed6 passed · 0 failed2026-10-15 20:10 UTC- Status
- passed
- Last passed
- 2026-10-15 20:10 UTC
- Tests
- 6 / 6
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions4
winston@3.19.0winston-transport@4.9.0@types/node@26.6.5tsx@4.23.15
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 38799498972
The code
Exactly the files that passed. Your agent can pull them with get_recipe("winston-custom-format-transport") (install), or see them on GitHub.
package.json
{
"name": "winston-custom-format-transport",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"winston": "3.19.0",
"winston-transport": "4.9.0"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/logger.ts
import winston from "winston";
import Transport from "winston-transport";
const { combine, timestamp, errors, json, printf } = winston.format;
/** Custom format: masks secret-looking fields and adds a service tag. */
export const redact = winston.format((info, opts: { keys: string[] }) => {
for (const k of opts.keys) if (k in info) (info as Record<string, unknown>)[k] = "[REDACTED]";
return info;
});
/** Drops debug noise from a named module. */
export const dropModule = winston.format((info, opts: { module: string }) => (info.module === opts.module && info.level === "debug" ? false : info));
/** Custom transport that keeps entries in memory (for tests, or to forward to a queue). */
export class MemoryTransport extends Transport {
entries: Record<string, unknown>[] = [];
log(info: Record<string | symbol, unknown>, next: () => void) {
setImmediate(() => this.emit("logged", info));
this.entries.push(JSON.parse(info[Symbol.for("message")] as string));
next();
}
}
export function createLogger(mem: MemoryTransport, fixedTime = "2026-10-15T20:00:00.000Z") {
return winston.createLogger({
level: "debug",
defaultMeta: { service: "provenpin" },
format: combine(
errors({ stack: true }),
redact({ keys: ["password", "token"] }),
dropModule({ module: "noisy" }),
timestamp({ format: () => fixedTime }),
json(),
),
transports: [mem],
});
}
export const lineFormat = printf((i) => `${i.timestamp} ${i.level.toUpperCase()} ${i.message}`);
test/logger.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import winston from "winston";
import { createLogger, MemoryTransport, lineFormat } from "../src/logger.ts";
test("json entries with default meta and fixed timestamp", () => {
const mem = new MemoryTransport();
createLogger(mem).info("user signed in", { userId: 7 });
assert.deepEqual(mem.entries, [
{ level: "info", message: "user signed in", userId: 7, service: "provenpin", timestamp: "2026-10-15T20:00:00.000Z" },
]);
});
test("redacts secrets", () => {
const mem = new MemoryTransport();
createLogger(mem).warn("login", { user: "jp", password: "hunter2", token: "abc" });
assert.equal(mem.entries[0].password, "[REDACTED]");
assert.equal(mem.entries[0].token, "[REDACTED]");
assert.equal(mem.entries[0].user, "jp");
});
test("filter format drops debug from noisy module only", () => {
const mem = new MemoryTransport();
const log = createLogger(mem);
log.debug("tick", { module: "noisy" });
log.debug("tock", { module: "billing" });
log.error("boom", { module: "noisy" });
assert.deepEqual(mem.entries.map((e) => e.message), ["tock", "boom"]);
});
test("errors keep stack and child loggers add meta", () => {
const mem = new MemoryTransport();
const child = createLogger(mem).child({ requestId: "r-1" });
child.error(new Error("db down"));
assert.equal(mem.entries[0].message, "db down");
assert.equal(mem.entries[0].requestId, "r-1");
assert.match(String(mem.entries[0].stack), /Error: db down/);
});
test("level threshold", () => {
const mem = new MemoryTransport();
const log = createLogger(mem);
log.level = "warn";
log.info("hidden");
log.warn("shown");
assert.deepEqual(mem.entries.map((e) => e.message), ["shown"]);
});
test("printf line format", () => {
const info = lineFormat.transform({ level: "info", message: "hi", timestamp: "T" } as winston.Logform.TransformableInfo) as Record<symbol, string>;
assert.equal(info[Symbol.for("message")], "T INFO hi");
});
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
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.