docs: add user guide, cookbook, and rewrite README

This commit is contained in:
Tiara Rodney 2026-03-14 04:33:53 +01:00
parent 741b959820
commit 1a74f2afa4
No known key found for this signature in database
GPG key ID: 5CD8EC1D46106723
3 changed files with 621 additions and 54 deletions

View file

@ -1 +1,333 @@
The doc/README.md
# 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](#basic-usage)
- [Loggers](#loggers)
- [Handlers](#handlers)
- [Formatters](#formatters)
- [Filters](#filters)
- [Configuration](#configuration)
- [Log record attributes](#log-record-attributes)
## Basic usage
The simplest way to start logging is with `basicConfig`:
```javascript
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
```javascript
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
```javascript
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:
```javascript
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
```javascript
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`).
```javascript
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`
```javascript
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.
```javascript
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
```javascript
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)`:
```javascript
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
```javascript
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](#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:
```javascript
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:
```javascript
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:
```javascript
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:
```javascript
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` |

View file

@ -1,37 +1,216 @@
# Using logging in multiple modules
# Logging cookbook
Multiple calls to `logging.getLogger('someLogger')` return a reference to the
same logger object. This is true not only within the same module, but also
across modules as long as it is in the same Python interpreter process. It is
true for references to the same object; additionally, application code can
define and configure a parent logger in one module and create (but not
configure) a child logger in a separate module, and all logger calls to the
child will pass up to the parent. Here is a main module:
Recipes and patterns for common logging tasks with `@administratrix/esm-logging`.
``javascript
import * as logging from 'eslib/logging';
import * as my_module from './my_module';
## Using logging across multiple modules
// create logger with 'spam_application'
var logger = logging.getLogger('spam_application');
logger.setLevel(logging.DEBUG);
`getLogger` returns the same logger instance for a given scope name across
your entire application. Configure handlers on a parent logger and create
child loggers in each module:
// create file handler which logs even debug messages
var fh = logging.FileHandler('spam.log')
fh.setLevel(logging.DEBUG);
```javascript
// main.js
import * as logging from '@administratrix/esm-logging';
import { ConsoleHandler } from '@administratrix/esm-logging/src/handler';
import { Formatter } from '@administratrix/esm-logging/src/formatter';
import { doWork } from './worker.js';
// create console handler with a higher log level
var ch = logging.StreamHandler();
ch.setLevel(logging.ERROR);
const logger = logging.manager.MANAGER.getLogger('myapp');
logger.setLevel(logging.log_level.DEBUG);
// create formatter and add it to the handlers
var formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s');
fh.setFormatter(formatter);
ch.setFormatter(formatter);
const handler = new ConsoleHandler();
handler.level = logging.log_level.DEBUG;
handler.formatter = new Formatter({
fmt: '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
});
logger.addHandler(handler);
// add the handlers to the logger
logger.addHandler(fh);
logger.addHandler(ch);
logger.info('creating an instance of auxiliary_module.Auxiliary')
logger.info('Application starting');
doWork();
logger.info('Application finished');
```
```javascript
// worker.js
import * as logging from '@administratrix/esm-logging';
const logger = logging.manager.MANAGER.getLogger('myapp.worker');
export function doWork() {
logger.debug('Starting work');
// ... do something ...
logger.info('Work completed');
}
```
Because `myapp.worker` is a child of `myapp`, its records propagate up to the
handler configured on `myapp`. No handler configuration is needed in
`worker.js`.
Output:
```
2026-03-14 10:00:00.000 - myapp - INFO - Application starting
2026-03-14 10:00:00.001 - myapp.worker - DEBUG - Starting work
2026-03-14 10:00:00.002 - myapp.worker - INFO - Work completed
2026-03-14 10:00:00.003 - myapp - INFO - Application finished
```
## Logging to multiple destinations
Attach multiple handlers to a single logger, each with its own level and
formatter:
```javascript
import * as logging from '@administratrix/esm-logging';
import { ConsoleHandler, StderrHandler } from '@administratrix/esm-logging/src/handler';
import { Formatter } from '@administratrix/esm-logging/src/formatter';
const logger = logging.manager.MANAGER.getLogger('myapp');
logger.setLevel(logging.log_level.DEBUG);
// console handler: shows everything with detailed format
const consoleHandler = new ConsoleHandler();
consoleHandler.level = logging.log_level.DEBUG;
consoleHandler.formatter = new Formatter({
fmt: '%(asctime)s [%(levelname)s] %(name)s: %(message)s',
});
logger.addHandler(consoleHandler);
// stderr handler: only errors, compact format
const errorHandler = new StderrHandler(logging.log_level.ERROR);
errorHandler.formatter = new Formatter({
fmt: 'ERROR %(asctime)s %(name)s: %(message)s',
datefmt: '%Y-%m-%d %H:%M:%S',
});
logger.addHandler(errorHandler);
```
## Using basicConfig for simple scripts
For quick scripts, `basicConfig` sets up a handler on the root logger:
```javascript
import * as logging from '@administratrix/esm-logging';
logging.config.basicConfig({
level: logging.log_level.INFO,
format: '%(levelname)s: %(message)s',
});
const logger = logging.manager.MANAGER.getLogger('script');
logger.info('Running');
logger.warning('Check this');
```
Output:
```
INFO: Running
WARNING: Check this
```
## Filtering records
### By scope
Only allow records from a specific part of the hierarchy:
```javascript
import { Filter } from '@administratrix/esm-logging/src/filter';
const dbFilter = new Filter('myapp.db');
handler.addFilter(dbFilter);
// handler now only processes records from 'myapp.db' and its children
```
### By custom criteria
Use any object with a `filter(record)` method:
```javascript
handler.addFilter({
filter(record) {
// only pass records that contain 'important' in the message
return record.msg.includes('important');
}
});
```
## Custom formatters
Create formatters with different format strings for different contexts:
```javascript
import { Formatter } from '@administratrix/esm-logging/src/formatter';
// detailed format for development
const devFormatter = new Formatter({
fmt: '%(asctime)s %(levelname)s %(name)s: %(message)s',
datefmt: '%H:%M:%S',
});
// compact format for production
const prodFormatter = new Formatter({
fmt: '%(levelname)s:%(name)s:%(message)s',
});
```
## Custom handlers
Subclass `Handler` and implement `emit(record)`:
```javascript
import { Handler } from '@administratrix/esm-logging/src/handler';
class BufferHandler extends Handler {
constructor(level) {
super(level);
this.buffer = [];
}
emit(record) {
try {
this.buffer.push(this.format(record));
if (this.buffer.length >= 100) {
this.flush();
}
} catch (e) {
this.handleError(record);
}
}
flush() {
// send buffered records somewhere
const batch = this.buffer.splice(0);
// ... process batch ...
}
}
```
## Disabling logging below a threshold
The manager's `disable` property suppresses all logging at or below a given
level across all loggers:
```javascript
import * as logging from '@administratrix/esm-logging';
// suppress DEBUG and INFO globally
logging.manager.MANAGER.disable = logging.log_level.INFO;
```
## Reconfiguring with force
`basicConfig` only takes effect once. To reconfigure, use `force: true`:
```javascript
logging.config.basicConfig({
level: logging.log_level.DEBUG,
format: '%(asctime)s %(message)s',
force: true,
});
```
This removes and closes all existing handlers on the root logger before
applying the new configuration.