Skip to content

Latest commit

Β 

History

History
931 lines (695 loc) Β· 24.2 KB

File metadata and controls

931 lines (695 loc) Β· 24.2 KB

Logger

Stability: 1.1 - Active Development

This module is only available with the --experimental-logger CLI flag.

The node:logger module provides high-performance structured logging capabilities for Node.js applications. It uses diagnostics_channel internally to dispatch log events to consumers, allowing multiple consumers to receive logs independently.

Why node:logger instead of console

The built-in console.log() and related console APIs are designed for simple text output and debugging during development. They write directly to stdout/stderr as unstructured text, provide no log levels beyond log/warn/error/debug, and offer no mechanism for routing logs to different destinations or filtering by severity in production environments.

node:logger addresses these limitations with capabilities that production applications typically require:

  • Structured output: Log records are emitted as structured objects (e.g., JSON) rather than plain text, making them machine-parseable and compatible with log aggregation systems like Elasticsearch, Datadog, and Splunk.
  • Log levels with filtering: Six severity levels (trace through fatal) with numeric ordering allow fine-grained control over which logs are emitted. Both loggers and consumers can set independent minimum levels, so debug logs can be written to a file without appearing on the console.
  • Producer-consumer separation via diagnostics_channel: Loggers (producers) and consumers are decoupled through diagnostics_channel. Application code logs without knowing where logs go; consumers decide the destination (stdout, files, network). Multiple consumers can process the same log records independently.
  • Child loggers with context propagation: logger.child() creates loggers that automatically include inherited context fields (e.g., requestId, service) in every log record, eliminating the need to manually pass context through call chains.
  • Serializers and the serialize symbol: Custom serializers and the [serialize]() symbol ensure sensitive data (passwords, tokens) is excluded from logs and complex objects are reduced to loggable representations without manual transformation at each call site.
  • Zero-cost level checks: logger.debug.enabled allows skipping expensive computation when a level is disabled, something console does not support.
  • No third-party dependency required: node:logger provides structured logging out of the box, removing the need for userland loggers like pino or winston for common use cases.
import { Logger, JSONConsumer } from 'node:logger';

const logger = new Logger({ level: 'info' });
const consumer = new JSONConsumer({ level: 'info' });
consumer.attach();

logger.info('Hello world');
// Outputs: {"level":"info","time":1234567890,"msg":"Hello world"}
const { Logger, JSONConsumer } = require('node:logger');

const logger = new Logger({ level: 'info' });
const consumer = new JSONConsumer({ level: 'info' });
consumer.attach();

logger.info('Hello world');
// Outputs: {"level":"info","time":1234567890,"msg":"Hello world"}

Log levels

The logger supports the following log levels, in order of severity:

Level Description
trace Detailed debugging information
debug Debug information
info General information
warn Warning messages
error Error messages
fatal Critical errors

Log levels follow RFC 5424 severity ordering (lowest to highest).

Class: Logger

The Logger class is used to create log records and publish them to diagnostics_channel channels.

new Logger([options])

  • options {Object}
    • level {string} Minimum log level. Default: 'info'.
    • bindings {Object} Context fields added to all log records.
    • serializers {Object} Custom serializer functions for specific fields. Default: {}.

Creates a new Logger instance.

import { Logger } from 'node:logger';

const logger = new Logger({
  level: 'debug',
  bindings: { service: 'my-app', version: '1.0.0' },
});
const { Logger } = require('node:logger');

const logger = new Logger({
  level: 'debug',
  bindings: { service: 'my-app', version: '1.0.0' },
});

logger.trace(msg[, fields])

logger.trace(obj)

logger.trace(error[, fields])

  • msg {string} Log message. When called with a string, logs that string as the message.
  • fields {Object} Additional fields to include in the log record. Only used with the string msg signature. Each key-value pair is added to the log record. Values must be JSON-serializable (strings, numbers, booleans, null, plain objects, and arrays). Values that are not JSON-serializable (such as BigInt, functions, or Symbol) will cause JSON.stringify() to throw. If a field key matches a registered serializer, the serializer is applied to the value before serialization.
  • obj {Object} Object containing a required msg {string} property and additional fields that will be included in the log record. All properties other than msg are treated as log fields and follow the same serialization rules as fields. This form is useful when the set of fields is determined dynamically or when using spread syntax.
  • error {Error} Error object to log. The error's message property becomes the log message and the error is serialized into the err field.

