npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@azerothjs/logger

v2.1.0

Published

AzerothJS logger - one zero-dependency logger with two faces: colored, iconed developer output and pino-class NDJSON for production, plus the framework's startup banner

Readme

@azerothjs/logger

AzerothJS logger - one zero-dependency logger with two faces: colored, iconed developer output and pino-class NDJSON for production, plus the framework's startup banner

npm License: MIT Node >= 22


Part of AzerothJS - the fine-grained fullstack framework. One zero-dependency logger with two faces: colored, iconed developer output on a TTY, and pino-class NDJSON for production - plus the banner every AzerothJS process starts with.


Install

[!NOTE] ESM-only, Node >= 22:

npm install @azerothjs/logger

Overview

import { createLogger } from '@azerothjs/logger';

const log = createLogger({ redact: ['authorization'] });

log.info('server listening', { port: 3000 });
log.warn('slow handler', { route: '/api/search', durationMs: 1203 });
log.error('request failed', { error: new Error('upstream timeout', { cause: reason }) });

const requestLog = log.child({ requestId: 'r-42' }); // context binds once, rides every line
requestLog.debug('cache miss', { key: 'user:7' });

The SAME calls render for whoever is watching:

A developer's terminal (TTY, not production) - one designed line per event: a dim seconds clock, the level icon in its color, the message bold, fields hanging off dim interpuncts, errors as indented blocks with the cause chain walked:

14:32:07 ● server listening · port=3000
14:32:09 ▲ warn  slow handler · route=/api/search · durationMs=1203
14:32:11 ✖ error request failed
    at handler (src/routes.ts:42:11)
    caused by: Error: upstream timeout

Production / a pipe / a collector - byte-clean NDJSON, one line per event, stable key order, epoch-millis time, no ANSI ever:

{"level":"info","time":1770000000000,"msg":"server listening","port":3000}
{"level":"warn","time":1770000002421,"msg":"slow handler","route":"/api/search","durationMs":1203}

Face selection is automatic (TTY + NODE_ENV !== 'production' = pretty, anything else = NDJSON) and overridable: createLogger({ face: 'ndjson' }) in code, or AZEROTH_LOG=json, AZEROTH_LOG=debug, AZEROTH_LOG=pretty:trace from the environment. In a browser, levels map onto styled console methods.


What the design promises

  • A disabled level is free. Below-threshold methods ARE a shared no-op - a log.trace(...) in a hot loop costs a plain call, so instrumentation can stay in production code. Guard genuinely expensive field construction with log.enabled('debug').
  • Child context is serialized once. child(fields) pre-renders its bindings into the NDJSON line, so contextual logging (request ids, job ids) costs about what one extra string concat costs - not a re-serialization on every line.
  • Redaction happens before any sink. redact: ['authorization', 'cookie'] replaces values at the logger, so no formatter, transport, or adapter can leak them.
  • Errors are first-class. Any Error field serializes as { name, message, stack } with the full cause chain (depth-capped against cycles).
  • The color social contract is honored. NO_COLOR always wins; FORCE_COLOR colors a pipe for CI viewers; a non-TTY stream is byte-clean; icons degrade to ASCII on terminals that cannot render them.

The pretty face's design

The developer face renders MEANING, not just strings, and its rules are deliberate:

  • info lines drop the level word - the icon carries it; warn/error keep theirs and read louder by contrast, with their message tinted in the level's color.

  • Semantic values: an http(s):// URL renders in the brand ice-blue; a status field that is a real status code renders as a verdict (2xx green, 3xx cyan, 4xx yellow, 5xx red); everything else stays plain on purpose.

  • Request sentences: a record carrying method, path, status, and durationMs - the shape @azerothjs/http's logRequests emits - reads as prose, with the method in its REST color:

    14:32:12 ● GET /healthz → 200 · 0.48ms
    14:32:15 ✖ error POST /orders → 500 · 3.1ms
  • hide: a context field bound on every line (service in a single-service dev terminal) is noise to a human and signal to a collector - hide it from THIS face only, every other face and file keeps it:

    import { createLogger, prettySink } from '@azerothjs/logger';
    
    const log = createLogger({ sink: prettySink({ hide: ['service', 'requestId'] }), fields: { service: 'api' } });

