@moonstar-x/logger
v2.0.0
Published
A tiny, zero-dependency logger for Node.js with levels, colors, timestamps, child loggers and JSON output.
Maintainers
Readme
@moonstar-x/logger
A tiny, zero-dependency logger for Node.js with levels, colors, timestamps, child loggers and JSON output.
- Zero dependencies. Nothing but
node:utilandnode:process. - Seven levels, from
tracetofatal, with asilentthreshold to turn everything off. - Pretty by default, JSON when you need it. One colored line per message for humans, one JSON object per line for log collectors.
- Child loggers. Namespaced names and inherited structured bindings.
- Configurable from the environment.
LOG_LEVEL,LOG_FORMAT,LOG_TIMESTAMP,NO_COLORandFORCE_COLORare all honored out of the box. - Written in TypeScript, shipped as both ESM and CJS with full type declarations.
Installation
npm install @moonstar-x/loggerThis package requires Node.js >=22.12.0.
Usage
The default logger
The package exports a ready-to-use logger, configured from the environment.
import logger from '@moonstar-x/logger';
logger.info('server listening on port 3000');
logger.warn('cache miss, falling back to the database');
logger.error(new Error('could not reach the database'));CommonJS works too:
const { default: logger } = require('@moonstar-x/logger');
logger.info('server listening on port 3000');Logger methods are not bound to their instance, so call them on the logger (
logger.info(...)) instead of destructuring them (const { info } = logger).
Logging messages
Every method takes any number of arguments. Strings are used as-is, everything else is inspected, which means objects are serialized and errors keep their stack trace.
logger.trace('entering the request handler'); // gray, stdout
logger.debug('resolved config', config); // green, stdout
logger.info('user signed in', { userId: 42 }); // cyan, stdout
logger.log('plain message'); // white, stdout
logger.warn('token is about to expire'); // yellow, stderr
logger.error(new Error('request failed')); // red, stderr
logger.fatal('unrecoverable, shutting down'); // magenta, stderrWhich prints:
(1:57:54 PM) - [TRACE] - entering the request handler
(1:57:54 PM) - [DEBUG] - resolved config { port: 3000 }
(1:57:54 PM) - [INFO] - user signed in { userId: 42 }
(1:57:54 PM) - [LOG] - plain message
(1:57:54 PM) - [WARN] - token is about to expire
(1:57:54 PM) - [ERROR] - Error: request failed
at file:///app/index.js:12:14
...
(1:57:54 PM) - [FATAL] - unrecoverable, shutting downYou can also clear the terminal, which is a no-op when the output is not a TTY:
logger.clear();Log levels
A logger only emits messages at or above its own level. log is an alias of info that keeps its own tag and color.
| Level | Severity | Method | Color | Destination |
| -------- | -------- | ----------------- | ------- | ----------- |
| trace | 10 | logger.trace() | gray | stdout |
| debug | 20 | logger.debug() | green | stdout |
| info | 30 | logger.info() | cyan | stdout |
| info | 30 | logger.log() | white | stdout |
| warn | 40 | logger.warn() | yellow | stderr |
| error | 50 | logger.error() | red | stderr |
| fatal | 60 | logger.fatal() | magenta | stderr |
| silent | — | — | — | — |
The level can be read, set, or checked at any time:
logger.level; // 'info'
logger.level = 'debug'; // throws a TypeError on an invalid level
logger.setLevel('debug'); // same, but chainable
if (logger.isLevelEnabled('debug')) {
logger.debug('expensive dump', buildExpensiveReport());
}Creating a logger
createLogger builds a logger from the environment, with the options you pass taking precedence.
import { createLogger } from '@moonstar-x/logger';
const logger = createLogger({
name: 'api',
level: 'debug',
format: 'pretty'
});
logger.info('ready');
// (1:57:54 PM) - [INFO] - (api) - readyTo ignore the environment entirely, construct the class directly:
import { Logger } from '@moonstar-x/logger';
const logger = new Logger({ level: 'warn' });Child loggers
child() creates a logger that inherits its parent's configuration. Names are joined with a colon and bindings are
merged, so subsystems are easy to tell apart.
const logger = createLogger({ name: 'api' });
const database = logger.child('db');
const pool = database.child({ name: 'pool', level: 'trace', bindings: { poolId: 1 } });
database.info('connected');
// (1:57:54 PM) - [INFO] - (api:db) - connected
pool.trace('acquired a connection');
// (1:57:54 PM) - [TRACE] - (api:db:pool) - acquired a connectionJSON output
Switching the format to json renders one JSON object per line, ready to be picked up by a log collector. This is
where bindings come in: they are structured fields added to every entry.
const logger = createLogger({
format: 'json',
name: 'api',
bindings: { service: 'checkout', version: '2.0.0' }
});
logger.info('order placed');{"level":"info","time":"2026-09-10T18:57:54.678Z","name":"api","service":"checkout","version":"2.0.0","msg":"order placed"}Bindings are serialized safely: circular references are replaced with [Circular], BigInt values are stringified,
and errors are turned into their name, message and stack.
Custom destinations
Anything with a write method is a valid destination, which makes it trivial to point a logger at a file, a socket or
an in-memory buffer — very handy in tests.
import fs from 'node:fs';
const file = fs.createWriteStream('app.log', { flags: 'a' });
const logger = createLogger({ format: 'json', stdout: file, stderr: file });const lines = [];
const logger = createLogger({ stdout: { write: (line) => lines.push(line) }, color: false });Configuration
Every option has a sensible default, so createLogger() is a valid call.
| Option | Type | Default | Description |
|-------------|----------------------------------------------------------------|------------------|-------------------------------------------------------------------------------------------------|
| bindings | Record<string, unknown> | {} | Structured fields added to every entry. Only rendered in json format. |
| color | boolean | auto-detected | Forces colors on or off, bypassing NO_COLOR, FORCE_COLOR and TTY detection. |
| depth | number \| null | null | How deep objects are inspected. null means unlimited. |
| format | 'pretty' \| 'json' | 'pretty' | How the output is rendered. |
| level | LogLevel | 'info' | The minimum severity that gets logged. |
| name | string | — | A name shown on every entry, useful to tell subsystems apart. |
| stderr | Destination | process.stderr | Where warn, error and fatal are written. |
| stdout | Destination | process.stdout | Where trace, debug, info and log are written. |
| timestamp | 'iso' \| 'time' \| ((date: Date) => string) \| false \| true | true | How the timestamp is rendered. true picks a locale time for pretty and ISO 8601 for json. |
Environment variables
createLogger and the default logger read these on startup. Explicit options always win.
| Variable | Effect |
|-----------------|-------------------------------------------------------------------------|
| LOG_LEVEL | The minimum severity, e.g. debug. Invalid values are ignored. |
| LOG_FORMAT | json or pretty. |
| LOG_TIMESTAMP | iso, time, or off / false to drop the timestamp. |
| NO_COLOR | Disables colors, per the NO_COLOR convention. |
| FORCE_COLOR | Forces colors on, even when the output is not a TTY. |
LOG_LEVEL=debug LOG_FORMAT=json node index.jsAPI
| Export | Description |
|--------------------------------------------------------------------------|--------------------------------------------------------------------------|
| logger (also the default export) | The default logger, configured from the environment. |
| createLogger(options?) | Creates a logger from the environment, with options taking precedence. |
| Logger | The logger class, for when the environment should be ignored. |
| optionsFromEnvironment(environment?) | Reads logger defaults from an environment object. |
| LOG_LEVELS, LEVEL_SEVERITY, LOG_METHODS | The level and method tables the logger is built on. |
| isLogLevel(value), shouldLog(level, threshold) | Level helpers. |
| formatPretty, formatJson, formatTimestamp, serializeArgs | The rendering primitives, exported for custom formatting. |
| createStyler(isEnabled), isColorSupported(destination, environment?) | The ANSI helpers. |
Types are shipped with the package: LoggerOptions, LogFormat, LogLevel, LoggableLevel, LogMethod,
MethodSpec, LogEntry, TimestampFormat, Destination, Styler and StyleName.