Logs a message at the trace level.

// String message
logger.trace('Detailed trace message');

// String message with additional fields
logger.trace('User action', { userId: 123, action: 'click' });

// Object form: msg is required, all other properties become log fields
logger.trace({ msg: 'Object format', requestId: 'abc123', duration: 42 });

logger.debug(msg[, fields])

logger.debug(obj)

logger.debug(error[, fields])

  • msg {string} Log message. When called with a string, logs that string as the message.
  • fields {Object} Additional fields to include in the log record. Only used with the string msg signature.
  • obj {Object} Object containing a required msg {string} property and additional fields that will be included in the log record.
  • error {Error} Error object to log. The error's message property becomes the log message and the error is serialized into the err field.

Logs a message at the debug level.

logger.debug('Debug information');
logger.debug('Processing request', { requestId: 'abc123' });

logger.info(msg[, fields])

logger.info(obj)

logger.info(error[, fields])

  • msg {string} Log message. When called with a string, logs that string as the message.
  • fields {Object} Additional fields to include in the log record. Only used with the string msg signature.
  • obj {Object} Object containing a required msg {string} property and additional fields that will be included in the log record.
  • error {Error} Error object to log. The error's message property becomes the log message and the error is serialized into the err field.

Logs a message at the info level.

logger.info('Server started');
logger.info('Request received', { method: 'GET', path: '/api/users' });
logger.info({ msg: 'User logged in', userId: 123 });

logger.warn(msg[, fields])

logger.warn(obj)

logger.warn(error[, fields])

  • msg {string} Log message. When called with a string, logs that string as the message.
  • fields {Object} Additional fields to include in the log record. Only used with the string msg signature.
  • obj {Object} Object containing a required msg {string} property and additional fields that will be included in the log record.
  • error {Error} Error object to log. The error's message property becomes the log message and the error is serialized into the err field.

Logs a message at the warn level.

logger.warn('Deprecated API used');
logger.warn('High memory usage', { memoryUsage: process.memoryUsage() });

logger.error(msg[, fields])

logger.error(obj)

logger.error(error[, fields])

  • msg {string} Log message. When called with a string, logs that string as the message.
  • fields {Object} Additional fields to include in the log record. Only used with the string msg signature.
  • obj {Object} Object containing a required msg {string} property and additional fields that will be included in the log record.
  • error {Error} Error object to log. The error's message property becomes the log message and the error is serialized into the err field.

Logs a message at the error level.

logger.error('Database connection failed');
logger.error(new Error('Something went wrong'));
logger.error(new Error('Request failed'), { requestId: 'abc123' });

logger.fatal(msg[, fields])

logger.fatal(obj)

logger.fatal(error[, fields])

  • msg {string} Log message. When called with a string, logs that string as the message.
  • fields {Object} Additional fields to include in the log record. Only used with the string msg signature.
  • obj {Object} Object containing a required msg {string} property and additional fields that will be included in the log record.
  • error {Error} Error object to log. The error's message property becomes the log message and the error is serialized into the err field.

Logs a message at the fatal level.

logger.fatal('Application crash');
logger.fatal(new Error('Unrecoverable error'));

logger.child(bindings[, options])

Stability: 1.1 - Active Development

  • bindings {Object} Additional context fields for the child logger.
  • options {Object}
    • level {string} Log level for the child logger.
    • serializers {Object} Additional serializers for the child logger.
  • Returns: {Logger} A new child logger instance.

Creates a child logger with additional context bindings. Child loggers inherit the parent's configuration and add their own bindings to all log records.