Display only, always: values are styled, never altered, and the NDJSON faces carry every field with full epoch timestamps.


The banner

The framework's face at startup - @azerothjs/http's serve() and the Vite dev plugin print it, and anything built on AzerothJS can too:

import { printBanner } from '@azerothjs/logger';

printBanner({
    version: '1.4.0',
    subtitle: 'worker',
    entries: [['Queue', 'jobs'], ['Concurrency', '8']],
    readyMs: performance.now() - startedAt
});
  ▲ AzerothJS v1.4.0  worker

    Queue        jobs
    Concurrency  8

    ✓ Ready in 12 ms

printBanner self-gates: it renders only on an interactive terminal outside production - a piped or collected stream never carries it. The ready time is whatever YOU measured; the banner never invents numbers.


Environment

| Variable | Effect | | --- | --- | | AZEROTH_LOG | debug (level), json / pretty (face), or both: json:info | | NO_COLOR | disables all color, everywhere, unconditionally | | FORCE_COLOR | forces color onto a non-TTY (1/2/3 = 16/256/truecolor) | | NODE_ENV=production | auto face picks NDJSON and the banner stays silent |


Log files

[!NOTE] The file sinks are Node-only (they use node:fs/node:path), so they live at the @azerothjs/logger/node subpath. The main @azerothjs/logger entry stays browser-safe - import createLogger and the console/pretty/tee sinks from there, the file sinks from /node.

Point the logger at a file to append forever, or at a FOLDER for day-named files with size rotation and retention - rotation is rename-free (a new name opens, the old file just stops growing), which is what makes it correct on Windows, where an open file cannot be renamed and antivirus loves holding handles:

import { createLogger } from '@azerothjs/logger';
import { fileStream } from '@azerothjs/logger/node';

// logs/app-2026-07-21.ndjson, app-2026-07-21.2.ndjson, ... - NDJSON, never ANSI
const log = createLogger({ stream: fileStream('logs/', { maxFileBytes: 32 * 1024 * 1024, maxFiles: 14 }) });

This rides the fused NDJSON fast path untouched. Lines batch in a bounded buffer and hit disk on a size threshold, a flush interval (default 1 s), flush()/close(), and process exit - so a process.exit() or a graceful SIGTERM loses nothing, and an external hard kill loses at most the flush interval. When the disk cannot keep up or a write fails, lines are DROPPED and counted - never blocking the event loop, never growing unbounded - with one stderr notice and a log lines dropped record on recovery: logging must never break the system.

Both faces at once - pretty for eyes, a file for the record. terminalSink() is the terminal half done right: pretty on a dev TTY (or under AZEROTH_LOG=pretty), NDJSON when piped or in production. A URL target anchors the folder to your package instead of the process cwd:

import { createLogger, teeSink, terminalSink } from '@azerothjs/logger';
import { fileSink } from '@azerothjs/logger/node';

const file = fileSink(new URL('../logs/', import.meta.url));
const log = createLogger({ sink: teeSink(terminalSink(), file) });
// on shutdown: file.close()  (flushes; process exit also flushes automatically)

And with @azerothjs/http, one line per request - pretty on your terminal, NDJSON in the folder. Compose the tee: a bare stream: fileStream(...) would send EVERYTHING to the file and leave your terminal silent (a file is not a TTY, so the logger picks its NDJSON face for it - and no environment variable can repoint an explicit stream):

import { createLogger, teeSink, terminalSink } from '@azerothjs/logger';
import { fileSink } from '@azerothjs/logger/node';

const log = createLogger({ sink: teeSink(terminalSink(), fileSink('logs/')) });
new App({ observe: logRequests(log) });

Custom sinks

A sink is one function; everything else is composition:

import { createLogger } from '@azerothjs/logger';

const seen: unknown[] = [];
const log = createLogger({ sink: (record) => seen.push(record) }); // a test spy

The record - { level, message, time, fields } - is the whole contract; an OpenTelemetry adapter, a test spy, or @azerothjs/http's request logging all consume it structurally. For files, prefer fileStream/fileSink above - they add the batching, rotation, and crash-safety a bare write stream does not have.


Related

Part of the AzerothJS monorepo. Related packages: @azerothjs/http (whose request logging consumes this record shape), @azerothjs/ws (WebSockets), and @azerothjs/cron (scheduling).