babypino
v1.0.0
Published
Minimalist, zero-dependency, pino-compatible logger that writes newline-delimited JSON to stdout — a lightweight drop-in default (with a console-backed browser build).
Maintainers
Readme
babypino
A minimalist, zero-runtime-dependency logger with a pino-compatible surface. It writes newline-delimited JSON to stdout, and nothing else.
What this is (and isn't)
babypino is not trying to be faster than pino, or more capable than pino. It does less, on purpose.
It exists for one niche: libraries that accept a pino logger from their users.
Such a library needs a default for when the user passes nothing. A no-op logger
hides everything; depending on pino drags a real logging stack (and its
dependencies) into every consumer. babypino is the middle ground - an extremely
lightweight, zero-dependency default that does the minimum useful thing (write
JSON lines to stdout) while exposing pino's Logger shape, so the same code path
works whether or not the user supplies a real logger.
function createWidget({ logger = new Babypino() } = {}) {
logger.info('widget created'); // works with babypino or a real pino instance
}In production, your users pass a serious pino instance with levels, redaction, transports, and async flushing. babypino is the friendly default, not the destination.
It is deliberately minimal: level gating is a no-op (every call is written),
isLevelEnabled() always returns true, and level is stored but never
filters. If you need any of pino's real machinery, use pino — see the FAQ.
Install
npm install babypinoUsage
import { Babypino, babypino } from 'babypino'; // or: const { Babypino, babypino } = require('babypino')
const log = new Babypino(); // or babypino() — a drop-in for pino(options?, stream?)
log.info('hello world');
log.info({ reqId: 'abc' }, 'handled %s in %dms', 'GET /', 12);
log.error(new Error('boom'));
const child = log.child({ service: 'api' });
child.warn('low disk');Output (one compact JSON object per line):
{"level":30,"time":1717761600000,"pid":1234,"hostname":"host","msg":"hello world"}Options
Matches pino's pino(options?, stream?) signature — the output stream is the
second argument (default process.stdout), and the first argument may be the
stream on its own:
new Babypino(); // or babypino()
new Babypino(options);
new Babypino(stream); // bare stream
new Babypino(options, stream);| option | default | effect |
| ------------ | ---------------------------- | --------------------------------------------------------------------- |
| colour | stream.getColorDepth() > 1 | ANSI colour on/off, or a partial Record<Level, string> of overrides |
| base | { pid, hostname } | static fields added to every line; null to omit |
| name | — | added to every line as a name field |
| enabled | true | when false, the logger is a no-op |
| level | 'info' | initial level label — stored & returned, but does not gate output |
| msgPrefix | — | string prefixed to every message; children concatenate onto it |
| crlf | false | use \r\n line endings instead of \n |
| timestamp | true | include the epoch-millis time field; false omits it |
| messageKey | 'msg' | key the message is written under |
| errorKey | 'err' | key the serialized error is written under |
| nestedKey | — | nest a merged object under this key instead of spreading it |
These are all output-shaping options — they change the line babypino writes,
nothing more. Any other pino option (e.g. redact, serializers, transport)
type-checks via a catch-all index signature but has no effect.
logger.versionreports the pino API version babypino targets (e.g.10.x), not babypino's own package version. Some pino-aware code readslogger.versionto feature-detect, so it mirrors pino rather than this package.
pino-compatible behaviour
- Object-first merge:
log.info({ a: 1 }, 'msg')merges{ a: 1 }into the record. - Error-first:
log.error(err)serialises toerr: { type, message, stack, ...ownProps }and setsmsgtoerr.messagewhen no explicit message is given. util.formatinterpolation for the message and trailing args.- Numeric levels (
trace:10 … fatal:60) and epoch-millistime, matching pino's defaults. - Child loggers inherit parent bindings (child keys win) and share the stream.
The surface (trace/debug/info/warn/error/fatal/silent, child, bindings,
setBindings, isLevelEnabled, level, levels, flush) is structurally
compatible with pino's Logger. This is enforced at build time — see
test/types.test-d.ts, which fails to type-check if the
surface ever drifts from import('pino').Logger.
Browser
There is a separate, console-backed build (like pino's own browser.js).
Bundlers pick it up automatically via the package's browser export — your
import doesn't change:
import { babypino } from 'babypino'; // resolves to the browser build when bundling for the webIt is intentionally different from the Node build: no JSON serialisation, no
pid/hostname/time, no streams. Each call is forwarded to the matching
console method (with the logger's bindings prepended), so DevTools render
objects and errors natively:
babypino().child({ reqId: 'r1' }).info({ items: 3 }, 'cart updated');
// → console.info({ reqId: 'r1' }, { items: 3 }, 'cart updated')Same Babypino / babypino API and types; it honours level, enabled,
msgPrefix, and onChild (the record-shaping options don't apply to console).
Zero node: imports, so it bundles with nothing to polyfill.
Because there is no node:events in the browser, the build can't extend
EventEmitter — but, like pino's browser logger, it still exposes on/off/
emit/once/addListener/… as no-ops, so consumer code that calls
logger.on('level-change', …) won't throw (those events simply never fire).
FAQ
Do you support transports (pino.transport)?
Does it spark joy?
Do you support redaction (redact)?
Does it spark joy?
Do you support custom serializers?
Does it spark joy?
Do you support pretty-printing (pino-pretty)?
Does it spark joy?
Do you support asynchronous logging / sonic-boom?
Which part of "minimalist" keeps tripping you up?
Do you support custom levels (customLevels)?
Which part of "minimalist" keeps tripping you up?
Do you support level filtering / isLevelEnabled?
isLevelEnabled returns true. Always.
Do you support pino.multistream?
Which part of "minimalist" keeps tripping you up?
Do you support mixin, hooks, or formatters?
Which part of "minimalist" keeps tripping you up?
This is a serious production logger though, right? No. That's the whole point. Pass a real pino in production; keep this as the default.
Development
Requires Node.js 18+ (the dev toolchain — tsx/tsup/mocha — needs it; c8 coverage needs 20+). The shipped runtime itself supports Node 16+.
npm run build # bundle src/ → dist (ESM + CJS + types) via tsup
npm run lint # strict, type-aware ESLint (typescript-eslint)
npm run typecheck # tsc --noEmit (implementation + pino compatibility check)
npm test # prettier + lint + typecheck, then mocha with c8 coverage
npm run bench # microbenchmark: babypino vs real pinoThe package ships zero runtime dependencies; everything above is dev-only.
License
MIT