Note for library authors: The level option in child() is intended for application code only. Library and module authors should NOT override the log level in child loggers. Instead, libraries should inherit the parent logger's level to respect the application developer's log level configuration. Application developers can use this feature to isolate specific components or adjust verbosity for particular subsystems they directly control.

import { Logger } from 'node:logger';

const logger = new Logger({ bindings: { service: 'my-app' } });
const requestLogger = logger.child({ requestId: 'abc123' });

requestLogger.info('Processing request');
// Log includes: service: 'my-app', requestId: 'abc123'
const { Logger } = require('node:logger');

const logger = new Logger({ bindings: { service: 'my-app' } });
const requestLogger = logger.child({ requestId: 'abc123' });

requestLogger.info('Processing request');
// Log includes: service: 'my-app', requestId: 'abc123'

logger.<level>.enabled

  • {boolean} true if the level is enabled, false otherwise.

Each log method (trace, debug, info, warn, error, fatal) has an enabled property that indicates whether that level is enabled for this logger.

Use this to check if a level is enabled before performing expensive computations:

if (logger.debug.enabled) {
  // Perform expensive debug computation only if debug is enabled
  logger.debug('Debug info', { expensiveData: computeDebugData() });
}

// Typos will throw a TypeError (safer than silent failure)
// logger.debg.enabled β†’ TypeError: Cannot read properties of undefined

For dynamic level checks, use property access:

const level = config.logLevel; // 'info', 'debug', etc.
if (logger[level]?.enabled) {
  logger[level]('Dynamic log message');
}

Class: LogConsumer

The LogConsumer class is the base class for log consumers. Consumers subscribe to diagnostics_channel events and process log records.

One channel is published per log level. The channel names are:

  • log:trace
  • log:debug
  • log:info
  • log:warn
  • log:error
  • log:fatal

Advanced users may subscribe to these channels directly via diagnostics_channel.channel(name) instead of using a LogConsumer.

new LogConsumer([options])

  • options {Object}
    • level {string} Minimum log level to consume. Default: 'info'.

Creates a new LogConsumer instance.

consumer.attach()

Attaches the consumer to log channels. After calling this method, the consumer will receive log records from all loggers.

const consumer = new JSONConsumer({ level: 'info' });
consumer.attach();
// Consumer now receives all log records at 'info' level and above

consumer.detach()

Detaches the consumer from log channels. After calling this method, the consumer will no longer receive log records.

const consumer = new JSONConsumer({ level: 'info' });
consumer.attach();
// ... later
consumer.detach();
// Consumer no longer receives log records

consumer.<level>.enabled

  • {boolean} true if the level is enabled, false otherwise.

Each log level (trace, debug, info, warn, error, fatal) has an enabled property that indicates whether that level is enabled for this consumer.

const { LogConsumer } = require('node:logger');

const consumer = new LogConsumer({ level: 'info' });

console.log(consumer.debug.enabled); // false (below threshold)
console.log(consumer.info.enabled);  // true
console.log(consumer.error.enabled); // true

// Typos will throw a TypeError (safer than silent failure)
// consumer.debg.enabled β†’ TypeError: Cannot read properties of undefined

consumer.handle(record)

  • record {Object} The log record to handle.
    • level {string} Log level.
    • msg {string} Log message.
    • time {number} Timestamp in milliseconds.
    • bindingsStr {string} Pre-serialized bindings JSON string.
    • fields {Object} Additional log fields.

Handles a log record. Subclasses must implement this method.

import { LogConsumer } from 'node:logger';

class CustomConsumer extends LogConsumer {
  handle(record) {
    console.log(`[${record.level}] ${record.msg}`);
  }
}
const { LogConsumer } = require('node:logger');

class CustomConsumer extends LogConsumer {
  handle(record) {
    console.log(`[${record.level}] ${record.msg}`);
  }
}

Class: JSONConsumer

  • Extends: {LogConsumer}

The JSONConsumer class outputs log records as JSON to a stream.

