esm-logging/docs/README.md

9.7 KiB

esm-logging user guide

This guide covers the @administratrix/esm-logging library, a quasi-port of Python's logging module for ECMAScript. It targets browser environments and provides hierarchical, configurable logging with formatters, handlers, and filters.

Table of contents

Basic usage

The simplest way to start logging is with basicConfig:

import * as logging from '@administratrix/esm-logging';

logging.config.basicConfig({
    level: logging.log_level.DEBUG,
    format: '%(levelname)s:%(name)s:%(message)s',
});

const logger = logging.manager.MANAGER.getLogger('myapp');
logger.info('Hello, world');
// output: INFO:myapp:Hello, world

basicConfig creates a StreamHandler writing to stderr (via console.error) and attaches it to the root logger. It is a one-shot function: calling it again has no effect unless you pass force: true.

Loggers

Loggers are the entry point for emitting log records. They are organized in a dot-separated hierarchy managed by a singleton Manager.

Creating loggers

const logger = logging.manager.MANAGER.getLogger('myapp');
const childLogger = logging.manager.MANAGER.getLogger('myapp.db');

Calling getLogger with the same scope always returns the same logger instance. The hierarchy is established automatically: myapp.db is a child of myapp.

Setting levels

logger.setLevel(logging.log_level.WARNING);

A logger only processes messages at or above its effective level. The effective level is determined by walking up the parent chain until a logger with a non-zero level is found. The root logger defaults to WARNING.

Logging methods

Each level has a corresponding method:

logger.debug('Detailed diagnostic info');
logger.info('Things are working');
logger.warning('Something unexpected');
logger.error('An operation failed');
logger.critical('System is in trouble');

Propagation

By default, log records propagate up the hierarchy. A record emitted by myapp.db will be handled by handlers on myapp.db, then myapp, then the root logger. This means you typically only need to configure handlers on the root logger or on high-level loggers.

Checking if a level is enabled

if (logger.isEnabledFor(logging.log_level.DEBUG)) {
    logger.debug('Expensive computation: ' + computeDebugInfo());
}

This avoids the cost of building the message string when the level would be filtered out anyway.

Handlers

Handlers determine where log records go. A logger can have multiple handlers, and each handler can have its own level and formatter.

Available handlers

StreamHandler

Writes formatted output to a Writable stream. Defaults to stderr (via console.error).

import { StreamHandler } from '@administratrix/esm-logging/src/handler';
import { ConsoleWritable } from '@administratrix/esm-logging/src/helper/stream';

const handler = new StreamHandler(new ConsoleWritable());
logger.addHandler(handler);

ConsoleHandler

Maps log levels to the appropriate browser console method:

  • ERROR and CRITICAL use console.error
  • WARNING uses console.warn
  • DEBUG and INFO use console.log
import { ConsoleHandler } from '@administratrix/esm-logging/src/handler';

const handler = new ConsoleHandler();
handler.level = logging.log_level.DEBUG;
logger.addHandler(handler);

StderrHandler

Always writes to console.error, regardless of level. Useful when you want all output on stderr.

import { StderrHandler } from '@administratrix/esm-logging/src/handler';

const handler = new StderrHandler(logging.log_level.WARNING);
logger.addHandler(handler);

FileHandler

Not available in browser environments. Throws NotImplementedError on construction. Use ConsoleHandler or a storage-backed handler instead.

Handler methods

handler.level = logging.log_level.INFO;  // only handle INFO and above
handler.formatter = myFormatter;         // set a custom formatter
handler.close();                         // release resources

Custom handlers

Subclass Handler and implement emit(record):

import { Handler } from '@administratrix/esm-logging/src/handler';

class ArrayHandler extends Handler {
    constructor() {
        super();
        this.records = [];
    }

    emit(record) {
        this.records.push(this.format(record));
    }
}

Formatters

Formatters control how log records are rendered as strings. The default format is %(message)s (just the message). The basic format used by basicConfig is %(levelname)s:%(name)s:%(message)s.

Creating a formatter

import { Formatter } from '@administratrix/esm-logging/src/formatter';

const formatter = new Formatter({
    fmt: '%(asctime)s - %(levelname)s - %(name)s - %(message)s',
    datefmt: '%Y-%m-%d %H:%M:%S',
});

handler.formatter = formatter;

Format string placeholders

Formatters use %-style substitution. Available placeholders correspond to log record attributes:

%(name)s        Logger scope name
%(levelno)d     Numeric log level
%(levelname)s   Text log level (DEBUG, INFO, etc.)
%(message)s     The formatted message
%(asctime)s     Human-readable timestamp
%(created)f     Milliseconds since epoch (Date.now())

Date formatting

The datefmt option accepts strftime-style tokens:

Token Meaning Example
%Y Four-digit year 2026
%m Zero-padded month 03
%d Zero-padded day 14
%H Hour (24h) 09
%M Minute 05
%S Second 30

If datefmt is omitted, an ISO 8601-like format is used: 2026-03-14 09:05:30.123.

Filters

Filters provide fine-grained control over which records get processed. They can be attached to loggers or handlers.

Scope-based filtering

A Filter initialized with a scope name only allows records from that scope and its children:

import { Filter } from '@administratrix/esm-logging/src/filter';

const filter = new Filter('myapp.db');
handler.addFilter(filter);
// only records from 'myapp.db' and 'myapp.db.*' will pass

A filter initialized with an empty string allows all records.

Custom filters

Any object with a filter(record) method can be used:

handler.addFilter({
    filter(record) {
        return record.levelno >= logging.log_level.WARNING;
    }
});

Configuration

basicConfig

basicConfig is a convenience function for simple, one-shot configuration of the root logger:

logging.config.basicConfig({
    level: logging.log_level.DEBUG,
    format: '%(asctime)s %(levelname)s %(message)s',
    datefmt: '%Y-%m-%d %H:%M:%S',
});

Options

Option Type Description
level number Root logger level
format string Format string for the handler's formatter
datefmt string Date format string
style string Format style ('%' only, currently)
handlers Handler[] Pre-built handlers to attach
stream Writable Stream for the default StreamHandler
force boolean Remove existing handlers first

stream and handlers are mutually exclusive.

Manual configuration

For more control, configure loggers and handlers directly:

const root = logging.manager.MANAGER.root;
const handler = new ConsoleHandler();
handler.level = logging.log_level.DEBUG;
handler.formatter = new Formatter({
    fmt: '%(asctime)s [%(levelname)s] %(name)s: %(message)s',
});
root.addHandler(handler);
root.setLevel(logging.log_level.DEBUG);

Log record attributes

A LogRecord carries the following attributes:

Attribute Type Description
scope string Logger name that created the record
name string Same as scope
levelno number Numeric level (10, 20, 30, 40, 50)
levelname string Text level (DEBUG, INFO, etc.)
msg string The raw message template
args any[] Substitution arguments for %s in msg
message string The formatted message (set by formatter)
created number Milliseconds since Unix epoch
asctime string Formatted timestamp (set by formatter)

The getMessage() method on LogRecord performs %s argument substitution on msg using args.

Module structure

The library is organized into submodules, all re-exported from the main entry point:

Import path Contents
config basicConfig()
filter Filter, Filterer
formatter Formatter, STYLES, DEFAULT_FORMATTER
handler Handler, StreamHandler, ConsoleHandler, StderrHandler, FileHandler
log_level Level constants, getLevelName(), checkLevel()
log_record LogRecord, factory functions
logger Logger, RootLogger, ROOT
manager Manager, MANAGER