| name | overview |
| description | Front-door orientation for `@warlock.js/logger` — structured channel-based logging with six severity levels (debug / info / warn / error / success / fatal), PII redaction floor, buffered file/JSON channels, optional SentryLog forwarding, async log.flush() + signal-flush on shutdown, ergonomic helpers (timer, assert). Standalone — no `@warlock.js/core` required. TRIGGER when: code imports anything from `@warlock.js/logger`; user asks "what does @warlock.js/logger do", "compare with pino / winston / bunyan", "structured logging for Node", "which logger should I use", "how do channels work"; package.json adds `@warlock.js/logger`. Skip: specific task already known — load the matching task skill directly (`logger-basics`, `configure-logger`, `pick-log-channel`, `write-custom-log-channel`, `ship-logs-to-sentry`, `redact-sensitive-log-fields`, `filter-log-entries`, `flush-logs-on-shutdown`, `capture-unhandled-errors`, `use-log-helpers`, `test-logging-code`); plain `console.log` in throwaway scripts. |
@warlock.js/logger — overview
Structured logging for Node. Six severity levels (with fatal strictly above error), a singleton plus a Logger class, channel-based fan-out (one entry → many sinks), PII redaction as a floor that channels can extend, buffered file writes with signal-triggered flush on shutdown, an awaitable async log.flush() for network/async channels, an optional Sentry channel, and a couple of ergonomic helpers (timer, assert) that turn boilerplate into one-liners.
Ships standalone — @warlock.js/core is not required. Drop it into any Node project.
When to reach for it
- Building a Node service that needs structured logs (key-value pairs, not bare strings) and you want them to land in multiple destinations (console for dev, JSON file for prod, third-party sink for audits) without rewriting the call sites.
- You'd reach for pino or winston but want a smaller surface that's already wired into Warlock conventions (
module / action / message shape, redaction floor, signal flush built-in).
- Your team agrees that
console.log doesn't survive contact with production — you need filtering, level routing, channel-specific sinks, and a redaction story before secrets leak into Slack/Datadog.
Skip if your code is a throwaway script where console.log is genuinely fine — there's no value in adding a dependency for one-off logs.
The mental model in one paragraph
You write log.info("auth", "login", "user signed in", { userId }). The logger fans that single entry out to every registered channel (ConsoleLog, FileLog, JSONFileLog, SentryLog, or your custom subclass). Each channel decides whether to emit it (per-level whitelist, per-channel filter predicate, logger-wide minimum severity). Redaction runs once at the logger level and can be extended per channel — never relaxed. Buffered channels (file + JSON file) drain on flush — synchronously via log.flushSync() / enableAutoFlush(['SIGINT', 'SIGTERM', 'beforeExit']), or asynchronously via await log.flush() (the only path that works for network channels like SentryLog). That's the whole package.
Skills index
Eleven task skills cover everything. Load the one that matches your job — most callers only ever need logger-basics + configure-logger + pick-log-channel.
Foundations
Start here. The log singleton, the six levels (debug / info / warn / error / success / fatal), how fan-out works, the module / action / message / context shape every entry carries.
Wire channels at boot — log.addChannel, log.setChannels, log.configure({ channels, autoFlushOn, redact, minLevel }). Branch on NODE_ENV, replace the channel list, isolate a library's logger from the host singleton.
Channels
Pick one of the four built-ins: ConsoleLog (terminal, colored), FileLog (plain .log on disk with rotation), JSONFileLog (structured JSON for aggregators — Datadog, Loki, ELK), SentryLog (errors + breadcrumbs to Sentry; @sentry/node is an optional peer).
The SentryLog channel in depth — event-vs-breadcrumb level mapping, dual init modes (reuse an existing client or pass options), draining via Sentry.flush(timeout), behavior when the optional peer isn't installed.
Extend LogChannel<Options> for sinks the built-ins don't cover — Slack, HTTP endpoint, in-memory buffer, database. The lazy init() lifecycle (setTimeout(0)), the terminal: true/false ANSI-stripping behavior, and the difference between flush() (async, network) and flushSync() (sync, files) are subtle — read this skill before subclassing.
Production concerns
Strip secrets before they reach a sink. Logger-wide setRedact({ paths, censor }) is the security floor; per-channel redact configs add paths (never remove). Dotted-glob paths (*, **); censor as string or function (value, path) => any.
Drop entries before they cost anything. Logger-wide setMinLevel("info") is the fast path; per-channel levels array + filter predicate for fine control.
Buffered channels need explicit drain. log.flushSync() (sync) for file channels — also wired by enableAutoFlush(['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK', 'SIGUSR2', 'beforeExit']). await log.flush() (async) for network/async channels like SentryLog — the only path that can await an HTTPS round-trip on a graceful shutdown.
captureAnyUnhandledRejection() hooks unhandledRejection (→ log.error("app", ...), process kept alive) and uncaughtException (→ log.fatal("app", ...) then process.exit(1) — restoring the non-zero exit the listener would otherwise suppress, so a fatal crash never becomes a silent exit 0; opt out with { exitOnUncaughtException: false }). It flushes before exiting and falls back to console.error when no terminal channel is set. One call at startup.
Ergonomics + testing
Two shortcuts every Logger exposes: log.assert(condition, module, action, message, context?) logs an error only when the condition is falsy (free on the happy path); log.timer(module, action) returns an end-function that emits info with a measured durationMs.
Silence the logger globally in tests via log.setChannels([]) in setupFiles. Assert specific entries with a capturing LogChannel subclass — it proves an entry was actually delivered through the pipeline (filters, redaction), not merely that a method was called, and it isolates cleanly by swapping log.channels.
Built-in channels at a glance
| Channel | Sink | terminal | Buffered |
|---|
ConsoleLog | process.stdout | true (colors kept) | no |
FileLog | .log files | false (ANSI stripped) | yes (5s timer or 100-entry buffer) |
JSONFileLog | .json files | false (ANSI stripped) | yes (same buffering) |
SentryLog | Sentry events + breadcrumbs | false | via the Sentry SDK's own transport (@sentry/node is an optional peer) |
terminal: true is the flag that decides whether the channel sees raw colored messages or stripped plain text. Custom channels: pick true if you write to a terminal, false for anything else.
What this package deliberately doesn't do
- Distributed tracing. Use OpenTelemetry. Logger gets you structured local logs with
module / action; trace correlation is a different problem.
- Log aggregator integrations beyond Sentry.
SentryLog is bundled (with @sentry/node as an optional peer). For Datadog, Loki, ELK, etc., either write a custom channel that POSTs to the aggregator or use JSONFileLog + a sidecar (fluentbit, vector, promtail).
- Pretty-printing of arbitrary objects.
ConsoleLog has a showContext flag that runs util.inspect on the context object; for richer formatting, use JSONFileLog and view the file through your favorite viewer.
- Log analysis. Querying / aggregating / alerting is on the sink side (Loki, ELK, Datadog).
See also
@warlock.js/core/warlock-conventions — the parent framework's conventions; logger is one of its foundation packages and ships transitively when you install core.
- When synced via agent-kit, this
overview/SKILL.md is flattened to the front-door skill .claude/skills/warlock-js-logger-overview/ — every cross-link above uses the @warlock.js/logger/<skill>/SKILL.md name form so it survives that flattening.