new JSONConsumer([options])

  • options {Object}
    • level {string} Minimum log level to consume. Default: 'info'.

    • stream {number|string|Object} Output destination. One of:

      • A file descriptor (number).
      • A file path (string).
      • A stream-like object implementing all of the following methods:
        • write(chunk)
        • flush(callback)
        • flushSync()
        • end()

      A plain stream.Writable (e.g. from fs.createWriteStream()) does not satisfy this contract because it lacks flush()/flushSync(); wrap it or use a stream that implements the required methods. Default: stdout (fd 1).

    • fields {Object} Additional fields to include in every log record. Default: {}.

Creates a new JSONConsumer instance.

import { JSONConsumer } from 'node:logger';

// Output to stdout (default)
const consumer1 = new JSONConsumer({ level: 'info' });

// Output to a file
const consumer2 = new JSONConsumer({
  level: 'debug',
  stream: '/var/log/app.log',
});

// Output to stderr
const consumer3 = new JSONConsumer({
  level: 'error',
  stream: 2,
});

// Add fields to every log
const consumer4 = new JSONConsumer({
  level: 'info',
  fields: { hostname: 'server-1', env: 'production' },
});
const { JSONConsumer } = require('node:logger');

// Output to stdout (default)
const consumer1 = new JSONConsumer({ level: 'info' });

// Output to a file
const consumer2 = new JSONConsumer({
  level: 'debug',
  stream: '/var/log/app.log',
});

Non-JSON-serializable values

