Compare commits
No commits in common. "dev" and "serious" have entirely different histories.
29 changed files with 1444 additions and 4883 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
[submodule "vendor/tiara-gitflow-spec"]
|
|
||||||
path = vendor/tiara-gitflow-spec
|
|
||||||
url = git@bitbucket.org:byteb4rb1e/tiara-gitflow-spec.git
|
|
||||||
87
README.md
87
README.md
|
|
@ -1,89 +1,4 @@
|
||||||
# esm-logging
|
|
||||||
|
|
||||||
A quasi-port of the Python standard library
|
|
||||||
[logging](https://docs.python.org/3/library/logging.html) module to
|
|
||||||
ECMAScript. Browser-compatible, zero dependencies.
|
|
||||||
|
|
||||||
## Why?
|
* [Logging Cookbook](doc/logging-cookbook.md)
|
||||||
|
|
||||||
Logging is important. It is important for debugging purposes, leading to
|
|
||||||
faster and more resilient development, for traceability leading to better
|
|
||||||
security. Most logging libraries I've discovered didn't satisfy me, introduced
|
|
||||||
weird concepts and all in all just weren't great. Other programming 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
|
|
||||||
([PEP 282](https://peps.python.org/pep-0282/)). Python's logging facilities
|
|
||||||
are implemented by the [logging](https://docs.python.org/3/library/logging.html)
|
|
||||||
module, which is part of the standard library and has been since 2002. It was
|
|
||||||
originally authored by Vinay Sajip.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @administratrix/esm-logging
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import * as logging from '@administratrix/esm-logging';
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
|
||||||
214
TODO
214
TODO
|
|
@ -1,214 +0,0 @@
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 1
|
|
||||||
Type: feature
|
|
||||||
Title: string formatting utilities
|
|
||||||
Status: in-progress
|
|
||||||
Priority: high
|
|
||||||
Created: 2025-05-01
|
|
||||||
Relationships:
|
|
||||||
Description: implement utilities for formatting strings. The formatting should
|
|
||||||
be inspired by Python 3K PEP 3101 in addition to their standard
|
|
||||||
library utilities starting from ver. 3.7. Optimizations should
|
|
||||||
focus on V8 support.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 2
|
|
||||||
Type: feature
|
|
||||||
Title: describe development workflow in CONTRIBUTING.md
|
|
||||||
Status: open
|
|
||||||
Priority: medium
|
|
||||||
Created: 2025-05-01
|
|
||||||
Relationships:
|
|
||||||
Description: It's a good idea to describe the development workflow, including
|
|
||||||
branching strategies earlier on, so that if someone is interested
|
|
||||||
in forking, they can pick up right away. It's not meant for
|
|
||||||
contributions though. I'm currently not interested in external
|
|
||||||
contributions.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 3
|
|
||||||
Type: bugfix
|
|
||||||
Title: modularize testing further
|
|
||||||
Status: done
|
|
||||||
Priority: high
|
|
||||||
Created: 2025-05-01
|
|
||||||
Relationships:
|
|
||||||
Description: Since I am going to implement unit tests as well as integration
|
|
||||||
tests and probably some benchmarks, it makes sense to introduce
|
|
||||||
another sub-level directory for each type of test, say
|
|
||||||
tests/unit/, tests/integration, etc.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 4
|
|
||||||
Type: feature
|
|
||||||
Title: migrate testing framework from Jest to Mocha
|
|
||||||
Status: in-progress
|
|
||||||
Priority: high
|
|
||||||
Created: 2025-05-01
|
|
||||||
Relationships:
|
|
||||||
Description: I really don't like behavior-driven testing, at least when it comes
|
|
||||||
to unit testing. It feels like Walldorf education... Where I need
|
|
||||||
to come up with an abstraction for describing my test. A function
|
|
||||||
has an input and gives an output. That's what I want to test. I'm
|
|
||||||
not trying to find the philosophical meaning of my functions...
|
|
||||||
Hopefully Mocha is the savior. I'm sticking to xUnit based testing
|
|
||||||
from now on.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 5
|
|
||||||
Type: bugfix
|
|
||||||
Title: fix critical bugs across core modules
|
|
||||||
Status: done
|
|
||||||
Priority: high
|
|
||||||
Created: 2026-03-13
|
|
||||||
Relationships:
|
|
||||||
Description: multiple logic bugs prevent the library from functioning.
|
|
||||||
manager.ts getLogger() has an inverted type check and missing
|
|
||||||
return statement. config.ts basicConfig() assigns wrong option
|
|
||||||
fields (filemode instead of datefmt/style) and has unreachable
|
|
||||||
conditions. logger.ts isEnabledFor() always caches false,
|
|
||||||
_log() never calls handle(), and makeRecord() uses .keys()
|
|
||||||
instead of Object.keys(). handler.ts format() has no return
|
|
||||||
statement and is missing a formatter getter.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 6
|
|
||||||
Type: feature
|
|
||||||
Title: implement remaining Logger level methods
|
|
||||||
Status: done
|
|
||||||
Priority: high
|
|
||||||
Created: 2026-03-13
|
|
||||||
Relationships: dependsOn:5
|
|
||||||
Description: Logger only implements debug(). The info(), warning(), error(),
|
|
||||||
and critical() methods are missing entirely. The _log() method
|
|
||||||
also needs to be completed so it actually calls handle() after
|
|
||||||
creating the LogRecord.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 7
|
|
||||||
Type: feature
|
|
||||||
Title: implement Formatter subsystem
|
|
||||||
Status: done
|
|
||||||
Priority: high
|
|
||||||
Created: 2026-03-13
|
|
||||||
Relationships: dependsOn:1
|
|
||||||
Description: the three core Formatter methods are stubs returning placeholder
|
|
||||||
strings. PercentFormatterStyle._format() needs to perform actual
|
|
||||||
%-style field substitution against LogRecord attributes.
|
|
||||||
Formatter.formatTime() needs a JS Date/Intl-based implementation
|
|
||||||
replacing the Python time.strftime references.
|
|
||||||
Formatter.formatError() needs to format Error.stack into a
|
|
||||||
string. The datetime helper module is empty and needs time
|
|
||||||
formatting utilities.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 8
|
|
||||||
Type: feature
|
|
||||||
Title: implement browser-compatible Handler output
|
|
||||||
Status: done
|
|
||||||
Priority: high
|
|
||||||
Created: 2026-03-13
|
|
||||||
Relationships: dependsOn:5
|
|
||||||
Description: StreamHandler has no emit() implementation and its constructor
|
|
||||||
ignores the stream parameter. The helper/stream module is a
|
|
||||||
stub that needs a browser-compatible Writable interface backed
|
|
||||||
by console output. FileHandler should be dropped or gated behind
|
|
||||||
a Node.js environment check since browser environments have no
|
|
||||||
filesystem write access. A ConsoleHandler targeting the browser
|
|
||||||
console API (console.log/warn/error by level) is the primary
|
|
||||||
output target.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 9
|
|
||||||
Type: feature
|
|
||||||
Title: implement Manager logger hierarchy
|
|
||||||
Status: done
|
|
||||||
Priority: high
|
|
||||||
Created: 2026-03-13
|
|
||||||
Relationships: dependsOn:5
|
|
||||||
Description: Manager.getLogger() does not establish parent-child
|
|
||||||
relationships between loggers based on dot-separated names.
|
|
||||||
The Placeholder fixup logic is incomplete. Logger propagation
|
|
||||||
depends on this hierarchy being correctly built so that child
|
|
||||||
loggers forward records to parent handlers.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 10
|
|
||||||
Type: feature
|
|
||||||
Title: add documentation
|
|
||||||
Status: done
|
|
||||||
Priority: medium
|
|
||||||
Created: 2026-03-13
|
|
||||||
Relationships:
|
|
||||||
Description: write user-facing documentation covering module usage, API
|
|
||||||
reference, and configuration. The docs/ directory has stubs
|
|
||||||
(README.md, logging-cookbook.md) that need to be fleshed out.
|
|
||||||
TypeDoc generates API docs from source but a prose guide with
|
|
||||||
examples is needed for onboarding.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 11
|
|
||||||
Type: feature
|
|
||||||
Title: add support for asynchronous calls
|
|
||||||
Status: open
|
|
||||||
Priority: low
|
|
||||||
Created: 2026-03-13
|
|
||||||
Relationships:
|
|
||||||
Description: introduce async-aware logging so that handlers which perform
|
|
||||||
I/O (e.g. network, storage) do not block the caller. This may
|
|
||||||
involve an AsyncHandler base class or async emit() overloads,
|
|
||||||
and consideration of how log record ordering is preserved
|
|
||||||
across concurrent async contexts.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 12
|
|
||||||
Type: feature
|
|
||||||
Title: implement OCSF formatter
|
|
||||||
Status: open
|
|
||||||
Priority: low
|
|
||||||
Created: 2026-03-13
|
|
||||||
Relationships:
|
|
||||||
Description: implement a Formatter subclass that outputs log records in
|
|
||||||
Open Cybersecurity Schema Framework (OCSF) format. This
|
|
||||||
enables structured security event logging compatible with
|
|
||||||
SIEM systems and security analytics tooling.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
|
|
||||||
ID: 13
|
|
||||||
Type: feature
|
|
||||||
Title: implement browser local storage handler
|
|
||||||
Status: done
|
|
||||||
Priority: medium
|
|
||||||
Created: 2026-03-13
|
|
||||||
Relationships: dependsOn:8
|
|
||||||
Description: implement a Handler subclass that persists log records to
|
|
||||||
browser localStorage or IndexedDB as a replacement for
|
|
||||||
FileHandler in browser environments. Should support log
|
|
||||||
rotation by size or entry count to avoid exceeding storage
|
|
||||||
quotas.
|
|
||||||
|
|
@ -26,7 +26,6 @@ definitions:
|
||||||
- build/debug/**/*
|
- build/debug/**/*
|
||||||
- build/debug/*
|
- build/debug/*
|
||||||
script:
|
script:
|
||||||
- make clean
|
|
||||||
- make build/debug CI=1
|
- make build/debug CI=1
|
||||||
- step: &build-release
|
- step: &build-release
|
||||||
name: Build (Release)
|
name: Build (Release)
|
||||||
|
|
@ -36,7 +35,6 @@ definitions:
|
||||||
- build/release/**/*
|
- build/release/**/*
|
||||||
- build/release/*
|
- build/release/*
|
||||||
script:
|
script:
|
||||||
- make clean
|
|
||||||
- make build/release CI=1
|
- make build/release CI=1
|
||||||
- step: &build-doc
|
- step: &build-doc
|
||||||
name: Build (Doc)
|
name: Build (Doc)
|
||||||
|
|
@ -46,7 +44,6 @@ definitions:
|
||||||
- build/doc/**/*
|
- build/doc/**/*
|
||||||
- build/doc/*
|
- build/doc/*
|
||||||
script:
|
script:
|
||||||
- make clean
|
|
||||||
- make build/doc CI=1
|
- make build/doc CI=1
|
||||||
- step: &dist
|
- step: &dist
|
||||||
name: Package
|
name: Package
|
||||||
|
|
@ -55,7 +52,6 @@ definitions:
|
||||||
artifacts:
|
artifacts:
|
||||||
- dist/*
|
- dist/*
|
||||||
script:
|
script:
|
||||||
- rm -rvf test-reports/
|
|
||||||
- make dist CI=1
|
- make dist CI=1
|
||||||
pipelines:
|
pipelines:
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
334
docs/README.md
334
docs/README.md
|
|
@ -1,333 +1 @@
|
||||||
# esm-logging user guide
|
The doc/README.md
|
||||||
|
|
||||||
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` |
|
|
||||||
|
|
@ -1,216 +1,37 @@
|
||||||
# Logging cookbook
|
# Using logging in multiple modules
|
||||||
|
|
||||||
Recipes and patterns for common logging tasks with `@administratrix/esm-logging`.
|
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:
|
||||||
|
|
||||||
## Using logging across multiple modules
|
``javascript
|
||||||
|
import * as logging from 'eslib/logging';
|
||||||
|
import * as my_module from './my_module';
|
||||||
|
|
||||||
`getLogger` returns the same logger instance for a given scope name across
|
// create logger with 'spam_application'
|
||||||
your entire application. Configure handlers on a parent logger and create
|
var logger = logging.getLogger('spam_application');
|
||||||
child loggers in each module:
|
logger.setLevel(logging.DEBUG);
|
||||||
|
|
||||||
```javascript
|
// create file handler which logs even debug messages
|
||||||
// main.js
|
var fh = logging.FileHandler('spam.log')
|
||||||
import * as logging from '@administratrix/esm-logging';
|
fh.setLevel(logging.DEBUG);
|
||||||
import { ConsoleHandler } from '@administratrix/esm-logging/src/handler';
|
|
||||||
import { Formatter } from '@administratrix/esm-logging/src/formatter';
|
|
||||||
import { doWork } from './worker.js';
|
|
||||||
|
|
||||||
const logger = logging.manager.MANAGER.getLogger('myapp');
|
// create console handler with a higher log level
|
||||||
logger.setLevel(logging.log_level.DEBUG);
|
var ch = logging.StreamHandler();
|
||||||
|
ch.setLevel(logging.ERROR);
|
||||||
|
|
||||||
const handler = new ConsoleHandler();
|
// create formatter and add it to the handlers
|
||||||
handler.level = logging.log_level.DEBUG;
|
var formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s');
|
||||||
handler.formatter = new Formatter({
|
fh.setFormatter(formatter);
|
||||||
fmt: '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
ch.setFormatter(formatter);
|
||||||
});
|
|
||||||
logger.addHandler(handler);
|
|
||||||
|
|
||||||
logger.info('Application starting');
|
// add the handlers to the logger
|
||||||
doWork();
|
logger.addHandler(fh);
|
||||||
logger.info('Application finished');
|
logger.addHandler(ch);
|
||||||
|
|
||||||
|
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.
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import eslint from "@eslint/js";
|
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
|
|
||||||
export default tseslint.config(
|
|
||||||
eslint.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
);
|
|
||||||
|
|
@ -19,5 +19,5 @@ export default {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
roots: ['./tests/unit']
|
roots: ['./tests']
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1250
package-lock.json
generated
1250
package-lock.json
generated
File diff suppressed because it is too large
Load diff
24
package.json
24
package.json
|
|
@ -1,17 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "@administratrix/esm-logging",
|
"name": "administratrix/esm-logging",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "port of Python standard library logging module",
|
"description": "port of Python standard library logging module",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"engines": {
|
|
||||||
"node": ">= 20.11.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build/debug": "tsc -p tsconfig.debug.json",
|
"test": "jest",
|
||||||
"build/release": "tsc",
|
"build": "npm run build:release",
|
||||||
"build/doc": "typedoc",
|
"build:release": "tsc",
|
||||||
"dist": "ts-node -P tsconfig.node.json scripts/npm-pack.ts build/release dist",
|
"build:debug": "tsc -p tsconfig.debug.json",
|
||||||
"test-reports/unit": "jest"
|
"doc": "typedoc --entryPoints src/index.ts --html build/doc",
|
||||||
|
"publish_": "ts-node -P tsconfig.node.json scripts/publish.ts",
|
||||||
|
"dist": "ts-node -P tsconfig.node.json scripts/npm-pack.ts build/release dist"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -20,16 +19,13 @@
|
||||||
"author": "Tiara Rodney",
|
"author": "Tiara Rodney",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"eslint": "^9.39.4",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-junit": "^16.0.0",
|
"jest-junit": "^16.0.0",
|
||||||
"ts-jest": "^29.3.2",
|
"ts-jest": "^29.3.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typedoc": "^0.28.3",
|
"typedoc": "^0.27.9",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3"
|
||||||
"typescript-eslint": "^8.57.0"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|
|
||||||
192
src/config.ts
192
src/config.ts
|
|
@ -1,192 +0,0 @@
|
||||||
|
|
||||||
import { MANAGER } from './manager';
|
|
||||||
import { ValueError } from './helper/error';
|
|
||||||
import { STYLES, Formatter } from './formatter';
|
|
||||||
import { StreamHandler, FileHandler, Handler } from './handler';
|
|
||||||
import { LogLevel } from './log-level';
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// Configuration classes and functions
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* options for basic configuration of logging module
|
|
||||||
*/
|
|
||||||
export interface BasicConfigOptions {
|
|
||||||
/*
|
|
||||||
* Specifies that a FileHandler be created, using the specified filename,
|
|
||||||
* rather than a StreamHandler.
|
|
||||||
*/
|
|
||||||
filename?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies the mode to open the file, if filename is specified (if
|
|
||||||
* filemode is unspecified, it defaults to 'a')
|
|
||||||
*/
|
|
||||||
filemode?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use the specified format string for the handler.
|
|
||||||
*/
|
|
||||||
format?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use the specified date/time format.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
datefmt?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If a format string is specified, use this to specify the type of format
|
|
||||||
* string (possible values '%', '{', '$', for %-formatting,
|
|
||||||
* :meth:`str.format` and :class:`string.Template`- defaults to '%').
|
|
||||||
*
|
|
||||||
* TODO: switch to enum
|
|
||||||
*/
|
|
||||||
style?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the root logger level to the specified level.
|
|
||||||
*/
|
|
||||||
level?: LogLevel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use the specified stream to initialize the StreamHandler. Note that this
|
|
||||||
* argument is incompatible with 'filename' - if both are present, 'stream'
|
|
||||||
* is ignored.
|
|
||||||
*
|
|
||||||
* TODO:
|
|
||||||
*/
|
|
||||||
stream?: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If specified, this should be an iterable of already created handlers,
|
|
||||||
* which will be added to the root logger. Any handler in the list which
|
|
||||||
* does not have a formatter assigned will be assigned the formatter created
|
|
||||||
* in this function.
|
|
||||||
*/
|
|
||||||
handlers?: Handler[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this keyword is specified as true, any existing handlers attached to
|
|
||||||
* the root logger are removed and closed, before carrying out the
|
|
||||||
* configuration as specified by the other arguments.
|
|
||||||
*/
|
|
||||||
force?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If specified together with a filename, this encoding is passed to the
|
|
||||||
* created FileHandler, causing it to be used when the file is opened.
|
|
||||||
*/
|
|
||||||
encoding?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If specified together with a filename, this value is
|
|
||||||
* passed to the created FileHandler, causing it to be used
|
|
||||||
* when the file is opened in text mode. If not specified,
|
|
||||||
* the default value is `backslashreplace`.
|
|
||||||
*/
|
|
||||||
errors?: string|null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do basic configuration for the logging system.
|
|
||||||
*
|
|
||||||
* This function does nothing if the root logger already has handlers
|
|
||||||
* configured, unless the keyword argument *force* is set to ``True``.
|
|
||||||
* It is a convenience method intended for use by simple scripts
|
|
||||||
* to do one-shot configuration of the logging package.
|
|
||||||
*
|
|
||||||
* The default behaviour is to create a StreamHandler which writes to
|
|
||||||
* sys.stderr, set a formatter using the BASIC_FORMAT format string, and
|
|
||||||
* add the handler to the root logger.
|
|
||||||
*
|
|
||||||
* A number of optional keyword arguments may be specified, which can alter
|
|
||||||
* the default behaviour.
|
|
||||||
*
|
|
||||||
* Note that you could specify a stream created using open(filename, mode)
|
|
||||||
* rather than passing the filename and mode in. However, it should be
|
|
||||||
* remembered that StreamHandler does not close its stream (since it may be
|
|
||||||
* using sys.stdout or sys.stderr), whereas FileHandler closes its stream
|
|
||||||
* when the handler is closed.
|
|
||||||
*
|
|
||||||
* TODO: refactor logic, there apparently is some redundancy in the original
|
|
||||||
* code
|
|
||||||
*/
|
|
||||||
export function basicConfig(options: BasicConfigOptions) {
|
|
||||||
const force = options.force ?? false;
|
|
||||||
var encoding = options.encoding ?? undefined;
|
|
||||||
var errors: string|undefined = options.errors ?? 'backslashreplace';
|
|
||||||
var handlers = options.handlers ?? [];
|
|
||||||
const filename = options.filename ?? null;
|
|
||||||
const stream = options.stream ?? null;
|
|
||||||
const filemode = options.filemode ?? 'a';
|
|
||||||
const dateformat = options.datefmt;
|
|
||||||
const style = options.style ?? '%';
|
|
||||||
const level = options.level ?? null;
|
|
||||||
|
|
||||||
if (!Object.keys(STYLES).includes(style)) {
|
|
||||||
throw new ValueError(
|
|
||||||
`style must be one of: ${Object.keys(STYLES).join(', ')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (force) {
|
|
||||||
for (var i = 0; i < MANAGER.root.handlers.length; i += 1) {
|
|
||||||
let h: Handler = MANAGER.root.handlers[i];
|
|
||||||
MANAGER.root.removeHandler(h);
|
|
||||||
h.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MANAGER.root.handlers.length > 0 && !force) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handlers.length > 0) {
|
|
||||||
if (stream || filename) {
|
|
||||||
throw new ValueError(
|
|
||||||
"'stream' or 'filename' should not be specified together " +
|
|
||||||
"with 'handlers'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (stream && filename) {
|
|
||||||
throw new ValueError(
|
|
||||||
"'stream' and 'filename' should not be specified together"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filename) {
|
|
||||||
if (filemode.match('b')) { errors = undefined }
|
|
||||||
else { encoding = 'utf-8' }
|
|
||||||
|
|
||||||
handlers = [new FileHandler({
|
|
||||||
filename: filename,
|
|
||||||
filemode: filemode,
|
|
||||||
encoding: encoding,
|
|
||||||
errors: errors
|
|
||||||
})];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
handlers = [new StreamHandler(stream)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < handlers.length; i += 1) {
|
|
||||||
const h = handlers[i];
|
|
||||||
|
|
||||||
if (h.formatter === null) {
|
|
||||||
h.formatter = new Formatter({
|
|
||||||
fmt: options.format ?? STYLES[style][1],
|
|
||||||
datefmt: dateformat,
|
|
||||||
style: style
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
MANAGER.root.addHandler(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (level !== null) { MANAGER.root.setLevel(level) }
|
|
||||||
}
|
|
||||||
114
src/filter.ts
114
src/filter.ts
|
|
@ -1,114 +0,0 @@
|
||||||
import { LogRecord } from './log-record';
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// Filter classes and functions
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export type FilterCallable = (record: LogRecord) => boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter instances are used to perform arbitrary filtering of LogRecords.
|
|
||||||
*
|
|
||||||
* Loggers and Handlers can optionally use Filter instances to filter records as
|
|
||||||
* desired. The base filter class only allows events which are below a certain
|
|
||||||
* point in the logger hierarchy. For example, a filter initialized with "A.B"
|
|
||||||
* will allow events logged by loggers "A.B", initialized with the empty string,
|
|
||||||
* all events are passed.
|
|
||||||
*/
|
|
||||||
export class Filter {
|
|
||||||
public readonly scope: string;
|
|
||||||
public readonly slen: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize with the name of the logger which ,together with its children,
|
|
||||||
* will have its events allowed through the filter. If no name is specified,
|
|
||||||
* allow every event.
|
|
||||||
*
|
|
||||||
* @param name - name of logging scope
|
|
||||||
*/
|
|
||||||
constructor(scope: string) {
|
|
||||||
this.scope = scope ?? '';
|
|
||||||
this.slen = this.scope.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inspect a record, if it should be logged.
|
|
||||||
*
|
|
||||||
* Returns true if the record should be logged, or false otherwise. If
|
|
||||||
* deemed appropriate, the record may be modified in-place.
|
|
||||||
*
|
|
||||||
* @param - scope of log record to inspect
|
|
||||||
* @param - log record to inspect
|
|
||||||
*/
|
|
||||||
filter(record: LogRecord): boolean {
|
|
||||||
if (this.slen == 0 || this.scope == record.scope) { return true }
|
|
||||||
else if (!record.scope.substring(0, this.slen)) { return false }
|
|
||||||
return (record.scope[this.slen] == '.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Filterer {
|
|
||||||
filters: Filter[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the specified filter to this handler.
|
|
||||||
*
|
|
||||||
* @param filter
|
|
||||||
*/
|
|
||||||
addFilter(filter: Filter) {
|
|
||||||
if (!this.filters.includes(filter)) { this.filters.push(filter) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified filter from this handler.
|
|
||||||
*
|
|
||||||
* @param filter
|
|
||||||
*/
|
|
||||||
removeFilter(filter: Filter) {
|
|
||||||
if (this.filters.includes(filter)) {
|
|
||||||
this.filters.splice(this.filters.indexOf(filter), 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a record is loggable by consulting all the filters.
|
|
||||||
*
|
|
||||||
* The default is to allow the record to be logged; any filter can veto this
|
|
||||||
* by returning a false value.
|
|
||||||
* If a filter attached to a handler returns a log record instance, then
|
|
||||||
* that instance is used in place of the original log record in any further
|
|
||||||
* processing of the event by that handler.
|
|
||||||
* If a filter returns any other true value, the original log record is used
|
|
||||||
* in any further processing of the event by that handler.
|
|
||||||
*
|
|
||||||
* If none of the filters return false values, this method returns a log
|
|
||||||
* record.
|
|
||||||
*
|
|
||||||
* If any of the filters return a false value, this method returns a false
|
|
||||||
* value.
|
|
||||||
*
|
|
||||||
* @param filter
|
|
||||||
*/
|
|
||||||
filter(record: LogRecord): LogRecord|null {
|
|
||||||
|
|
||||||
for (var i = 0; i < this.filters.length; i += 1) {
|
|
||||||
let result: boolean|LogRecord = false;
|
|
||||||
|
|
||||||
let filter = this.filters[i];
|
|
||||||
|
|
||||||
if (typeof (filter as Filter).filter == 'function') {
|
|
||||||
result = (filter as Filter).filter(record)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
result = (filter as unknown as FilterCallable)(record)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result) { return null }
|
|
||||||
|
|
||||||
if ((result as any) instanceof LogRecord) { record = result as unknown as LogRecord }
|
|
||||||
}
|
|
||||||
|
|
||||||
return record
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
217
src/formatter.ts
217
src/formatter.ts
|
|
@ -1,217 +0,0 @@
|
||||||
import { MyError, ValueError } from './helper/error';
|
|
||||||
import { LogRecord } from './log-record';
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// Formatter classes and functions
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export interface PercentFormatterStyleOptions {
|
|
||||||
fmt?: string,
|
|
||||||
defaults: {[key: string]: any};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* %-style formatting for log records.
|
|
||||||
*
|
|
||||||
* Substitutes %(name)s, %(name)d, %(name)f placeholders from record
|
|
||||||
* attributes.
|
|
||||||
*/
|
|
||||||
class PercentFormatterStyle {
|
|
||||||
public static defaultFormat = '%(message)s';
|
|
||||||
public static asctimeFormat = '%(asctime)s';
|
|
||||||
public static asctimeSearch = '%(asctime)';
|
|
||||||
public static validationPattern =
|
|
||||||
/%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]/;
|
|
||||||
|
|
||||||
public fmt: string;
|
|
||||||
private defaults: {[key: string]: any};
|
|
||||||
|
|
||||||
constructor(options: PercentFormatterStyleOptions) {
|
|
||||||
this.fmt = options.fmt ?? PercentFormatterStyle.defaultFormat;
|
|
||||||
this.defaults = options.defaults;
|
|
||||||
}
|
|
||||||
|
|
||||||
usesTime(): boolean {
|
|
||||||
return this.fmt.indexOf(PercentFormatterStyle.asctimeSearch) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the input format, ensure it matches the correct style
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
if (!PercentFormatterStyle.validationPattern.test(this.fmt)) {
|
|
||||||
throw new ValueError(
|
|
||||||
`Invalid format '${this.fmt}' for ` +
|
|
||||||
`'${PercentFormatterStyle.defaultFormat[0]}'`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _format(record: LogRecord): string {
|
|
||||||
const values: {[key: string]: any} = {
|
|
||||||
...this.defaults,
|
|
||||||
...(record as unknown as {[key: string]: any}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.fmt.replace(
|
|
||||||
/%\((\w+)\)([#0+ -]*(?:\*|\d+)?(?:\.(?:\*|\d+))?[diouxefgcrsa%])/g,
|
|
||||||
(_match, key, spec) => {
|
|
||||||
if (!(key in values)) {
|
|
||||||
throw new ValueError(`formatting field not found in record: ${key}`);
|
|
||||||
}
|
|
||||||
const val = values[key];
|
|
||||||
const conversion = spec[spec.length - 1];
|
|
||||||
switch (conversion) {
|
|
||||||
case 'd':
|
|
||||||
case 'i':
|
|
||||||
return String(Math.floor(Number(val)));
|
|
||||||
case 'f':
|
|
||||||
return String(Number(val));
|
|
||||||
case 's':
|
|
||||||
return String(val);
|
|
||||||
default:
|
|
||||||
return String(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
format(record: LogRecord): string {
|
|
||||||
try {
|
|
||||||
return this._format(record)
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
if (e instanceof ValueError) { throw e }
|
|
||||||
throw new ValueError(`formatting field not found in record: ${e}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const BASIC_FORMAT = '%(levelname)s:%(name)s:%(message)s';
|
|
||||||
|
|
||||||
export const STYLES: {[key: string]: [{ new(options: PercentFormatterStyleOptions): PercentFormatterStyle}, string]} = {
|
|
||||||
'%': [PercentFormatterStyle, BASIC_FORMAT],
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FormatterOptions {
|
|
||||||
fmt?: string
|
|
||||||
datefmt?: string
|
|
||||||
style?: string
|
|
||||||
validate?: boolean
|
|
||||||
defaults?: {[key: string]: any}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formatter instances are used to convert a LogRecord to text.
|
|
||||||
*
|
|
||||||
* Formatters need to know how a LogRecord is constructed. They are
|
|
||||||
* responsible for converting a LogRecord to (usually) a string which can
|
|
||||||
* be interpreted by either a human or an external system. The base Formatter
|
|
||||||
* allows a formatting string to be specified. If none is supplied, the
|
|
||||||
* style-dependent default value, "%(message)s", "{message}", or
|
|
||||||
* "${message}", is used.
|
|
||||||
*
|
|
||||||
* The Formatter can be initialized with a format string which makes use of
|
|
||||||
* knowledge of the LogRecord attributes - e.g. the default value mentioned
|
|
||||||
* above makes use of the fact that the user's message and arguments are pre-
|
|
||||||
* formatted into a LogRecord's message attribute. Currently, the useful
|
|
||||||
* attributes in a LogRecord are described by:
|
|
||||||
*
|
|
||||||
* %(name)s Name of the logger (logging channel)
|
|
||||||
* %(levelno)s Numeric logging level for the message (DEBUG, INFO,
|
|
||||||
* WARNING, ERROR, CRITICAL)
|
|
||||||
* %(levelname)s Text logging level for the message ("DEBUG", "INFO",
|
|
||||||
* "WARNING", "ERROR", "CRITICAL")
|
|
||||||
* %(created)f Time when the LogRecord was created (Date.now()
|
|
||||||
* return value)
|
|
||||||
* %(asctime)s Textual time when the LogRecord was created
|
|
||||||
* %(message)s The result of record.getMessage(), computed just as
|
|
||||||
* the record is emitted
|
|
||||||
*/
|
|
||||||
export class Formatter {
|
|
||||||
public static defaultTimeFormat = 'YYYY-MM-DDTHH:mm:ss';
|
|
||||||
public static defaultMsecFormat = '%s.%03d';
|
|
||||||
|
|
||||||
protected style: PercentFormatterStyle;
|
|
||||||
protected fmt: string;
|
|
||||||
protected datefmt: string|undefined;
|
|
||||||
|
|
||||||
constructor(options?: FormatterOptions) {
|
|
||||||
options = options ?? {};
|
|
||||||
const style = options.style ?? '%';
|
|
||||||
const validate = options.validate ?? true;
|
|
||||||
|
|
||||||
if (!Object.keys(STYLES).includes(style)) {
|
|
||||||
throw new ValueError(`style must be one of: ${Object.keys(STYLES).join(', ')}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.style = new STYLES[style][0]({
|
|
||||||
fmt: options.fmt,
|
|
||||||
defaults: options.defaults ?? {}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (validate) { this.style.validate() }
|
|
||||||
|
|
||||||
this.fmt = this.style.fmt;
|
|
||||||
|
|
||||||
this.datefmt = options.datefmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
usesTime(): boolean {
|
|
||||||
return this.style.usesTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format the specified record as text.
|
|
||||||
*
|
|
||||||
* The record's attribute dictionary is used as the operand to a string
|
|
||||||
* formatting operation which yields the returned string. Before the
|
|
||||||
* formatting operation, a couple of preparatory steps are carried out.
|
|
||||||
* The message attribute of the record is computed using LogRecord.getMessage().
|
|
||||||
* If the formatting string uses the time, formatTime() is called to format
|
|
||||||
* the event time.
|
|
||||||
*/
|
|
||||||
format(record: LogRecord): string {
|
|
||||||
record.message = record.getMessage();
|
|
||||||
|
|
||||||
if (this.usesTime()) {
|
|
||||||
record.asctime = this.formatTime(record, this.datefmt);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.style.format(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the creation time of the specified LogRecord as formatted text.
|
|
||||||
*
|
|
||||||
* If datefmt is specified, it is used as a strftime-style format string.
|
|
||||||
* Supported tokens: %Y, %m, %d, %H, %M, %S.
|
|
||||||
* Otherwise, an ISO8601-like format is used.
|
|
||||||
*/
|
|
||||||
formatTime(record: LogRecord, datefmt?: string): string {
|
|
||||||
const dt = new Date(record.created);
|
|
||||||
|
|
||||||
if (datefmt) {
|
|
||||||
return datefmt
|
|
||||||
.replace('%Y', String(dt.getFullYear()))
|
|
||||||
.replace('%m', String(dt.getMonth() + 1).padStart(2, '0'))
|
|
||||||
.replace('%d', String(dt.getDate()).padStart(2, '0'))
|
|
||||||
.replace('%H', String(dt.getHours()).padStart(2, '0'))
|
|
||||||
.replace('%M', String(dt.getMinutes()).padStart(2, '0'))
|
|
||||||
.replace('%S', String(dt.getSeconds()).padStart(2, '0'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const iso = dt.toISOString();
|
|
||||||
return iso.replace('T', ' ').replace('Z', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format and return the specified exception information as a string.
|
|
||||||
*/
|
|
||||||
formatError(ei: MyError): string {
|
|
||||||
if (ei.stack) { return ei.stack }
|
|
||||||
return String(ei);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_FORMATTER = new Formatter();
|
|
||||||
347
src/handler.ts
347
src/handler.ts
|
|
@ -1,347 +0,0 @@
|
||||||
import { LogLevel, checkLevel, NOTSET, WARNING, ERROR } from './log-level';
|
|
||||||
import { LogRecord } from './log-record';
|
|
||||||
import { Formatter, DEFAULT_FORMATTER } from './formatter';
|
|
||||||
import { Filterer } from './filter';
|
|
||||||
import { NotImplementedError } from './helper/error';
|
|
||||||
import { Writable, DEFAULT_STREAM, StderrWritable } from './helper/stream';
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// Handler classes and functions
|
|
||||||
//----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type Handlers = {[key: string]: Handler};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* map of handler names to handlers
|
|
||||||
*/
|
|
||||||
const HANDLERS: Handlers = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* added to allow handlers to be removed in reverse order of initialization
|
|
||||||
*/
|
|
||||||
const HANDLER_LIST: WeakRef<Handler>[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a handler to the internal cleanup list using a weak reference.
|
|
||||||
*
|
|
||||||
* @param handler -
|
|
||||||
*/
|
|
||||||
function addHandlerRef(handler: Handler) {
|
|
||||||
HANDLER_LIST.push(new WeakRef(handler));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a handler with the specified *name*, or None if there isn't one with
|
|
||||||
* that name.
|
|
||||||
*/
|
|
||||||
export function getHandlerByName(name: string): Handler|null {
|
|
||||||
return HANDLERS[name] ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return all known handler names as an immutable set
|
|
||||||
*/
|
|
||||||
export function getHandlerNames(): Handlers { return Object.freeze(HANDLERS) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler instances dispatch logging events to specific destinations.
|
|
||||||
*
|
|
||||||
* The base handler class. Acts as a placeholder which defines the Handler
|
|
||||||
* interface. Handlers can optionally use Formatter instances to format
|
|
||||||
* records as desired. By default, no formatter is specified; in this case,
|
|
||||||
* the 'raw' message as determined by record.message is logged.
|
|
||||||
*/
|
|
||||||
export class Handler extends Filterer {
|
|
||||||
|
|
||||||
protected _scope: string|null = null;
|
|
||||||
protected _formatter: Formatter|null = null;
|
|
||||||
protected _level: number;
|
|
||||||
protected _closed: boolean = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the instance - basically setting the formatter to None
|
|
||||||
* and the filter list to empty
|
|
||||||
*/
|
|
||||||
constructor(level?: LogLevel) {
|
|
||||||
super();
|
|
||||||
this._level = checkLevel(level ?? NOTSET);
|
|
||||||
// Add the handler to the global HANDLER_LIST (for cleanup on shutdown)
|
|
||||||
addHandlerRef(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
get level(): number { return this._level }
|
|
||||||
set level(level: LogLevel|string) { this._level = checkLevel(level) }
|
|
||||||
|
|
||||||
get scope(): string|null { return this._scope }
|
|
||||||
set scope(scope: string) { this._scope = scope }
|
|
||||||
get closed(): boolean { return this._closed }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format the specified record.
|
|
||||||
*
|
|
||||||
* If a formatter is set, use it. Otherwise, use the default formatter for
|
|
||||||
* the module.
|
|
||||||
*/
|
|
||||||
format(record: LogRecord): string {
|
|
||||||
const fmt = this.formatter ?? DEFAULT_FORMATTER;
|
|
||||||
return fmt.format(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do whatever it takes to actually log the specified logging record.
|
|
||||||
*
|
|
||||||
* This version is intended to be implemented by subclasses and so raises a
|
|
||||||
* NotImplementedError.
|
|
||||||
*/
|
|
||||||
emit(record: LogRecord) {
|
|
||||||
throw new NotImplementedError(
|
|
||||||
'emit must be implemented by Handler subclass'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Conditionally emit the specfied logging record.
|
|
||||||
*
|
|
||||||
* Emission depends on filters which may have been added to the handler.
|
|
||||||
* Wrap the actual emission of the record with acquisition/release of the
|
|
||||||
* I/O thread lock.
|
|
||||||
*/
|
|
||||||
handle(record: LogRecord) {
|
|
||||||
const rv = this.filter(record);
|
|
||||||
if (!rv) { return }
|
|
||||||
let filtered = record;
|
|
||||||
if ((rv as any) instanceof LogRecord) {
|
|
||||||
filtered = rv as unknown as LogRecord
|
|
||||||
}
|
|
||||||
this.emit(filtered)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tidy up any resources used by the handler
|
|
||||||
*
|
|
||||||
* This version removes the handler from an internal map of handlers, which
|
|
||||||
* is used for handler lookup by scope. Subclasses should ensure that this
|
|
||||||
* gets called from overriden close() methods.
|
|
||||||
*/
|
|
||||||
close() {
|
|
||||||
this._closed = true;
|
|
||||||
|
|
||||||
if (this.scope && Object.keys(HANDLERS).includes(this.scope)) {
|
|
||||||
delete HANDLERS[this.scope]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle errors which occur during an emit() call.
|
|
||||||
*
|
|
||||||
* This method should be called from handlers when an exception is
|
|
||||||
* encountered during an emit() call.
|
|
||||||
*/
|
|
||||||
handleError(record: LogRecord) {
|
|
||||||
try {
|
|
||||||
console.error('--- Logging error ---');
|
|
||||||
console.error('Error in handler for record:', record.scope, record.msg);
|
|
||||||
}
|
|
||||||
catch (_) {
|
|
||||||
// silently ignore errors in error handling
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get formatter(): Formatter|null { return this._formatter }
|
|
||||||
set formatter(fmt: Formatter) { this._formatter = fmt }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A handler class which writes logging records, appropriately formatted,
|
|
||||||
* to a stream. Note that this class does not close the stream, as
|
|
||||||
* the default stream may be shared.
|
|
||||||
*/
|
|
||||||
export class StreamHandler extends Handler {
|
|
||||||
protected stream: Writable;
|
|
||||||
|
|
||||||
constructor(stream?: Writable) {
|
|
||||||
super();
|
|
||||||
this.stream = stream ?? DEFAULT_STREAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(record: LogRecord) {
|
|
||||||
try {
|
|
||||||
const msg = this.format(record);
|
|
||||||
this.stream.write(msg);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
this.handleError(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A handler class which writes logging records to the browser console,
|
|
||||||
* mapping log levels to the appropriate console method.
|
|
||||||
*
|
|
||||||
* This is the primary handler for browser environments.
|
|
||||||
*/
|
|
||||||
export class ConsoleHandler extends Handler {
|
|
||||||
emit(record: LogRecord) {
|
|
||||||
try {
|
|
||||||
const msg = this.format(record);
|
|
||||||
|
|
||||||
if (record.levelno >= ERROR) {
|
|
||||||
console.error(msg);
|
|
||||||
}
|
|
||||||
else if (record.levelno >= WARNING) {
|
|
||||||
console.warn(msg);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
this.handleError(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileHandlerOptions {
|
|
||||||
filename: string
|
|
||||||
filemode?: string
|
|
||||||
encoding?: string
|
|
||||||
errors?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FileHandler extends StreamHandler {
|
|
||||||
constructor(options: FileHandlerOptions) {
|
|
||||||
super();
|
|
||||||
throw new NotImplementedError(
|
|
||||||
'FileHandler is not available in browser environments. ' +
|
|
||||||
'Use ConsoleHandler or a storage-backed handler instead.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A handler that persists log records to browser localStorage.
|
|
||||||
*
|
|
||||||
* Records are stored as a JSON array of formatted strings under a
|
|
||||||
* configurable key. Log rotation is supported by entry count and/or
|
|
||||||
* byte size to avoid exceeding storage quotas.
|
|
||||||
*/
|
|
||||||
export interface LocalStorageHandlerOptions {
|
|
||||||
/**
|
|
||||||
* localStorage key to store entries under.
|
|
||||||
* Defaults to 'esm-logging'.
|
|
||||||
*/
|
|
||||||
key?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum number of entries to retain. Oldest entries are discarded
|
|
||||||
* first. Set to 0 for no entry limit. Defaults to 1000.
|
|
||||||
*/
|
|
||||||
maxEntries?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum size in bytes (as measured by the JSON-serialized string
|
|
||||||
* length). Oldest entries are discarded to stay within this limit.
|
|
||||||
* Set to 0 for no byte limit. Defaults to 0.
|
|
||||||
*/
|
|
||||||
maxBytes?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LocalStorageHandler extends Handler {
|
|
||||||
protected key: string;
|
|
||||||
protected maxEntries: number;
|
|
||||||
protected maxBytes: number;
|
|
||||||
|
|
||||||
constructor(options?: LocalStorageHandlerOptions) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
if (typeof localStorage === 'undefined') {
|
|
||||||
throw new NotImplementedError(
|
|
||||||
'LocalStorageHandler requires a browser environment with localStorage'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.key = options?.key ?? 'esm-logging';
|
|
||||||
this.maxEntries = options?.maxEntries ?? 1000;
|
|
||||||
this.maxBytes = options?.maxBytes ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(record: LogRecord) {
|
|
||||||
try {
|
|
||||||
const msg = this.format(record);
|
|
||||||
const entries = this.getEntries();
|
|
||||||
entries.push(msg);
|
|
||||||
this._rotate(entries);
|
|
||||||
this._setEntries(entries);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
this.handleError(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve all stored log entries.
|
|
||||||
*/
|
|
||||||
getEntries(): string[] {
|
|
||||||
try {
|
|
||||||
const data = localStorage.getItem(this.key);
|
|
||||||
return data ? JSON.parse(data) : [];
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all stored log entries.
|
|
||||||
*/
|
|
||||||
clearEntries() {
|
|
||||||
localStorage.removeItem(this.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
super.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discard oldest entries to stay within maxEntries and maxBytes limits.
|
|
||||||
*/
|
|
||||||
protected _rotate(entries: string[]) {
|
|
||||||
if (this.maxEntries > 0) {
|
|
||||||
while (entries.length > this.maxEntries) {
|
|
||||||
entries.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.maxBytes > 0) {
|
|
||||||
while (entries.length > 0) {
|
|
||||||
const serialized = JSON.stringify(entries);
|
|
||||||
if (serialized.length <= this.maxBytes) { break }
|
|
||||||
entries.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _setEntries(entries: string[]) {
|
|
||||||
localStorage.setItem(this.key, JSON.stringify(entries));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is like a StreamHandler using sys.stderr, but always uses
|
|
||||||
* whatever sys.stderr is currently set to rather than the value of
|
|
||||||
* sys.stderr at handler construction time.
|
|
||||||
*/
|
|
||||||
export class StderrHandler extends Handler {
|
|
||||||
constructor(level: LogLevel) { super(level) }
|
|
||||||
|
|
||||||
emit(record: LogRecord) {
|
|
||||||
try {
|
|
||||||
const msg = this.format(record);
|
|
||||||
console.error(msg);
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
this.handleError(record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +1,2 @@
|
||||||
/**
|
|
||||||
* Minimal writable stream interface for browser compatibility.
|
|
||||||
*
|
|
||||||
* This abstracts over Node.js streams and browser console output,
|
|
||||||
* providing a common write target for handlers.
|
|
||||||
*/
|
|
||||||
export interface Writable {
|
|
||||||
write(data: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
export type MillisecondsSinceUnixEpoch = number;
|
||||||
* A Writable backed by console.log.
|
|
||||||
*/
|
|
||||||
export class ConsoleWritable implements Writable {
|
|
||||||
write(data: string): void {
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Writable backed by console.error (stderr equivalent).
|
|
||||||
*/
|
|
||||||
export class StderrWritable implements Writable {
|
|
||||||
write(data: string): void {
|
|
||||||
console.error(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default output stream. Uses console.error to match Python's default
|
|
||||||
* of writing to sys.stderr.
|
|
||||||
*/
|
|
||||||
export const DEFAULT_STREAM: Writable = new StderrWritable();
|
|
||||||
|
|
|
||||||
1317
src/index.ts
1317
src/index.ts
File diff suppressed because it is too large
Load diff
118
src/log-level.ts
118
src/log-level.ts
|
|
@ -1,118 +0,0 @@
|
||||||
/*---------------------------------------------------------------------------
|
|
||||||
Level related stuff
|
|
||||||
---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
Default levels and level names, these can be replaced with any positive set
|
|
||||||
of values having corresponding names. There is a pseudo-level, NOTSET, which
|
|
||||||
is only really there as a lower limit for user-defined levels. Handlers and
|
|
||||||
loggers are initialized with NOTSET so that they will log all messages, even
|
|
||||||
at user-defined levels.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type LogLevel = number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An indication that something unexpected happened, or that a problem might
|
|
||||||
* occur in the near future (e.g. ‘disk space low’). The software is still
|
|
||||||
* working as expected.
|
|
||||||
*/
|
|
||||||
export const CRITICAL = 50;
|
|
||||||
export const FATAL = CRITICAL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Due to a more serious problem, the software has not been able to perform some
|
|
||||||
* function.
|
|
||||||
*/
|
|
||||||
export const ERROR = 40;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An indication that something unexpected happened, or that a problem might
|
|
||||||
* occur in the near future (e.g. ‘disk space low’). The software is still
|
|
||||||
* working as expected.
|
|
||||||
*/
|
|
||||||
export const WARNING = 30;
|
|
||||||
export const WARN = WARNING;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirmation that things are working as expected.
|
|
||||||
*/
|
|
||||||
export const INFO = 20;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detailed information, typically only of interest to a developer trying to
|
|
||||||
* diagnose a problem.
|
|
||||||
*/
|
|
||||||
export const DEBUG = 10;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When set on a logger, indicates that ancestor loggers are to be consulted to
|
|
||||||
* determine the effective level. If that still resolves to NOTSET, then all
|
|
||||||
* events are logged. When set on a handler, all events are handled.
|
|
||||||
*/
|
|
||||||
export const NOTSET = 0;
|
|
||||||
|
|
||||||
const LEVELTONAME: {[key: number]: string} = {
|
|
||||||
[CRITICAL]: 'CRITICAL',
|
|
||||||
[ERROR]: 'ERROR',
|
|
||||||
[WARNING]: 'WARNING',
|
|
||||||
[INFO]: 'INFO',
|
|
||||||
[DEBUG]: 'DEBUG',
|
|
||||||
[NOTSET]: 'NOTSET'
|
|
||||||
}
|
|
||||||
|
|
||||||
const NAMETOLEVEL: {[key: string]: number} = {
|
|
||||||
CRITICAL: CRITICAL,
|
|
||||||
ERROR: ERROR,
|
|
||||||
WARNING: WARNING,
|
|
||||||
INFO: INFO,
|
|
||||||
DEBUG: DEBUG,
|
|
||||||
NOTSET: NOTSET,
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLevelNamesMapping() {
|
|
||||||
return Object.assign({}, NAMETOLEVEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the textual or numeric representation of logging level 'level'
|
|
||||||
*
|
|
||||||
* @param level
|
|
||||||
*/
|
|
||||||
export function getLevelName(level: string|number): string|number {
|
|
||||||
var result: string|number = LEVELTONAME[level as number];
|
|
||||||
if (result !== undefined) { return result }
|
|
||||||
result = NAMETOLEVEL[level as string];
|
|
||||||
if (result !== undefined) { return result }
|
|
||||||
return `Level ${level}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Associate 'levelName' with 'level'
|
|
||||||
*
|
|
||||||
* @param level
|
|
||||||
* @param levelName
|
|
||||||
*/
|
|
||||||
export function addLevelName(level: number, levelName: string) {
|
|
||||||
LEVELTONAME[level] = levelName;
|
|
||||||
NAMETOLEVEL[levelName] = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkLevel(level: number|string): number {
|
|
||||||
var rv: number;
|
|
||||||
|
|
||||||
if (typeof level == 'number') { rv = level }
|
|
||||||
|
|
||||||
else if (typeof level == 'string') {
|
|
||||||
if (!Object.keys(NAMETOLEVEL).includes(level as string)) {
|
|
||||||
throw new Error(`Unknown level: ${level}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
rv = NAMETOLEVEL[level]
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
throw new Error(`Level not a number or valid string: ${level}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv
|
|
||||||
}
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
import { getLevelName, LogLevel } from './log-level';
|
|
||||||
import { MillisecondsSinceUnixEpoch } from './helper/datetime';
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// The logging record
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* options for instantiating a new log record
|
|
||||||
*/
|
|
||||||
export interface LogRecordOptions {
|
|
||||||
/**
|
|
||||||
* The numeric level of the logging event (such as 10 for DEBUG, 20 for
|
|
||||||
* INFO, etc). Note that this is converted to two attributes of the
|
|
||||||
* LogRecord: levelno for the numeric value and levelname for the
|
|
||||||
* corresponding level name.
|
|
||||||
*/
|
|
||||||
level: number,
|
|
||||||
file?: string,
|
|
||||||
/**
|
|
||||||
* The line number in the source file where the logging call was made.
|
|
||||||
*/
|
|
||||||
lno?: number,
|
|
||||||
/**
|
|
||||||
* The event description message, which can be a %-format string with
|
|
||||||
* placeholders for variable data, or an arbitrary object (see Using
|
|
||||||
* arbitrary objects as messages).
|
|
||||||
*/
|
|
||||||
msg: string,
|
|
||||||
/**
|
|
||||||
* Variable data to merge into the msg argument to obtain the event
|
|
||||||
* description.
|
|
||||||
*/
|
|
||||||
args?: any[],
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LogRecordFactory = { (name: string, options: LogRecordOptions): LogRecord };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LogRecord instances are created every time something is logged. They contain
|
|
||||||
* all the information pertinent to the event being logged. The main
|
|
||||||
* information parssed in is msg and args, which are combined using str(msg) %
|
|
||||||
* args to create the message field of the record. The record also includes
|
|
||||||
* information such as when the record was created, the source line where the
|
|
||||||
* logging call was made, and any exception information to be logged.
|
|
||||||
*/
|
|
||||||
export class LogRecord {
|
|
||||||
public readonly levelno: LogLevel;
|
|
||||||
public readonly levelname: string|LogLevel;
|
|
||||||
public readonly scope: string;
|
|
||||||
public readonly name: string;
|
|
||||||
public readonly msg: string;
|
|
||||||
public readonly args: any[]|undefined;
|
|
||||||
public message: string = '';
|
|
||||||
|
|
||||||
public readonly created: MillisecondsSinceUnixEpoch = Date.now();
|
|
||||||
public asctime: string = '';
|
|
||||||
|
|
||||||
constructor(scope: string, options: LogRecordOptions) {
|
|
||||||
this.levelno = options.level;
|
|
||||||
this.levelname = getLevelName(options.level);
|
|
||||||
this.scope = scope;
|
|
||||||
this.name = scope;
|
|
||||||
this.msg = options.msg;
|
|
||||||
this.args = options.args;
|
|
||||||
}
|
|
||||||
|
|
||||||
getMessage(): string {
|
|
||||||
let msg = String(this.msg);
|
|
||||||
if (this.args && this.args.length > 0) {
|
|
||||||
msg = this.args.reduce(
|
|
||||||
(s, arg) => s.replace('%s', String(arg)),
|
|
||||||
msg
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export var logRecordFactory = (scope: string, options: LogRecordOptions) => {
|
|
||||||
return new LogRecord(scope, options)
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define which class use when instantiating log records.
|
|
||||||
*
|
|
||||||
* @param factory - A callable which will be called to instantiate a log record.
|
|
||||||
* Pass a clojure, if your factory is a class already.
|
|
||||||
*/
|
|
||||||
export function setLogRecordFactory(factory: LogRecordFactory) {
|
|
||||||
logRecordFactory = factory
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLogRecordFactory(): LogRecordFactory {
|
|
||||||
return logRecordFactory
|
|
||||||
}
|
|
||||||
363
src/logger.ts
363
src/logger.ts
|
|
@ -1,363 +0,0 @@
|
||||||
import {
|
|
||||||
LogLevel,
|
|
||||||
DEBUG,
|
|
||||||
INFO,
|
|
||||||
WARNING,
|
|
||||||
ERROR,
|
|
||||||
CRITICAL,
|
|
||||||
NOTSET,
|
|
||||||
checkLevel,
|
|
||||||
} from './log-level';
|
|
||||||
import {
|
|
||||||
LogRecord,
|
|
||||||
logRecordFactory,
|
|
||||||
LogRecordOptions,
|
|
||||||
} from './log-record';
|
|
||||||
import { Handler, StderrHandler } from './handler';
|
|
||||||
import {
|
|
||||||
NotImplementedError,
|
|
||||||
KeyError,
|
|
||||||
ValueError,
|
|
||||||
StackTrace,
|
|
||||||
} from './helper/error';
|
|
||||||
import { Manager } from './manager';
|
|
||||||
import { Filterer } from './filter';
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// Logger classes and functions
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export type ExecutionInfo = [string, Error, StackTrace];
|
|
||||||
|
|
||||||
export var throwErrors: boolean = true;
|
|
||||||
|
|
||||||
export const DEFAULT_LAST_RESORT = new StderrHandler(WARNING);
|
|
||||||
|
|
||||||
export var lastResort = DEFAULT_LAST_RESORT;
|
|
||||||
|
|
||||||
export type LoggerClass = { new(): Logger };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* context of a logging event/trigger
|
|
||||||
*/
|
|
||||||
export interface LogOptions{
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
excInfo: ExecutionInfo|Error|null,
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
extra: {[key: string]: any}|null,
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
stackInfo: boolean,
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
stackLevel: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_LOG_OPTIONS: LogOptions = Object.freeze({
|
|
||||||
excInfo: null,
|
|
||||||
extra: null,
|
|
||||||
stackInfo: false,
|
|
||||||
stackLevel: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instances of the logger class represent a single logging channel. A 'logging
|
|
||||||
* channel' indicates an area of an application. Exactly how an 'area' is
|
|
||||||
* defined is up to the application developer. Since an application can have any
|
|
||||||
* number of areas, logging channels are identified by a unique string.
|
|
||||||
* Application areas can be nested (e.g. an area of input process might include
|
|
||||||
* sub-areas "read CSV file", "read XLS files" and "read Gnumeric files"). To
|
|
||||||
* cater for this natural nesting, channel ames are organized into a namespace
|
|
||||||
* hierarchy where levels are separated by periods, much like the Java or Python
|
|
||||||
* package namespace. So in the instance given above, channel names might be
|
|
||||||
* "input" for the upper level, and "input.csv", "input.xls" and "input.gnu" for
|
|
||||||
* the sub-levels.
|
|
||||||
* There is no arbitrary limit to the depth of nesting.
|
|
||||||
*/
|
|
||||||
export class Logger extends Filterer {
|
|
||||||
public readonly scope: string;
|
|
||||||
public _level: number;
|
|
||||||
private _manager: Manager|null = null;
|
|
||||||
public readonly parent: Logger|null = null;
|
|
||||||
public readonly propagate: boolean = true;
|
|
||||||
public readonly handlers: Handler[] = [];
|
|
||||||
public readonly disabled: boolean = false;
|
|
||||||
private cache: {[key: number]: boolean} = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the logger with a name and an optional level
|
|
||||||
*
|
|
||||||
* @param scope -
|
|
||||||
* @param level -
|
|
||||||
* @param manager -
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
scope: string,
|
|
||||||
level?: LogLevel,
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.scope = scope;
|
|
||||||
this._level = checkLevel(level ?? NOTSET);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get level() { return this._level }
|
|
||||||
|
|
||||||
public set level(level: LogLevel) { this._level = checkLevel(level) }
|
|
||||||
|
|
||||||
public get manager(): Manager|null { return this._manager }
|
|
||||||
|
|
||||||
public set manager(manager: Manager) {
|
|
||||||
if (this._manager) {
|
|
||||||
throw new ValueError('logger can only be assigned to manager once');
|
|
||||||
}
|
|
||||||
this._manager = manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setLevel(level: LogLevel) {
|
|
||||||
this.level = checkLevel(level);
|
|
||||||
|
|
||||||
//this.manager.clearCache()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the effective level for this logger.
|
|
||||||
*
|
|
||||||
* Loop through this logger and its parents in the logger hierarchy, looking
|
|
||||||
* for a non-zero logging level. Return the first one found.
|
|
||||||
*/
|
|
||||||
public getEffectiveLevel() {
|
|
||||||
var logger: Logger|null = this;
|
|
||||||
|
|
||||||
while (logger) {
|
|
||||||
if (logger.level) { return logger.level }
|
|
||||||
logger = logger.parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NOTSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is this logger enabled for level 'level'?
|
|
||||||
*/
|
|
||||||
public isEnabledFor(level: LogLevel): boolean {
|
|
||||||
if (this.disabled) { return false }
|
|
||||||
|
|
||||||
if (level in this.cache) {
|
|
||||||
return this.cache[level];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._manager && this._manager.disable >= level) {
|
|
||||||
this.cache[level] = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cache[level] = level >= this.getEffectiveLevel();
|
|
||||||
return this.cache[level];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log 'msg % args' with severity 'DEBUG'
|
|
||||||
*
|
|
||||||
* To pass exception information, use the keyword argument exc_info with
|
|
||||||
* a true value, e.g.
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* logger.debug("Houston, we have a thorny problem", { exc_info: true })
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public debug(msg: string, options?: LogOptions) {
|
|
||||||
if (this.isEnabledFor(DEBUG)) { this._log(DEBUG, msg, options) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log 'msg % args' with severity 'INFO'
|
|
||||||
*/
|
|
||||||
public info(msg: string, options?: LogOptions) {
|
|
||||||
if (this.isEnabledFor(INFO)) { this._log(INFO, msg, options) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log 'msg % args' with severity 'WARNING'
|
|
||||||
*/
|
|
||||||
public warning(msg: string, options?: LogOptions) {
|
|
||||||
if (this.isEnabledFor(WARNING)) { this._log(WARNING, msg, options) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log 'msg % args' with severity 'ERROR'
|
|
||||||
*/
|
|
||||||
public error(msg: string, options?: LogOptions) {
|
|
||||||
if (this.isEnabledFor(ERROR)) { this._log(ERROR, msg, options) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log 'msg % args' with severity 'CRITICAL'
|
|
||||||
*/
|
|
||||||
public critical(msg: string, options?: LogOptions) {
|
|
||||||
if (this.isEnabledFor(CRITICAL)) { this._log(CRITICAL, msg, options) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A factory method which can be overriden in subclasses to create
|
|
||||||
* specialized LogRecords.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
protected makeRecord(
|
|
||||||
name: string,
|
|
||||||
level: LogLevel,
|
|
||||||
msg: string,
|
|
||||||
options: LogOptions,
|
|
||||||
): LogRecord {
|
|
||||||
|
|
||||||
var recordOptions: LogRecordOptions = {
|
|
||||||
level: level,
|
|
||||||
msg: msg,
|
|
||||||
};
|
|
||||||
|
|
||||||
var rv = logRecordFactory(name, recordOptions);
|
|
||||||
|
|
||||||
if (options.extra !== null) {
|
|
||||||
Object.entries(options.extra!).forEach((item) => {
|
|
||||||
|
|
||||||
var [k, v] = item;
|
|
||||||
|
|
||||||
if (['message', 'asctime'].includes(k) ||
|
|
||||||
Object.keys(rv).includes(k)) {
|
|
||||||
throw new KeyError(`attempt to overwrite ${k} in LogRecord`)
|
|
||||||
}
|
|
||||||
|
|
||||||
(rv as any)[k] = options.extra![k as string] as any
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Low-level logging routine which creates a LogRecord and then calls the
|
|
||||||
* handlers of this logger to handle the record.
|
|
||||||
*/
|
|
||||||
protected _log(level: LogLevel, msg: string, options?: LogOptions) {
|
|
||||||
options = options ?? DEFAULT_LOG_OPTIONS;
|
|
||||||
options = { ...DEFAULT_LOG_OPTIONS, ...options };
|
|
||||||
|
|
||||||
var sinfo=null;
|
|
||||||
|
|
||||||
if (options!.excInfo !== null) {
|
|
||||||
if (options!.excInfo instanceof Error) {
|
|
||||||
var excInfo: ExecutionInfo = [
|
|
||||||
typeof options!.excInfo,
|
|
||||||
options!.excInfo,
|
|
||||||
options!.excInfo.stack!
|
|
||||||
]
|
|
||||||
}
|
|
||||||
else if (!(options!.excInfo instanceof Array)) {
|
|
||||||
throw new NotImplementedError("would try to get the callee stack from the system. Probably will use stacktrace.js as this needs to be implemented browser-specific.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const record = this.makeRecord(this.scope, level, msg, options);
|
|
||||||
this.handle(this.scope, record);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call the handlers for the specified record.
|
|
||||||
*
|
|
||||||
* This method is used for unpickled records received from a socket, as well
|
|
||||||
* as those created locally. Logger-level filtering is applied.
|
|
||||||
*/
|
|
||||||
protected handle(scope: string, record: LogRecord) {
|
|
||||||
if (this.disabled) { return }
|
|
||||||
var maybeRecord = this.filter(record);
|
|
||||||
if (!maybeRecord) { return }
|
|
||||||
if ((maybeRecord as any) instanceof LogRecord) { record = maybeRecord }
|
|
||||||
this.callHandlers(record)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pass a record to all relevant handlers.
|
|
||||||
*
|
|
||||||
* Loop through all handlers for this logger and its parents n the logger
|
|
||||||
* hierarchy. If no handler was found, output a one-off error message to
|
|
||||||
* sys.stderr. Stop searching up the hierarchy whenever a logger with the
|
|
||||||
* "propagate" attribute set to zero is found - that will be the last logger
|
|
||||||
* whose handlers are called.
|
|
||||||
*/
|
|
||||||
protected callHandlers(record: LogRecord) {
|
|
||||||
var c: Logger|null = this;
|
|
||||||
var found = 0;
|
|
||||||
|
|
||||||
while (c) {
|
|
||||||
for (var i = 0; i < c.handlers.length; i += 1) {
|
|
||||||
let hdlr = c.handlers[i];
|
|
||||||
|
|
||||||
found = found + 1;
|
|
||||||
|
|
||||||
if (record.levelno >= hdlr.level) { hdlr.handle(record) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!c.propagate) { c = null }
|
|
||||||
else { c = c.parent }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found == 0) {
|
|
||||||
if (lastResort) {
|
|
||||||
if (record.levelno >= lastResort.level) {
|
|
||||||
lastResort.handle(record)
|
|
||||||
}
|
|
||||||
else if (throwErrors && (this.manager && !this.manager.emittedNoHandlerWarning)) {
|
|
||||||
console.error(
|
|
||||||
`No handlers could be found for logger ${this.scope}`
|
|
||||||
);
|
|
||||||
|
|
||||||
this.manager.emittedNoHandlerWarning = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear() {
|
|
||||||
for (var property in this.cache) delete this.cache[property];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified handler from this logger.
|
|
||||||
*/
|
|
||||||
public addHandler(hdlr: Handler) {
|
|
||||||
const i = this.handlers.indexOf(hdlr);
|
|
||||||
if (i === -1) { this.handlers.push(hdlr) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified handler from this logger.
|
|
||||||
*/
|
|
||||||
public removeHandler(hdlr: Handler) {
|
|
||||||
const i = this.handlers.indexOf(hdlr);
|
|
||||||
if (i !== -1) { delete this.handlers[i] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A root logger is not that different to any other logger, except that it must
|
|
||||||
* have a logging level and there is only one instance of in a manager's
|
|
||||||
* hierarchy.
|
|
||||||
*/
|
|
||||||
export class RootLogger extends Logger {
|
|
||||||
|
|
||||||
constructor(level: LogLevel) {
|
|
||||||
super('root', level);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* root logger (singleton)
|
|
||||||
*/
|
|
||||||
export const ROOT = new RootLogger(WARNING);
|
|
||||||
185
src/manager.ts
185
src/manager.ts
|
|
@ -1,185 +0,0 @@
|
||||||
import {
|
|
||||||
Logger,
|
|
||||||
LoggerClass,
|
|
||||||
RootLogger,
|
|
||||||
ROOT,
|
|
||||||
} from './logger';
|
|
||||||
import { LogRecordFactory } from './log-record';
|
|
||||||
import {
|
|
||||||
LogLevel,
|
|
||||||
NOTSET,
|
|
||||||
checkLevel,
|
|
||||||
} from './log-level'
|
|
||||||
|
|
||||||
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
// Manager classes and functions
|
|
||||||
//---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
var loggerClass = Logger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Placeholder instance
|
|
||||||
*/
|
|
||||||
class Placeholder {
|
|
||||||
public loggers: Logger[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* initialize with the specified logger being a child of this placeholder.
|
|
||||||
*/
|
|
||||||
constructor(logger: Logger) { this.push(logger) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add the specified logger as a child of this placeholder
|
|
||||||
*/
|
|
||||||
public push(logger: Logger) {
|
|
||||||
if (!this.loggers.includes(logger)) { this.loggers.push(logger) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* There is [under normal circumstances] just one Manager intance, which holds
|
|
||||||
* the hierarchy of loggers.
|
|
||||||
*/
|
|
||||||
export class Manager {
|
|
||||||
public readonly root: RootLogger;
|
|
||||||
protected _disable: number = 0;
|
|
||||||
public emittedNoHandlerWarning: boolean = false;
|
|
||||||
protected loggers: {[key: string]: Logger} = {};
|
|
||||||
protected _loggerClass: LoggerClass|null = null;
|
|
||||||
protected _logRecordFactory: LogRecordFactory|null = null;
|
|
||||||
|
|
||||||
public get disable(): number { return this._disable }
|
|
||||||
|
|
||||||
public set disable(level: LogLevel) { this._disable = checkLevel(level) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the manager with the root node of the logger hierarchy
|
|
||||||
*/
|
|
||||||
constructor(root: RootLogger) {
|
|
||||||
this.root = root;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a logger with the specified name (scope name), creating it, if it
|
|
||||||
* does not yet exist. This name is a dot-separated hierarchical name, such
|
|
||||||
* as "a", "a.b", "a.b.c" or similar.
|
|
||||||
*
|
|
||||||
* If a PlaceHolder existed for the specified name [i.e. the logger didn't
|
|
||||||
* exist but a child of it did], replace it with the created logger and fix
|
|
||||||
* up the parent/child references which pointed to the placeholder to now
|
|
||||||
* point to the logger.
|
|
||||||
*/
|
|
||||||
getLogger(scope: string): Logger {
|
|
||||||
let rv: Logger;
|
|
||||||
|
|
||||||
if (scope in this.loggers) {
|
|
||||||
const existing = this.loggers[scope];
|
|
||||||
|
|
||||||
if (existing instanceof Placeholder) {
|
|
||||||
rv = new (this._loggerClass ?? loggerClass)(scope, NOTSET);
|
|
||||||
rv.manager = this;
|
|
||||||
this.loggers[scope] = rv;
|
|
||||||
this._fixupChildren(existing, rv);
|
|
||||||
this._fixupParents(rv);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rv = existing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rv = new (this._loggerClass ?? loggerClass)(scope, NOTSET);
|
|
||||||
rv.manager = this;
|
|
||||||
this.loggers[scope] = rv;
|
|
||||||
this._fixupParents(rv);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure that there are either loggers or placeholders all the way from
|
|
||||||
* the specified logger to the root of the logger hierarchy.
|
|
||||||
*/
|
|
||||||
protected _fixupParents(logger: Logger) {
|
|
||||||
const name = logger.scope;
|
|
||||||
let i = name.lastIndexOf('.');
|
|
||||||
let rv: Logger | null = null;
|
|
||||||
|
|
||||||
while (i > 0 && !rv) {
|
|
||||||
const substr = name.substring(0, i);
|
|
||||||
|
|
||||||
if (!(substr in this.loggers)) {
|
|
||||||
this.loggers[substr] = new Placeholder(logger) as unknown as Logger;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const obj = this.loggers[substr];
|
|
||||||
|
|
||||||
if (obj instanceof Placeholder) {
|
|
||||||
obj.push(logger);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rv = obj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i = name.lastIndexOf('.', i - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
(logger as unknown as { parent: Logger }).parent = rv ?? this.root;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure that children of the placeholder ph are connected to the
|
|
||||||
* specified logger.
|
|
||||||
*/
|
|
||||||
protected _fixupChildren(ph: Placeholder, logger: Logger) {
|
|
||||||
const name = logger.scope;
|
|
||||||
|
|
||||||
for (const c of ph.loggers) {
|
|
||||||
if (c.parent!.scope.substring(0, name.length) !== name) {
|
|
||||||
(logger as unknown as { parent: Logger }).parent = c.parent!;
|
|
||||||
(c as unknown as { parent: Logger }).parent = logger;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the class to be used when instantiating a logger with this Manager.
|
|
||||||
*/
|
|
||||||
set loggerClass(class_: LoggerClass) {
|
|
||||||
if (class_ !== Logger) {
|
|
||||||
if (!(class_.prototype instanceof Logger)) {
|
|
||||||
throw new TypeError("logger not derived from logging.Logger: ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._loggerClass = class_;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the factory to be used when instantiating a log record with this
|
|
||||||
* Manager.
|
|
||||||
*/
|
|
||||||
set logRecordFactory(factory: LogRecordFactory) {
|
|
||||||
this._logRecordFactory = factory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* clear the cache for all loggers in loggerDict
|
|
||||||
*/
|
|
||||||
public clear() {
|
|
||||||
Object.values(this.loggers).forEach((logger) => {
|
|
||||||
if (!(logger instanceof Placeholder)) {
|
|
||||||
logger.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* log manager (singleton)
|
|
||||||
*/
|
|
||||||
export const MANAGER = new Manager(ROOT);
|
|
||||||
86
tests/test.ts
Normal file
86
tests/test.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import {expect, jest, test} from '@jest/globals';
|
||||||
|
|
||||||
|
describe('Logger', () => {
|
||||||
|
it('can be instantiated', () => {
|
||||||
|
//const logger = new logging.Logger('test', 0);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLevelName', () => {
|
||||||
|
var logging: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// there are a couple of singletons, which I'm not yet sure if they need
|
||||||
|
// to be reloaded for every test case
|
||||||
|
logging = require('../src');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('numeric to textual representation of built-ins', () => {
|
||||||
|
expect(
|
||||||
|
logging.getLevelName(logging.CRITICAL)
|
||||||
|
).toBe('CRITICAL');
|
||||||
|
expect(
|
||||||
|
logging.getLevelName(logging.FATAL)
|
||||||
|
).toBe('CRITICAL');
|
||||||
|
expect(
|
||||||
|
logging.getLevelName(logging.ERROR)
|
||||||
|
).toBe('ERROR');
|
||||||
|
expect(
|
||||||
|
logging.getLevelName(logging.WARNING)
|
||||||
|
).toBe('WARNING');
|
||||||
|
expect(
|
||||||
|
logging.getLevelName(logging.WARN)
|
||||||
|
).toBe('WARNING');
|
||||||
|
expect(
|
||||||
|
logging.getLevelName(logging.INFO)
|
||||||
|
).toBe('INFO');
|
||||||
|
expect(
|
||||||
|
logging.getLevelName(logging.DEBUG)
|
||||||
|
).toBe('DEBUG');
|
||||||
|
expect(
|
||||||
|
logging.getLevelName(logging.NOTSET)
|
||||||
|
).toBe('NOTSET');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('textual to numeric representation of built-ins', () => {
|
||||||
|
expect(
|
||||||
|
logging.getLevelName('CRITICAL')
|
||||||
|
).toBe(logging.CRITICAL);
|
||||||
|
expect(
|
||||||
|
logging.getLevelName('FATAL')
|
||||||
|
).toBe(`Level FATAL`);
|
||||||
|
expect(
|
||||||
|
logging.getLevelName('ERROR')
|
||||||
|
).toBe(logging.ERROR);
|
||||||
|
expect(
|
||||||
|
logging.getLevelName('WARNING')
|
||||||
|
).toBe(logging.WARNING);
|
||||||
|
expect(
|
||||||
|
logging.getLevelName('WARN')
|
||||||
|
).toBe('Level WARN');
|
||||||
|
expect(
|
||||||
|
logging.getLevelName('INFO')
|
||||||
|
).toBe(logging.INFO);
|
||||||
|
expect(
|
||||||
|
logging.getLevelName('DEBUG')
|
||||||
|
).toBe(logging.DEBUG);
|
||||||
|
expect(
|
||||||
|
logging.getLevelName('NOTSET')
|
||||||
|
).toBe(logging.NOTSET);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('addLevelName', () => {
|
||||||
|
var logging: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
logging = require('../src');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('numeric to textual representation of built-ins', () => {
|
||||||
|
logging.addLevelName(80, 'FOOBAR');
|
||||||
|
expect(logging.getLevelName(80)).toBe('FOOBAR');
|
||||||
|
expect(logging.getLevelName('FOOBAR')).toBe(80);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
import {expect, jest, test} from '@jest/globals';
|
|
||||||
import { basicConfig } from '../../src/config';
|
|
||||||
import { MANAGER } from '../../src/manager';
|
|
||||||
import { Handler } from '../../src/handler';
|
|
||||||
import { LogRecord } from '../../src/log-record';
|
|
||||||
import { Formatter } from '../../src/formatter';
|
|
||||||
import { DEBUG, WARNING, ERROR } from '../../src/log-level';
|
|
||||||
describe('basicConfig', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
MANAGER.root.handlers.splice(0, MANAGER.root.handlers.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('adds a handler to the root logger', () => {
|
|
||||||
basicConfig({});
|
|
||||||
expect(MANAGER.root.handlers.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sets root logger level', () => {
|
|
||||||
basicConfig({ level: DEBUG });
|
|
||||||
expect(MANAGER.root.level).toBe(DEBUG);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('does nothing when handlers already exist and force is false', () => {
|
|
||||||
basicConfig({ level: DEBUG });
|
|
||||||
const handlerCount = MANAGER.root.handlers.length;
|
|
||||||
basicConfig({ level: ERROR });
|
|
||||||
expect(MANAGER.root.handlers.length).toBe(handlerCount);
|
|
||||||
expect(MANAGER.root.level).toBe(DEBUG);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('replaces handlers when force is true', () => {
|
|
||||||
basicConfig({ level: DEBUG });
|
|
||||||
basicConfig({ level: ERROR, force: true });
|
|
||||||
expect(MANAGER.root.level).toBe(ERROR);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws on invalid style', () => {
|
|
||||||
expect(() => {
|
|
||||||
basicConfig({ style: 'X' });
|
|
||||||
}).toThrow('style must be one of');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws when stream and filename both specified', () => {
|
|
||||||
expect(() => {
|
|
||||||
basicConfig({
|
|
||||||
stream: process.stderr,
|
|
||||||
filename: 'test.log',
|
|
||||||
});
|
|
||||||
}).toThrow('should not be specified together');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws when handlers and stream both specified', () => {
|
|
||||||
class TestHandler extends Handler {
|
|
||||||
emit(record: LogRecord) {}
|
|
||||||
}
|
|
||||||
expect(() => {
|
|
||||||
basicConfig({
|
|
||||||
handlers: [new TestHandler()],
|
|
||||||
stream: process.stderr,
|
|
||||||
});
|
|
||||||
}).toThrow('should not be specified together');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uses provided handlers', () => {
|
|
||||||
class TestHandler extends Handler {
|
|
||||||
emit(record: LogRecord) {}
|
|
||||||
}
|
|
||||||
const handler = new TestHandler();
|
|
||||||
basicConfig({ handlers: [handler] });
|
|
||||||
expect(MANAGER.root.handlers).toContain(handler);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('assigns formatter to handlers without one', () => {
|
|
||||||
class TestHandler extends Handler {
|
|
||||||
emit(record: LogRecord) {}
|
|
||||||
}
|
|
||||||
const handler = new TestHandler();
|
|
||||||
expect(handler.formatter).toBeNull();
|
|
||||||
basicConfig({ handlers: [handler] });
|
|
||||||
expect(handler.formatter).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('preserves existing formatter on handlers', () => {
|
|
||||||
class TestHandler extends Handler {
|
|
||||||
emit(record: LogRecord) {}
|
|
||||||
}
|
|
||||||
const handler = new TestHandler();
|
|
||||||
const fmt = new Formatter({ fmt: '%(message)s' });
|
|
||||||
handler.formatter = fmt;
|
|
||||||
basicConfig({ handlers: [handler] });
|
|
||||||
expect(handler.formatter).toBe(fmt);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
import {expect, jest, test} from '@jest/globals';
|
|
||||||
import { Formatter, DEFAULT_FORMATTER } from '../../src/formatter';
|
|
||||||
import { LogRecord } from '../../src/log-record';
|
|
||||||
import { DEBUG, WARNING, INFO } from '../../src/log-level';
|
|
||||||
import { MyError } from '../../src/helper/error';
|
|
||||||
|
|
||||||
function makeRecord(level: number, msg: string): LogRecord {
|
|
||||||
return new LogRecord('test.module', { level, msg });
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Formatter', () => {
|
|
||||||
describe('constructor', () => {
|
|
||||||
test('uses default format when no options given', () => {
|
|
||||||
const fmt = new Formatter();
|
|
||||||
const record = makeRecord(WARNING, 'hello');
|
|
||||||
const result = fmt.format(record);
|
|
||||||
expect(result).toBe('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accepts custom format string', () => {
|
|
||||||
const fmt = new Formatter({ fmt: '%(levelname)s - %(message)s' });
|
|
||||||
const record = makeRecord(DEBUG, 'debug msg');
|
|
||||||
expect(fmt.format(record)).toBe('DEBUG - debug msg');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws on invalid style', () => {
|
|
||||||
expect(() => new Formatter({ style: '{' })).toThrow('style must be one of');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('format', () => {
|
|
||||||
test('substitutes %(name)s with logger scope', () => {
|
|
||||||
const fmt = new Formatter({ fmt: '[%(name)s] %(message)s' });
|
|
||||||
const record = makeRecord(INFO, 'test');
|
|
||||||
expect(fmt.format(record)).toBe('[test.module] test');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('substitutes %(levelno)d with numeric level', () => {
|
|
||||||
const fmt = new Formatter({ fmt: '%(levelno)d: %(message)s' });
|
|
||||||
const record = makeRecord(WARNING, 'warn');
|
|
||||||
expect(fmt.format(record)).toBe('30: warn');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('substitutes %(levelname)s with level name', () => {
|
|
||||||
const fmt = new Formatter({ fmt: '%(levelname)s %(message)s' });
|
|
||||||
const record = makeRecord(DEBUG, 'msg');
|
|
||||||
expect(fmt.format(record)).toBe('DEBUG msg');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles multiple placeholders', () => {
|
|
||||||
const fmt = new Formatter({
|
|
||||||
fmt: '%(levelname)s:%(name)s:%(levelno)d:%(message)s'
|
|
||||||
});
|
|
||||||
const record = makeRecord(WARNING, 'multi');
|
|
||||||
expect(fmt.format(record)).toBe('WARNING:test.module:30:multi');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws on unknown field', () => {
|
|
||||||
const fmt = new Formatter({ fmt: '%(nonexistent)s' });
|
|
||||||
const record = makeRecord(DEBUG, 'test');
|
|
||||||
expect(() => fmt.format(record)).toThrow('formatting field not found');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('populates asctime when format uses it', () => {
|
|
||||||
const fmt = new Formatter({
|
|
||||||
fmt: '%(asctime)s %(message)s'
|
|
||||||
});
|
|
||||||
const record = makeRecord(INFO, 'timed');
|
|
||||||
const result = fmt.format(record);
|
|
||||||
expect(result).toContain('timed');
|
|
||||||
expect(record.asctime.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('does not populate asctime when format does not use it', () => {
|
|
||||||
const fmt = new Formatter({ fmt: '%(message)s' });
|
|
||||||
const record = makeRecord(INFO, 'no time');
|
|
||||||
fmt.format(record);
|
|
||||||
expect(record.asctime).toBe('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatTime', () => {
|
|
||||||
test('returns ISO8601-like string by default', () => {
|
|
||||||
const fmt = new Formatter();
|
|
||||||
const record = makeRecord(INFO, 'test');
|
|
||||||
const result = fmt.formatTime(record);
|
|
||||||
expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uses custom datefmt with strftime tokens', () => {
|
|
||||||
const fmt = new Formatter();
|
|
||||||
const record = makeRecord(INFO, 'test');
|
|
||||||
const result = fmt.formatTime(record, '%Y/%m/%d');
|
|
||||||
expect(result).toMatch(/^\d{4}\/\d{2}\/\d{2}$/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatError', () => {
|
|
||||||
test('returns stack trace when available', () => {
|
|
||||||
const fmt = new Formatter();
|
|
||||||
const err = new MyError('test error');
|
|
||||||
const result = fmt.formatError(err);
|
|
||||||
expect(result).toContain('test error');
|
|
||||||
expect(result).toContain('\n');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns string representation when no stack', () => {
|
|
||||||
const fmt = new Formatter();
|
|
||||||
const err = new MyError('no stack');
|
|
||||||
err.stack = undefined;
|
|
||||||
const result = fmt.formatError(err);
|
|
||||||
expect(result).toContain('no stack');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('usesTime', () => {
|
|
||||||
test('returns true when format contains asctime', () => {
|
|
||||||
const fmt = new Formatter({ fmt: '%(asctime)s %(message)s' });
|
|
||||||
expect(fmt.usesTime()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns false when format does not contain asctime', () => {
|
|
||||||
const fmt = new Formatter({ fmt: '%(message)s' });
|
|
||||||
expect(fmt.usesTime()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('LogRecord.getMessage', () => {
|
|
||||||
test('returns the message string', () => {
|
|
||||||
const record = makeRecord(INFO, 'plain message');
|
|
||||||
expect(record.getMessage()).toBe('plain message');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('substitutes %s args into message', () => {
|
|
||||||
const record = new LogRecord('test', {
|
|
||||||
level: INFO,
|
|
||||||
msg: 'hello %s, you have %s items',
|
|
||||||
args: ['world', '5'],
|
|
||||||
});
|
|
||||||
expect(record.getMessage()).toBe('hello world, you have 5 items');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
import {expect, jest, test} from '@jest/globals';
|
|
||||||
import {
|
|
||||||
Handler,
|
|
||||||
StreamHandler,
|
|
||||||
ConsoleHandler,
|
|
||||||
StderrHandler,
|
|
||||||
FileHandler,
|
|
||||||
} from '../../src/handler';
|
|
||||||
import { Formatter, DEFAULT_FORMATTER } from '../../src/formatter';
|
|
||||||
import { LogRecord } from '../../src/log-record';
|
|
||||||
import { Writable } from '../../src/helper/stream';
|
|
||||||
import { DEBUG, INFO, WARNING, ERROR, CRITICAL, NOTSET } from '../../src/log-level';
|
|
||||||
|
|
||||||
function makeRecord(level: number, msg: string): LogRecord {
|
|
||||||
return new LogRecord('test', { level, msg });
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Handler', () => {
|
|
||||||
describe('constructor', () => {
|
|
||||||
test('defaults to NOTSET level', () => {
|
|
||||||
const h = new Handler();
|
|
||||||
expect(h.level).toBe(NOTSET);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accepts a level argument', () => {
|
|
||||||
const h = new Handler(WARNING);
|
|
||||||
expect(h.level).toBe(WARNING);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('level setter', () => {
|
|
||||||
test('sets the level without infinite recursion', () => {
|
|
||||||
const h = new Handler();
|
|
||||||
h.level = ERROR;
|
|
||||||
expect(h.level).toBe(ERROR);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('formatter', () => {
|
|
||||||
test('getter returns null when no formatter set', () => {
|
|
||||||
const h = new Handler();
|
|
||||||
expect(h.formatter).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getter returns assigned formatter', () => {
|
|
||||||
const h = new Handler();
|
|
||||||
const fmt = new Formatter();
|
|
||||||
h.formatter = fmt;
|
|
||||||
expect(h.formatter).toBe(fmt);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('format', () => {
|
|
||||||
test('uses DEFAULT_FORMATTER when no formatter assigned', () => {
|
|
||||||
const h = new Handler();
|
|
||||||
const record = makeRecord(DEBUG, 'hello');
|
|
||||||
const result = h.format(record);
|
|
||||||
expect(typeof result).toBe('string');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns a string', () => {
|
|
||||||
const h = new Handler();
|
|
||||||
const record = makeRecord(WARNING, 'test message');
|
|
||||||
expect(typeof h.format(record)).toBe('string');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('emit', () => {
|
|
||||||
test('throws NotImplementedError on base class', () => {
|
|
||||||
const h = new Handler();
|
|
||||||
const record = makeRecord(DEBUG, 'hello');
|
|
||||||
expect(() => h.emit(record)).toThrow('emit must be implemented');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('close', () => {
|
|
||||||
test('sets closed to true', () => {
|
|
||||||
const h = new Handler();
|
|
||||||
expect(h.closed).toBe(false);
|
|
||||||
h.close();
|
|
||||||
expect(h.closed).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleError', () => {
|
|
||||||
test('does not throw', () => {
|
|
||||||
const h = new Handler();
|
|
||||||
const record = makeRecord(DEBUG, 'test');
|
|
||||||
expect(() => h.handleError(record)).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('StreamHandler', () => {
|
|
||||||
test('writes formatted output to the stream', () => {
|
|
||||||
const written: string[] = [];
|
|
||||||
const stream: Writable = { write: (data: string) => { written.push(data) } };
|
|
||||||
const h = new StreamHandler(stream);
|
|
||||||
const record = makeRecord(WARNING, 'stream test');
|
|
||||||
h.emit(record);
|
|
||||||
expect(written.length).toBe(1);
|
|
||||||
expect(written[0]).toContain('stream test');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uses default stream when none provided', () => {
|
|
||||||
const h = new StreamHandler();
|
|
||||||
const record = makeRecord(DEBUG, 'default stream');
|
|
||||||
expect(() => h.emit(record)).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('calls handleError on emit failure', () => {
|
|
||||||
const stream: Writable = {
|
|
||||||
write: () => { throw new Error('write failed') }
|
|
||||||
};
|
|
||||||
const h = new StreamHandler(stream);
|
|
||||||
const record = makeRecord(DEBUG, 'fail');
|
|
||||||
expect(() => h.emit(record)).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ConsoleHandler', () => {
|
|
||||||
let origLog: typeof console.log;
|
|
||||||
let origWarn: typeof console.warn;
|
|
||||||
let origError: typeof console.error;
|
|
||||||
let logged: string[];
|
|
||||||
let warned: string[];
|
|
||||||
let errored: string[];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logged = [];
|
|
||||||
warned = [];
|
|
||||||
errored = [];
|
|
||||||
origLog = console.log;
|
|
||||||
origWarn = console.warn;
|
|
||||||
origError = console.error;
|
|
||||||
console.log = (...args: any[]) => { logged.push(args.join(' ')) };
|
|
||||||
console.warn = (...args: any[]) => { warned.push(args.join(' ')) };
|
|
||||||
console.error = (...args: any[]) => { errored.push(args.join(' ')) };
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
console.log = origLog;
|
|
||||||
console.warn = origWarn;
|
|
||||||
console.error = origError;
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uses console.error for ERROR level', () => {
|
|
||||||
const h = new ConsoleHandler();
|
|
||||||
h.emit(makeRecord(ERROR, 'error msg'));
|
|
||||||
expect(errored.length).toBe(1);
|
|
||||||
expect(errored[0]).toContain('error msg');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uses console.error for CRITICAL level', () => {
|
|
||||||
const h = new ConsoleHandler();
|
|
||||||
h.emit(makeRecord(CRITICAL, 'critical msg'));
|
|
||||||
expect(errored.length).toBe(1);
|
|
||||||
expect(errored[0]).toContain('critical msg');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uses console.warn for WARNING level', () => {
|
|
||||||
const h = new ConsoleHandler();
|
|
||||||
h.emit(makeRecord(WARNING, 'warn msg'));
|
|
||||||
expect(warned.length).toBe(1);
|
|
||||||
expect(warned[0]).toContain('warn msg');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uses console.log for INFO level', () => {
|
|
||||||
const h = new ConsoleHandler();
|
|
||||||
h.emit(makeRecord(INFO, 'info msg'));
|
|
||||||
expect(logged.length).toBe(1);
|
|
||||||
expect(logged[0]).toContain('info msg');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('uses console.log for DEBUG level', () => {
|
|
||||||
const h = new ConsoleHandler();
|
|
||||||
h.emit(makeRecord(DEBUG, 'debug msg'));
|
|
||||||
expect(logged.length).toBe(1);
|
|
||||||
expect(logged[0]).toContain('debug msg');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('StderrHandler', () => {
|
|
||||||
test('accepts a level', () => {
|
|
||||||
const h = new StderrHandler(ERROR);
|
|
||||||
expect(h.level).toBe(ERROR);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('emits via console.error', () => {
|
|
||||||
const errored: string[] = [];
|
|
||||||
const origError = console.error;
|
|
||||||
console.error = (...args: any[]) => { errored.push(args.join(' ')) };
|
|
||||||
try {
|
|
||||||
const h = new StderrHandler(DEBUG);
|
|
||||||
h.emit(makeRecord(WARNING, 'stderr test'));
|
|
||||||
expect(errored.length).toBe(1);
|
|
||||||
expect(errored[0]).toContain('stderr test');
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
console.error = origError;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('FileHandler', () => {
|
|
||||||
test('throws NotImplementedError on construction', () => {
|
|
||||||
expect(() => new FileHandler({
|
|
||||||
filename: 'test.log'
|
|
||||||
})).toThrow('not available in browser');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
import {expect, jest, test, beforeEach, afterEach} from '@jest/globals';
|
|
||||||
import { LocalStorageHandler } from '../../src/handler';
|
|
||||||
import { LogRecord } from '../../src/log-record';
|
|
||||||
import { DEBUG, WARNING, ERROR } from '../../src/log-level';
|
|
||||||
|
|
||||||
// mock localStorage for Node/Jest environment
|
|
||||||
const store: {[key: string]: string} = {};
|
|
||||||
const localStorageMock = {
|
|
||||||
getItem: (key: string): string | null => store[key] ?? null,
|
|
||||||
setItem: (key: string, value: string) => { store[key] = value },
|
|
||||||
removeItem: (key: string) => { delete store[key] },
|
|
||||||
};
|
|
||||||
|
|
||||||
function clearStore() {
|
|
||||||
for (const key of Object.keys(store)) { delete store[key] }
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeRecord(level: number, msg: string): LogRecord {
|
|
||||||
return new LogRecord('test', { level, msg });
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
clearStore();
|
|
||||||
(globalThis as any).localStorage = localStorageMock;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
clearStore();
|
|
||||||
delete (globalThis as any).localStorage;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('LocalStorageHandler', () => {
|
|
||||||
describe('constructor', () => {
|
|
||||||
test('uses default key and limits', () => {
|
|
||||||
const h = new LocalStorageHandler();
|
|
||||||
expect(h.getEntries()).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws when localStorage is unavailable', () => {
|
|
||||||
delete (globalThis as any).localStorage;
|
|
||||||
expect(() => new LocalStorageHandler()).toThrow(
|
|
||||||
'requires a browser environment'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accepts custom options', () => {
|
|
||||||
const h = new LocalStorageHandler({
|
|
||||||
key: 'custom-log',
|
|
||||||
maxEntries: 50,
|
|
||||||
maxBytes: 4096,
|
|
||||||
});
|
|
||||||
h.emit(makeRecord(DEBUG, 'test'));
|
|
||||||
expect(store['custom-log']).toBeDefined();
|
|
||||||
expect(store['esm-logging']).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('emit', () => {
|
|
||||||
test('stores formatted log entry', () => {
|
|
||||||
const h = new LocalStorageHandler();
|
|
||||||
h.emit(makeRecord(WARNING, 'hello'));
|
|
||||||
const entries = h.getEntries();
|
|
||||||
expect(entries.length).toBe(1);
|
|
||||||
expect(entries[0]).toContain('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('appends multiple entries', () => {
|
|
||||||
const h = new LocalStorageHandler();
|
|
||||||
h.emit(makeRecord(DEBUG, 'first'));
|
|
||||||
h.emit(makeRecord(DEBUG, 'second'));
|
|
||||||
h.emit(makeRecord(DEBUG, 'third'));
|
|
||||||
expect(h.getEntries().length).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('persists across handler instances with same key', () => {
|
|
||||||
const h1 = new LocalStorageHandler({ key: 'shared' });
|
|
||||||
h1.emit(makeRecord(DEBUG, 'from h1'));
|
|
||||||
|
|
||||||
const h2 = new LocalStorageHandler({ key: 'shared' });
|
|
||||||
const entries = h2.getEntries();
|
|
||||||
expect(entries.length).toBe(1);
|
|
||||||
expect(entries[0]).toContain('from h1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('rotation by entry count', () => {
|
|
||||||
test('discards oldest entries when maxEntries exceeded', () => {
|
|
||||||
const h = new LocalStorageHandler({ maxEntries: 3 });
|
|
||||||
h.emit(makeRecord(DEBUG, 'msg-1'));
|
|
||||||
h.emit(makeRecord(DEBUG, 'msg-2'));
|
|
||||||
h.emit(makeRecord(DEBUG, 'msg-3'));
|
|
||||||
h.emit(makeRecord(DEBUG, 'msg-4'));
|
|
||||||
|
|
||||||
const entries = h.getEntries();
|
|
||||||
expect(entries.length).toBe(3);
|
|
||||||
expect(entries[0]).toContain('msg-2');
|
|
||||||
expect(entries[2]).toContain('msg-4');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('maxEntries of 1 keeps only the latest', () => {
|
|
||||||
const h = new LocalStorageHandler({ maxEntries: 1 });
|
|
||||||
h.emit(makeRecord(DEBUG, 'old'));
|
|
||||||
h.emit(makeRecord(DEBUG, 'new'));
|
|
||||||
|
|
||||||
const entries = h.getEntries();
|
|
||||||
expect(entries.length).toBe(1);
|
|
||||||
expect(entries[0]).toContain('new');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('rotation by byte size', () => {
|
|
||||||
test('discards oldest entries when maxBytes exceeded', () => {
|
|
||||||
const h = new LocalStorageHandler({ maxEntries: 0, maxBytes: 100 });
|
|
||||||
// emit entries until we exceed the limit
|
|
||||||
for (let i = 0; i < 20; i++) {
|
|
||||||
h.emit(makeRecord(DEBUG, `message-${i}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = h.getEntries();
|
|
||||||
const serialized = JSON.stringify(entries);
|
|
||||||
expect(serialized.length).toBeLessThanOrEqual(100);
|
|
||||||
expect(entries.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getEntries', () => {
|
|
||||||
test('returns empty array when no entries', () => {
|
|
||||||
const h = new LocalStorageHandler();
|
|
||||||
expect(h.getEntries()).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns empty array on corrupt data', () => {
|
|
||||||
store['esm-logging'] = 'not valid json{{{';
|
|
||||||
const h = new LocalStorageHandler();
|
|
||||||
expect(h.getEntries()).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('clearEntries', () => {
|
|
||||||
test('removes all stored entries', () => {
|
|
||||||
const h = new LocalStorageHandler();
|
|
||||||
h.emit(makeRecord(DEBUG, 'to be cleared'));
|
|
||||||
expect(h.getEntries().length).toBe(1);
|
|
||||||
|
|
||||||
h.clearEntries();
|
|
||||||
expect(h.getEntries()).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('close', () => {
|
|
||||||
test('sets closed to true', () => {
|
|
||||||
const h = new LocalStorageHandler();
|
|
||||||
expect(h.closed).toBe(false);
|
|
||||||
h.close();
|
|
||||||
expect(h.closed).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import {expect, jest, test} from '@jest/globals';
|
|
||||||
import * as log_level from '../../src/log-level';
|
|
||||||
|
|
||||||
describe('Logger', () => {
|
|
||||||
it('can be instantiated', () => {
|
|
||||||
//const logger = new log_level.Logger('test', 0);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLevelName', () => {
|
|
||||||
it('numeric to textual representation of built-ins', () => {
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName(log_level.CRITICAL)
|
|
||||||
).toBe('CRITICAL');
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName(log_level.FATAL)
|
|
||||||
).toBe('CRITICAL');
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName(log_level.ERROR)
|
|
||||||
).toBe('ERROR');
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName(log_level.WARNING)
|
|
||||||
).toBe('WARNING');
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName(log_level.WARN)
|
|
||||||
).toBe('WARNING');
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName(log_level.INFO)
|
|
||||||
).toBe('INFO');
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName(log_level.DEBUG)
|
|
||||||
).toBe('DEBUG');
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName(log_level.NOTSET)
|
|
||||||
).toBe('NOTSET');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('textual to numeric representation of built-ins', () => {
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName('CRITICAL')
|
|
||||||
).toBe(log_level.CRITICAL);
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName('FATAL')
|
|
||||||
).toBe(`Level FATAL`);
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName('ERROR')
|
|
||||||
).toBe(log_level.ERROR);
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName('WARNING')
|
|
||||||
).toBe(log_level.WARNING);
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName('WARN')
|
|
||||||
).toBe('Level WARN');
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName('INFO')
|
|
||||||
).toBe(log_level.INFO);
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName('DEBUG')
|
|
||||||
).toBe(log_level.DEBUG);
|
|
||||||
expect(
|
|
||||||
log_level.getLevelName('NOTSET')
|
|
||||||
).toBe(log_level.NOTSET);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
describe('addLevelName', () => {
|
|
||||||
it('numeric to textual representation of built-ins', () => {
|
|
||||||
log_level.addLevelName(80, 'FOOBAR');
|
|
||||||
expect(log_level.getLevelName(80)).toBe('FOOBAR');
|
|
||||||
expect(log_level.getLevelName('FOOBAR')).toBe(80);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
import {expect, jest, test} from '@jest/globals';
|
|
||||||
import { Logger, RootLogger, ROOT } from '../../src/logger';
|
|
||||||
import { Handler } from '../../src/handler';
|
|
||||||
import { LogRecord } from '../../src/log-record';
|
|
||||||
import { Formatter } from '../../src/formatter';
|
|
||||||
import { Manager } from '../../src/manager';
|
|
||||||
import {
|
|
||||||
DEBUG,
|
|
||||||
INFO,
|
|
||||||
WARNING,
|
|
||||||
ERROR,
|
|
||||||
CRITICAL,
|
|
||||||
NOTSET,
|
|
||||||
} from '../../src/log-level';
|
|
||||||
import { MyError } from '../../src/helper/error';
|
|
||||||
|
|
||||||
describe('Logger', () => {
|
|
||||||
describe('constructor', () => {
|
|
||||||
test('sets scope and default level', () => {
|
|
||||||
const logger = new Logger('test');
|
|
||||||
expect(logger.scope).toBe('test');
|
|
||||||
expect(logger.level).toBe(NOTSET);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accepts explicit level', () => {
|
|
||||||
const logger = new Logger('test', WARNING);
|
|
||||||
expect(logger.level).toBe(WARNING);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('setLevel', () => {
|
|
||||||
test('changes the level', () => {
|
|
||||||
const logger = new Logger('test');
|
|
||||||
logger.setLevel(ERROR);
|
|
||||||
expect(logger.level).toBe(ERROR);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getEffectiveLevel', () => {
|
|
||||||
test('returns own level when set', () => {
|
|
||||||
const logger = new Logger('test', WARNING);
|
|
||||||
expect(logger.getEffectiveLevel()).toBe(WARNING);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns NOTSET when no level set and no parent', () => {
|
|
||||||
const logger = new Logger('test');
|
|
||||||
expect(logger.getEffectiveLevel()).toBe(NOTSET);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isEnabledFor', () => {
|
|
||||||
test('returns true when level meets effective level', () => {
|
|
||||||
const logger = new Logger('test', DEBUG);
|
|
||||||
expect(logger.isEnabledFor(WARNING)).toBe(true);
|
|
||||||
expect(logger.isEnabledFor(DEBUG)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns false when level is below effective level', () => {
|
|
||||||
const logger = new Logger('test', WARNING);
|
|
||||||
expect(logger.isEnabledFor(DEBUG)).toBe(false);
|
|
||||||
expect(logger.isEnabledFor(INFO)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('caches results for repeated calls', () => {
|
|
||||||
const logger = new Logger('test', WARNING);
|
|
||||||
const first = logger.isEnabledFor(DEBUG);
|
|
||||||
const second = logger.isEnabledFor(DEBUG);
|
|
||||||
expect(first).toBe(second);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('respects manager disable level', () => {
|
|
||||||
const root = new RootLogger(WARNING);
|
|
||||||
const manager = new Manager(root);
|
|
||||||
const logger = manager.getLogger('test');
|
|
||||||
logger.setLevel(DEBUG);
|
|
||||||
logger.manager = manager;
|
|
||||||
manager.disable = ERROR;
|
|
||||||
logger.clear();
|
|
||||||
expect(logger.isEnabledFor(DEBUG)).toBe(false);
|
|
||||||
expect(logger.isEnabledFor(WARNING)).toBe(false);
|
|
||||||
expect(logger.isEnabledFor(CRITICAL)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('manager property', () => {
|
|
||||||
test('starts as null', () => {
|
|
||||||
const logger = new Logger('test');
|
|
||||||
expect(logger.manager).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can be assigned once', () => {
|
|
||||||
const logger = new Logger('test');
|
|
||||||
const root = new RootLogger(WARNING);
|
|
||||||
const manager = new Manager(root);
|
|
||||||
logger.manager = manager;
|
|
||||||
expect(logger.manager).toBe(manager);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throws on second assignment', () => {
|
|
||||||
const logger = new Logger('test');
|
|
||||||
const root = new RootLogger(WARNING);
|
|
||||||
const manager = new Manager(root);
|
|
||||||
logger.manager = manager;
|
|
||||||
expect(() => { logger.manager = manager }).toThrow('logger can only be assigned to manager once');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('addHandler / removeHandler', () => {
|
|
||||||
test('adds and removes handlers', () => {
|
|
||||||
const logger = new Logger('test');
|
|
||||||
const handler = new Handler(DEBUG);
|
|
||||||
expect(logger.handlers.length).toBe(0);
|
|
||||||
logger.addHandler(handler);
|
|
||||||
expect(logger.handlers.length).toBe(1);
|
|
||||||
logger.removeHandler(handler);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('does not add duplicate handlers', () => {
|
|
||||||
const logger = new Logger('test');
|
|
||||||
const handler = new Handler(DEBUG);
|
|
||||||
logger.addHandler(handler);
|
|
||||||
logger.addHandler(handler);
|
|
||||||
expect(logger.handlers.length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('level methods', () => {
|
|
||||||
let logger: Logger;
|
|
||||||
let emitted: LogRecord[];
|
|
||||||
|
|
||||||
class TestHandler extends Handler {
|
|
||||||
emit(record: LogRecord) { emitted.push(record) }
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
logger = new Logger('test', DEBUG);
|
|
||||||
emitted = [];
|
|
||||||
logger.addHandler(new TestHandler(DEBUG));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('debug emits at DEBUG level', () => {
|
|
||||||
logger.debug('msg');
|
|
||||||
expect(emitted.length).toBe(1);
|
|
||||||
expect(emitted[0].levelno).toBe(DEBUG);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('info emits at INFO level', () => {
|
|
||||||
logger.info('msg');
|
|
||||||
expect(emitted.length).toBe(1);
|
|
||||||
expect(emitted[0].levelno).toBe(INFO);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('warning emits at WARNING level', () => {
|
|
||||||
logger.warning('msg');
|
|
||||||
expect(emitted.length).toBe(1);
|
|
||||||
expect(emitted[0].levelno).toBe(WARNING);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('error emits at ERROR level', () => {
|
|
||||||
logger.error('msg');
|
|
||||||
expect(emitted.length).toBe(1);
|
|
||||||
expect(emitted[0].levelno).toBe(ERROR);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('critical emits at CRITICAL level', () => {
|
|
||||||
logger.critical('msg');
|
|
||||||
expect(emitted.length).toBe(1);
|
|
||||||
expect(emitted[0].levelno).toBe(CRITICAL);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('level methods respect effective level', () => {
|
|
||||||
logger.setLevel(ERROR);
|
|
||||||
logger.clear();
|
|
||||||
logger.debug('no');
|
|
||||||
logger.info('no');
|
|
||||||
logger.warning('no');
|
|
||||||
expect(emitted.length).toBe(0);
|
|
||||||
logger.error('yes');
|
|
||||||
logger.critical('yes');
|
|
||||||
expect(emitted.length).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('makeRecord', () => {
|
|
||||||
test('throws KeyError when extra overwrites existing LogRecord key', () => {
|
|
||||||
const logger = new Logger('test', DEBUG);
|
|
||||||
expect(() => {
|
|
||||||
logger.debug('test', {
|
|
||||||
excInfo: null,
|
|
||||||
extra: { scope: 'override' },
|
|
||||||
stackInfo: false,
|
|
||||||
stackLevel: 1,
|
|
||||||
});
|
|
||||||
}).toThrow('attempt to overwrite scope in LogRecord');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('RootLogger', () => {
|
|
||||||
test('has scope "root"', () => {
|
|
||||||
const root = new RootLogger(WARNING);
|
|
||||||
expect(root.scope).toBe('root');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accepts a level', () => {
|
|
||||||
const root = new RootLogger(ERROR);
|
|
||||||
expect(root.level).toBe(ERROR);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
import {expect, jest, test} from '@jest/globals';
|
|
||||||
import { Manager } from '../../src/manager';
|
|
||||||
import { Logger, RootLogger } from '../../src/logger';
|
|
||||||
import { Handler } from '../../src/handler';
|
|
||||||
import { LogRecord } from '../../src/log-record';
|
|
||||||
import { WARNING, DEBUG, INFO, NOTSET } from '../../src/log-level';
|
|
||||||
|
|
||||||
function makeManager(): Manager {
|
|
||||||
return new Manager(new RootLogger(WARNING));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Manager', () => {
|
|
||||||
describe('constructor', () => {
|
|
||||||
test('stores the root logger', () => {
|
|
||||||
const root = new RootLogger(WARNING);
|
|
||||||
const manager = new Manager(root);
|
|
||||||
expect(manager.root).toBe(root);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLogger', () => {
|
|
||||||
test('returns a Logger instance', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const logger = manager.getLogger('app');
|
|
||||||
expect(logger).toBeInstanceOf(Logger);
|
|
||||||
expect(logger.scope).toBe('app');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns the same logger for the same scope', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const a = manager.getLogger('app');
|
|
||||||
const b = manager.getLogger('app');
|
|
||||||
expect(a).toBe(b);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns different loggers for different scopes', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const a = manager.getLogger('app');
|
|
||||||
const b = manager.getLogger('lib');
|
|
||||||
expect(a).not.toBe(b);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sets parent to root for top-level loggers', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const logger = manager.getLogger('app');
|
|
||||||
expect(logger.parent).toBe(manager.root);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sets parent to existing parent logger', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const parent = manager.getLogger('app');
|
|
||||||
const child = manager.getLogger('app.module');
|
|
||||||
expect(child.parent).toBe(parent);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('establishes hierarchy for deeply nested loggers', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const top = manager.getLogger('a');
|
|
||||||
const mid = manager.getLogger('a.b');
|
|
||||||
const leaf = manager.getLogger('a.b.c');
|
|
||||||
expect(leaf.parent).toBe(mid);
|
|
||||||
expect(mid.parent).toBe(top);
|
|
||||||
expect(top.parent).toBe(manager.root);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('creates placeholders for intermediate loggers', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const leaf = manager.getLogger('a.b.c');
|
|
||||||
expect(leaf.parent).toBe(manager.root);
|
|
||||||
|
|
||||||
const mid = manager.getLogger('a.b');
|
|
||||||
expect(leaf.parent).toBe(mid);
|
|
||||||
expect(mid.parent).toBe(manager.root);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fixes up children when intermediate logger is created', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const leaf = manager.getLogger('a.b.c');
|
|
||||||
const top = manager.getLogger('a');
|
|
||||||
|
|
||||||
expect(top.parent).toBe(manager.root);
|
|
||||||
|
|
||||||
const mid = manager.getLogger('a.b');
|
|
||||||
expect(leaf.parent).toBe(mid);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('disable', () => {
|
|
||||||
test('defaults to 0', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
expect(manager.disable).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can be set to a level', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
manager.disable = WARNING;
|
|
||||||
expect(manager.disable).toBe(WARNING);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('loggerClass', () => {
|
|
||||||
test('accepts Logger subclass', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
class CustomLogger extends Logger {}
|
|
||||||
expect(() => { manager.loggerClass = CustomLogger as any }).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rejects non-Logger class', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
class NotALogger {}
|
|
||||||
expect(() => {
|
|
||||||
manager.loggerClass = NotALogger as any;
|
|
||||||
}).toThrow(TypeError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accepts Logger itself', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
expect(() => { manager.loggerClass = Logger as any }).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getLogger - manager assignment', () => {
|
|
||||||
test('sets manager on newly created loggers', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const logger = manager.getLogger('app');
|
|
||||||
expect(logger.manager).toBe(manager);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('sets manager on loggers created from placeholders', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
manager.getLogger('a.b.c');
|
|
||||||
const mid = manager.getLogger('a.b');
|
|
||||||
expect(mid.manager).toBe(manager);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isEnabledFor respects manager.disable via getLogger', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const logger = manager.getLogger('app');
|
|
||||||
logger.setLevel(DEBUG);
|
|
||||||
|
|
||||||
expect(logger.isEnabledFor(DEBUG)).toBe(true);
|
|
||||||
|
|
||||||
manager.disable = WARNING;
|
|
||||||
logger.clear();
|
|
||||||
|
|
||||||
expect(logger.isEnabledFor(DEBUG)).toBe(false);
|
|
||||||
expect(logger.isEnabledFor(WARNING)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('propagation through hierarchy', () => {
|
|
||||||
test('child logger propagates records to parent handler', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const parent = manager.getLogger('app');
|
|
||||||
const child = manager.getLogger('app.module');
|
|
||||||
|
|
||||||
parent.setLevel(DEBUG);
|
|
||||||
child.setLevel(DEBUG);
|
|
||||||
|
|
||||||
const emitted: string[] = [];
|
|
||||||
const handler = new (class extends Handler {
|
|
||||||
emit(record: LogRecord) {
|
|
||||||
emitted.push(record.msg);
|
|
||||||
}
|
|
||||||
})(DEBUG);
|
|
||||||
|
|
||||||
parent.addHandler(handler);
|
|
||||||
child.info('propagated message');
|
|
||||||
|
|
||||||
expect(emitted).toContain('propagated message');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('child does not propagate to unrelated logger', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const unrelated = manager.getLogger('other');
|
|
||||||
const child = manager.getLogger('app.module');
|
|
||||||
|
|
||||||
unrelated.setLevel(DEBUG);
|
|
||||||
child.setLevel(DEBUG);
|
|
||||||
|
|
||||||
const emitted: string[] = [];
|
|
||||||
const handler = new (class extends Handler {
|
|
||||||
emit(record: LogRecord) {
|
|
||||||
emitted.push(record.msg);
|
|
||||||
}
|
|
||||||
})(DEBUG);
|
|
||||||
|
|
||||||
unrelated.addHandler(handler);
|
|
||||||
child.info('should not reach');
|
|
||||||
|
|
||||||
expect(emitted).not.toContain('should not reach');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('clear', () => {
|
|
||||||
test('clears logger caches', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
const logger = manager.getLogger('test');
|
|
||||||
logger.isEnabledFor(DEBUG);
|
|
||||||
manager.clear();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('does not throw when placeholders exist', () => {
|
|
||||||
const manager = makeManager();
|
|
||||||
manager.getLogger('a.b.c');
|
|
||||||
expect(() => manager.clear()).not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
1
vendor/tiara-gitflow-spec
vendored
1
vendor/tiara-gitflow-spec
vendored
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit ee93479edce1da28f4abf68a362427f8d3134f80
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue