Merge branch 'feature/10' into dev

This commit is contained in:
Tiara Rodney 2026-03-14 04:34:13 +01:00
commit 768ff9263d
No known key found for this signature in database
GPG key ID: 5CD8EC1D46106723
4 changed files with 622 additions and 55 deletions

104
README.md
View file

@ -1,33 +1,89 @@
# esm-logging # esm-logging
> This README is a stub. Working on it. Currently stabilizing the build A quasi-port of the Python standard library
environment after that I'll make it nice around here. [logging](https://docs.python.org/3/library/logging.html) module to
ECMAScript. Browser-compatible, zero dependencies.
A quasi-port of the Python standard library logging module to ECMAScript. ## Why?
# Why? Logging is important. It is important for debugging purposes, leading to
faster and more resilient development, for traceability leading to better
First of, because logging is important. It is important for debugging purposes, security. Most logging libraries I've discovered didn't satisfy me, introduced
leading to faster and more resilient development, for traceability leading to weird concepts and all in all just weren't great. Other programming language
better security. Most logging libraries I've discovered didn't satisfy me, ecosystems offer way nicer logging facilities. Take Rust for example, or...
introduced weird concepts and all in all just weren't great. Other programming Python! Python has PEP, giving it a very structured approach towards
language ecosystems offer way nicer logging facilities. Take Rust for example,
or... Python! Python has PEP, giving it a very structured approach towards
implementing new features and that's also how its logging facilities came to be implementing new features and that's also how its logging facilities came to be
([PEP 282](https://peps.python.org/pep-0282/)). Python's logging facilities are ([PEP 282](https://peps.python.org/pep-0282/)). Python's logging facilities
implemented by the [logging]() module, which is part of the standard library and are implemented by the [logging](https://docs.python.org/3/library/logging.html)
has been since 2002. It was originally authored by Vinay Sajip module, which is part of the standard library and has been since 2002. It was
originally authored by Vinay Sajip.
# Roadmap ## Installation
- do a quasi-port of the logging module with minimal amount of adaption ```bash
- add documentation npm install @administratrix/esm-logging
- add support for asynchronous calls ```
- implement Open Cybersecurity Framework (OCSF) formatter
- implement (Browser) local storage handler as a replacement for file handler
# Usage ## Quick start
For the time being, please check out my [CI ```javascript
service](https://bitbucket.org/byteb4rb1e/esm-logging/pipelines), for an idea on import * as logging from '@administratrix/esm-logging';
how to build this.
// one-shot configuration: sets up a console handler on the root logger
logging.config.basicConfig({ level: logging.log_level.INFO });
// create a logger for this module
const logger = logging.manager.MANAGER.getLogger('myapp');
logger.info('Application started');
logger.warning('Something looks off');
logger.error('Something went wrong');
```
## Concepts
The logging system is built around four core components:
- **Loggers** expose the interface that application code uses directly.
- **Handlers** send log records to the appropriate destination (console,
stderr, custom writable streams).
- **Formatters** control the layout of log records in the final output.
- **Filters** provide fine-grained control over which records to output.
Loggers are organized in a dot-separated hierarchy. A logger named `app.db`
is a child of the logger named `app`. Log records propagate up the hierarchy,
so a handler attached to `app` will also receive records from `app.db`.
## Log levels
| Constant | Value | Purpose |
|------------|-------|------------------------------------------|
| `CRITICAL` | 50 | A serious error, the program may not continue |
| `ERROR` | 40 | An error that prevented some operation |
| `WARNING` | 30 | Something unexpected, but the software still works |
| `INFO` | 20 | Confirmation that things work as expected |
| `DEBUG` | 10 | Detailed diagnostic information |
| `NOTSET` | 0 | All messages are processed |
## Documentation
See [docs/](docs/README.md) for the full user guide and
[docs/logging-cookbook.md](docs/logging-cookbook.md) for recipes and patterns.
API reference can be generated with TypeDoc:
```bash
npm run build/doc
```
## Roadmap
- [x] quasi-port of the logging module with minimal adaptation
- [x] add documentation
- [ ] add support for asynchronous calls
- [ ] implement Open Cybersecurity Schema Framework (OCSF) formatter
- [ ] implement browser local storage handler as a replacement for file handler
## License
UNLICENSED

2
TODO
View file

@ -156,7 +156,7 @@ Content-Type: application/issue
ID: 10 ID: 10
Type: feature Type: feature
Title: add documentation Title: add documentation
Status: open Status: done
Priority: medium Priority: medium
Created: 2026-03-13 Created: 2026-03-13
Relationships: Relationships:

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 Recipes and patterns for common logging tasks with `@administratrix/esm-logging`.
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:
``javascript ## Using logging across multiple modules
import * as logging from 'eslib/logging';
import * as my_module from './my_module';
// create logger with 'spam_application' `getLogger` returns the same logger instance for a given scope name across
var logger = logging.getLogger('spam_application'); your entire application. Configure handlers on a parent logger and create
logger.setLevel(logging.DEBUG); child loggers in each module:
// create file handler which logs even debug messages ```javascript
var fh = logging.FileHandler('spam.log') // main.js
fh.setLevel(logging.DEBUG); 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 const logger = logging.manager.MANAGER.getLogger('myapp');
var ch = logging.StreamHandler(); logger.setLevel(logging.log_level.DEBUG);
ch.setLevel(logging.ERROR);
// create formatter and add it to the handlers const handler = new ConsoleHandler();
var formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'); handler.level = logging.log_level.DEBUG;
fh.setFormatter(formatter); handler.formatter = new Formatter({
ch.setFormatter(formatter); fmt: '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
});
logger.addHandler(handler);
// add the handlers to the logger logger.info('Application starting');
logger.addHandler(fh); doWork();
logger.addHandler(ch); logger.info('Application finished');
logger.info('creating an instance of auxiliary_module.Auxiliary')
``` ```
```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.