The JSONConsumer uses JSON.stringify() internally. Values that are not JSON-serializable will cause an error at log time:

  • BigInt values throw a TypeError (BigInt value can't be serialized in JSON).
  • Symbol values are silently omitted by JSON.stringify().
  • Functions are silently omitted by JSON.stringify().
  • Circular references throw a TypeError.

Error objects are an exception: the built-in err serializer runs by default and converts them to plain objects (with type, message, stack, and a recursively serialized cause) before they reach JSON.stringify(), so logging errors directly does not trigger the issues above.

To log BigInt values, convert them to strings or numbers first:

logger.info('Large number', { id: bigIntValue.toString() });

To handle non-serializable types automatically, use a custom serializer:

const logger = new Logger({
  serializers: {
    count: (value) => (typeof value === 'bigint' ? value.toString() : value),
  },
});

consumer.flush([callback])

  • callback {Function} Called when flush completes.

Flushes pending writes to the underlying stream.

consumer.flush(() => {
  console.log('All logs flushed');
});

consumer.flushSync()

Flushes pending writes synchronously.

consumer.flushSync();

consumer.end()

Closes the consumer and its underlying stream.

consumer.end();

logger.stdSerializers

  • {Object}

An object containing standard serializer functions for common objects.

stdSerializers.err(error)

  • error {Error} Error object to serialize.
  • Returns: {Object} Serialized error object.

Serializes an Error object for logging. Includes type, message, stack, and any additional properties. Traverses the cause chain if present.

import { Logger, stdSerializers } from 'node:logger';

const logger = new Logger({
  serializers: {
    err: stdSerializers.err,
  },
});
const { Logger, stdSerializers } = require('node:logger');

const logger = new Logger({
  serializers: {
    err: stdSerializers.err,
  },
});

stdSerializers.req(request)

  • request {http.IncomingMessage} HTTP request object.
  • Returns: {Object} Serialized request object with method, url, headers, remoteAddress, and remotePort.

Serializes an HTTP request object for logging.

const http = require('node:http');
const { Logger, JSONConsumer, stdSerializers } = require('node:logger');

const logger = new Logger({
  serializers: {
    req: stdSerializers.req,
  },
});
const consumer = new JSONConsumer();
consumer.attach();

http.createServer((req, res) => {
  logger.info('Request received', { req });
  res.end('OK');
}).listen(3000);

stdSerializers.res(response)

  • response {http.ServerResponse} HTTP response object.
  • Returns: {Object} Serialized response object with statusCode and headers.

Serializes an HTTP response object for logging.

logger.info('Response sent', { res });

logger.serialize

  • {symbol}

A symbol that objects can implement to define custom serialization behavior for logging. Similar to util.inspect.custom.

When an object with a [serialize]() method is logged, the logger will call that method instead of serializing the object directly. This allows objects to control which properties are included in logs, filtering out sensitive data like passwords or tokens.

import { Logger, JSONConsumer, serialize } from 'node:logger';

class User {
  constructor(id, name, password) {
    this.id = id;
    this.name = name;
    this.password = password; // Sensitive!
  }

  // Define custom serialization
  [serialize]() {
    return {
      id: this.id,
      name: this.name,
      // password is excluded
    };
  }
}

const consumer = new JSONConsumer();
consumer.attach();

const logger = new Logger();
const user = new User(1, 'Alice', 'secret123');

logger.info({ msg: 'User logged in', user });
// Output: {"level":"info","time":...,"msg":"User logged in","user":{"id":1,"name":"Alice"}}
// Note: password is not included in the output
const { Logger, JSONConsumer, serialize } = require('node:logger');

class DatabaseConnection {
  constructor(host, user, password) {
    this.host = host;
    this.user = user;
    this.password = password;
  }

  [serialize]() {
    return {
      host: this.host,
      user: this.user,
      connected: this.isConnected,
      // password is excluded
    };
  }
}

The serialize symbol takes precedence over field-specific serializers. If an object has both a [serialize]() method and a matching serializer in the logger's serializers option, the [serialize]() method will be used.

Examples

Basic usage

import { Logger, JSONConsumer } from 'node:logger';

// Create a logger
const logger = new Logger({ level: 'info' });

// Create and attach a consumer
const consumer = new JSONConsumer({ level: 'info' });
consumer.attach();

// Log messages
logger.info('Application started');
logger.info('User logged in', { userId: 123 });
logger.error(new Error('Something went wrong'));

Child loggers for request tracing

import { Logger, JSONConsumer } from 'node:logger';
import { randomUUID } from 'node:crypto';

const logger = new Logger({
  bindings: { service: 'api-server' },
});

const consumer = new JSONConsumer();
consumer.attach();

function handleRequest(req, res) {
  const requestLogger = logger.child({
    requestId: randomUUID(),
    method: req.method,
    path: req.url,
  });

  requestLogger.info('Request started');
  // ... handle request ...
  requestLogger.info('Request completed', { statusCode: res.statusCode });
}

Multiple consumers

import { Logger, JSONConsumer } from 'node:logger';

const logger = new Logger({ level: 'trace' });

// Console output for development (info and above)
const consoleConsumer = new JSONConsumer({ level: 'info' });
consoleConsumer.attach();

// File output for debugging (all levels)
const fileConsumer = new JSONConsumer({
  level: 'trace',
  stream: '/var/log/app-debug.log',
});
fileConsumer.attach();

// Error file (errors only)
const errorConsumer = new JSONConsumer({
  level: 'error',
  stream: '/var/log/app-error.log',
});
errorConsumer.attach();

Custom serializers

import { Logger, JSONConsumer, stdSerializers } from 'node:logger';

const logger = new Logger({
  serializers: {
    err: stdSerializers.err,
    req: stdSerializers.req,
    res: stdSerializers.res,
    user: (user) => ({ id: user.id, email: user.email }), // Custom serializer
  },
});

const consumer = new JSONConsumer();
consumer.attach();

logger.info('User action', {
  user: { id: 1, email: 'user@example.com', password: 'secret' },
});
// Output will not include password due to custom serializer

Custom consumer

import { LogConsumer } from 'node:logger';
import { styleText } from 'node:util';

const levelStyles = {
  trace: 'gray',
  debug: 'cyan',
  info: 'green',
  warn: 'yellow',
  error: 'red',
  fatal: 'magenta',
};

class ConsoleColorConsumer extends LogConsumer {
  handle(record) {
    const style = levelStyles[record.level] ?? 'white';
    const label = styleText(style, `[${record.level.toUpperCase()}]`);
    console.log(`${label} ${record.msg}`);
  }
}

const consumer = new ConsoleColorConsumer({ level: 'debug' });
consumer.attach();