@holz/core 0.7.0-rc.2 → 0.8.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -16
- package/dist/holz-core.cjs +1 -1
- package/dist/holz-core.d.ts +78 -26
- package/dist/holz-core.js +48 -57
- package/package.json +3 -3
- package/src/__tests__/logger.test.ts +53 -10
- package/src/__tests__/operators.test.ts +2 -2
- package/src/index.ts +10 -3
- package/src/logger.ts +49 -55
- package/src/operators.ts +12 -11
- package/src/types.ts +111 -15
package/README.md
CHANGED
|
@@ -7,21 +7,32 @@
|
|
|
7
7
|
Here is an example of how to create a logger and use it to log a message:
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
|
-
import { createLogger } from '@holz/core';
|
|
10
|
+
import { createLogger, type LogLevel } from '@holz/core';
|
|
11
11
|
|
|
12
12
|
const logger = createLogger((log) => {
|
|
13
|
-
console.log(
|
|
13
|
+
console.log(logLevelPrefix[log.level], log.message);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
const logLevelPrefix: Record<LogLevel, string> = {
|
|
17
|
+
[level.trace]: '[trace]',
|
|
18
|
+
[level.debug]: '[debug]',
|
|
19
|
+
[level.info]: '[info]',
|
|
20
|
+
[level.warn]: '[warn]',
|
|
21
|
+
[level.error]: '[error]',
|
|
22
|
+
[level.fatal]: '[fatal]',
|
|
23
|
+
};
|
|
24
|
+
|
|
16
25
|
logger.info('Hello, world!');
|
|
17
26
|
```
|
|
18
27
|
|
|
19
|
-
This will log a message
|
|
28
|
+
This will log a message to the console.
|
|
20
29
|
|
|
21
30
|
```
|
|
22
31
|
[info] Hello, world!
|
|
23
32
|
```
|
|
24
33
|
|
|
34
|
+
This is a simplified example of what `@holz/console-backend` does under the hood.
|
|
35
|
+
|
|
25
36
|
## Logger Interface
|
|
26
37
|
|
|
27
38
|
The `Logger` class is the core of the library. It provides a set of methods for logging messages with different levels of severity. Each method takes a message and an optional context object.
|
|
@@ -30,14 +41,18 @@ You can also use the `namespace` method to create a new logger with a specific o
|
|
|
30
41
|
|
|
31
42
|
## Log Levels
|
|
32
43
|
|
|
33
|
-
The library provides
|
|
44
|
+
The library provides six log levels:
|
|
45
|
+
|
|
46
|
+
- `level.fatal`: Something critical failed and we can't recover.
|
|
47
|
+
- `level.error`: Something failed, but we can recover.
|
|
48
|
+
- `level.warn`: Something is concerning, but we can keep going.
|
|
49
|
+
- `level.info`: High-level progress updates.
|
|
50
|
+
- `level.debug`: Low-level progress updates, such as events (usually hidden).
|
|
51
|
+
- `level.trace`: Extremely verbose updates, such as function calls and branch conditions (usually hidden).
|
|
34
52
|
|
|
35
|
-
|
|
36
|
-
- `LogLevel.Info`: High-level progress updates.
|
|
37
|
-
- `LogLevel.Warn`: Something is concerning, but we can keep going.
|
|
38
|
-
- `LogLevel.Error`: Something critical failed and we can't continue.
|
|
53
|
+
Backends may do different things depending on severity, such as only uploading `info` or above to a log aggregator.
|
|
39
54
|
|
|
40
|
-
|
|
55
|
+
Each level is a number. Higher numbers mean higher severity.
|
|
41
56
|
|
|
42
57
|
## Log Structure
|
|
43
58
|
|
|
@@ -46,7 +61,7 @@ Every log message processed by Holz follows a specific structure defined by the
|
|
|
46
61
|
A `Log` object contains the following properties:
|
|
47
62
|
|
|
48
63
|
- `message`: The verbatim log message. This property should not contain any interpolated data.
|
|
49
|
-
- `level`: The severity of the log message, expressed as a member of the `LogLevel` enum. The available log levels are, in increasing order of severity: `
|
|
64
|
+
- `level`: The severity of the log message, expressed as a member of the `LogLevel` enum. The available log levels are, in increasing order of severity: `trace`, `debug`, `info`, `warn`, `error`, `fatal`.
|
|
50
65
|
- `origin`: The source of the log message. This property is an array of strings that typically identifies the library, module, or component that generated the log message, followed by more specific information. This property can be used to filter and group log messages based on their origin.
|
|
51
66
|
- `context`: A dictionary of key-value pairs that provides additional context for the log message. The values in this object must be JSON serializable. Because it's easy to accidentally include unsuitable log context, such as PII or deeply nested objects, the use of nested objects in the context is discouraged.
|
|
52
67
|
|
|
@@ -54,9 +69,9 @@ Here is an example of a log message expressed as a `Log` object:
|
|
|
54
69
|
|
|
55
70
|
```typescript
|
|
56
71
|
{
|
|
57
|
-
message: '
|
|
58
|
-
level:
|
|
59
|
-
origin: ['my-app', '
|
|
72
|
+
message: 'Established database connection',
|
|
73
|
+
level: level.info,
|
|
74
|
+
origin: ['my-app', 'db'],
|
|
60
75
|
context: {
|
|
61
76
|
host: 'localhost',
|
|
62
77
|
port: 5432,
|
|
@@ -93,7 +108,7 @@ import {
|
|
|
93
108
|
} from './custom-processors';
|
|
94
109
|
|
|
95
110
|
const logger = createLogger(
|
|
96
|
-
combine([consoleBackend, fileBackend, logUploadService])
|
|
111
|
+
combine([consoleBackend, fileBackend, logUploadService]),
|
|
97
112
|
);
|
|
98
113
|
```
|
|
99
114
|
|
|
@@ -105,8 +120,8 @@ The `filter` processor takes a filter function and another log processor as inpu
|
|
|
105
120
|
import { filter } from '@holz/core';
|
|
106
121
|
|
|
107
122
|
const debugLogFilter = filter(
|
|
108
|
-
(log) => log.level
|
|
109
|
-
consoleProcessor
|
|
123
|
+
(log) => log.level > level.debug,
|
|
124
|
+
consoleProcessor,
|
|
110
125
|
);
|
|
111
126
|
|
|
112
127
|
const logger = createLogger(debugLogFilter);
|
package/dist/holz-core.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const b=n=>r=>n.forEach(e=>e(r)),f=(n,r)=>e=>{n(e)&&r(e)},l={fatal:60,error:50,warn:40,info:30,debug:20,trace:10},i=(n,r)=>{const e=(a,c,d={})=>{n({message:c,level:a,origin:r,context:d})},o={owner:r,trace:e.bind(null,l.trace),debug:e.bind(null,l.debug),info:e.bind(null,l.info),warn:e.bind(null,l.warn),error:e.bind(null,l.error),fatal:e.bind(null,l.fatal),namespace:a=>i(n,r.concat(a)),withMiddleware:a=>i(a(n),r)},t={configurable:!1,enumerable:!1};return Object.defineProperties(o,{withMiddleware:t,namespace:t,trace:t,debug:t,info:t,warn:t,error:t,fatal:t})},g=n=>i(n,[]);exports.combine=b;exports.createLogger=g;exports.filter=f;exports.level=l;
|
package/dist/holz-core.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* called in sequence. Useful for sending a single log to multiple log
|
|
4
4
|
* destinations.
|
|
5
5
|
*/
|
|
6
|
-
export declare
|
|
6
|
+
export declare const combine: (processors: Array<LogProcessor>) => LogProcessor;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Create a new logger. The processor is up to you. There are plugins for
|
|
@@ -13,24 +13,60 @@ export declare function combine(processors: Array<LogProcessor>): LogProcessor;
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const createLogger: (processor: LogProcessor) => Logger;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Custom values allowed in log context. These values don't have to be JSON.
|
|
18
|
+
* The most common case is errors, which support richer error tracking in
|
|
19
|
+
* backends that support it.
|
|
20
|
+
*
|
|
21
|
+
* This is part of the public API. Backends may use declaration merging to
|
|
22
|
+
* extend the standard attributes. It's recommended to use symbols as keys for
|
|
23
|
+
* custom features, but not required.
|
|
24
|
+
*/
|
|
25
|
+
export declare interface CustomContext {
|
|
26
|
+
/**
|
|
27
|
+
* Error instance associated with the log message. This supports error
|
|
28
|
+
* tracking and shows prominently in visual backends like TTY output.
|
|
29
|
+
*/
|
|
30
|
+
error: Error;
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
/**
|
|
17
34
|
* Filter logs based on a filter function. If the function returns true, the
|
|
18
35
|
* log is kept and forwarded onto the next processor, otherwise it is
|
|
19
36
|
* discarded.
|
|
20
37
|
*/
|
|
21
|
-
export declare
|
|
38
|
+
export declare const filter: (
|
|
22
39
|
/** Returns `true` to forward the log. */
|
|
23
40
|
predicate: (log: Log) => boolean,
|
|
24
41
|
/** Where to send the log if it passes the filter. */
|
|
25
|
-
processor: LogProcessor)
|
|
42
|
+
processor: LogProcessor) => LogProcessor;
|
|
43
|
+
|
|
44
|
+
declare interface JsonContext {
|
|
45
|
+
[key: string]: JsonPrimitive | ReadonlyArray<JsonPrimitive>;
|
|
46
|
+
}
|
|
26
47
|
|
|
27
48
|
declare type JsonPrimitive = string | number | boolean | null | undefined;
|
|
28
49
|
|
|
50
|
+
export declare const level: {
|
|
51
|
+
/** A critical failure happened and the program must exit. */
|
|
52
|
+
readonly fatal: 60;
|
|
53
|
+
/** Something failed, but we can keep going. */
|
|
54
|
+
readonly error: 50;
|
|
55
|
+
/** Cause for concern, but we can keep going. */
|
|
56
|
+
readonly warn: 40;
|
|
57
|
+
/** High-level progress updates. */
|
|
58
|
+
readonly info: 30;
|
|
59
|
+
/** Verbose update about events or control flow (usually hidden). */
|
|
60
|
+
readonly debug: 20;
|
|
61
|
+
/** Extremely detailed progress updates (usually hidden). */
|
|
62
|
+
readonly trace: 10;
|
|
63
|
+
};
|
|
64
|
+
|
|
29
65
|
/** A log message where variables are carried as structured data. */
|
|
30
66
|
export declare interface Log {
|
|
31
67
|
/** The verbatim log message. Should not contain interpolated data. */
|
|
32
68
|
readonly message: string;
|
|
33
|
-
/** Log severity. */
|
|
69
|
+
/** Log severity. One of `levels.*`. */
|
|
34
70
|
readonly level: LogLevel;
|
|
35
71
|
/**
|
|
36
72
|
* Where the log message originated. Usually starts with a library or app name,
|
|
@@ -55,35 +91,42 @@ export declare interface Log {
|
|
|
55
91
|
readonly context: LogContext;
|
|
56
92
|
}
|
|
57
93
|
|
|
58
|
-
|
|
94
|
+
/**
|
|
95
|
+
* A more general type expected by logging backends. Supports type narrowing,
|
|
96
|
+
* so if a known field exists, it will be used instead of the generic JSON
|
|
97
|
+
* type.
|
|
98
|
+
*/
|
|
99
|
+
export declare type LogContext = Partial<CustomContext> & JsonContext;
|
|
59
100
|
|
|
60
|
-
|
|
61
|
-
|
|
101
|
+
/**
|
|
102
|
+
* This is the public API which generates and sends `Log` events through the
|
|
103
|
+
* user-defined processing pipeline.
|
|
104
|
+
*/
|
|
105
|
+
export declare interface Logger {
|
|
62
106
|
readonly owner: ReadonlyArray<string>;
|
|
63
|
-
constructor(processor: LogProcessor, owner: ReadonlyArray<string>);
|
|
64
107
|
/** Extend the logger to attach a class or module name to logs. */
|
|
65
|
-
namespace(owner: string)
|
|
108
|
+
namespace: (owner: string) => Logger;
|
|
109
|
+
/**
|
|
110
|
+
* Create a new logger which runs all logs through a middleware function.
|
|
111
|
+
* Useful for adding default context, doing ad-hoc filtering, or
|
|
112
|
+
* registering plugins. Only applies to this logger and its descendants.
|
|
113
|
+
*/
|
|
114
|
+
withMiddleware: (middleware: (next: LogProcessor) => LogProcessor) => Logger;
|
|
115
|
+
/** Log a verbose and frequent update. */
|
|
116
|
+
trace: <Context extends StrictContext<Context>>(message: string, context?: Context) => void;
|
|
66
117
|
/** Log a frequent and verbose progress update. */
|
|
67
|
-
debug: (message: string, context?:
|
|
118
|
+
debug: <Context extends StrictContext<Context>>(message: string, context?: Context) => void;
|
|
68
119
|
/** Log a high-level progress update. */
|
|
69
|
-
info: (message: string, context?:
|
|
120
|
+
info: <Context extends StrictContext<Context>>(message: string, context?: Context) => void;
|
|
70
121
|
/** Log something concerning. */
|
|
71
|
-
warn: (message: string, context?:
|
|
72
|
-
/** Log a
|
|
73
|
-
error: (message: string, context?:
|
|
74
|
-
|
|
122
|
+
warn: <Context extends StrictContext<Context>>(message: string, context?: Context) => void;
|
|
123
|
+
/** Log a failure. */
|
|
124
|
+
error: <Context extends StrictContext<Context>>(message: string, context?: Context) => void;
|
|
125
|
+
/** Log a catastrophic failure. */
|
|
126
|
+
fatal: <Context extends StrictContext<Context>>(message: string, context?: Context) => void;
|
|
75
127
|
}
|
|
76
128
|
|
|
77
|
-
export declare
|
|
78
|
-
/** Something critical failed and we can't continue. */
|
|
79
|
-
Error = "error",
|
|
80
|
-
/** Something is concerning, but we can keep going. */
|
|
81
|
-
Warn = "warn",
|
|
82
|
-
/** High-level progress updates. */
|
|
83
|
-
Info = "info",
|
|
84
|
-
/** Extremely verbose progress updates (usually hidden). */
|
|
85
|
-
Debug = "debug"
|
|
86
|
-
}
|
|
129
|
+
export declare type LogLevel = (typeof level)[keyof typeof level];
|
|
87
130
|
|
|
88
131
|
/**
|
|
89
132
|
* A logger is made up of log processors. These processors take structured
|
|
@@ -92,7 +135,16 @@ export declare enum LogLevel {
|
|
|
92
135
|
*/
|
|
93
136
|
export declare interface LogProcessor {
|
|
94
137
|
/** Do something with a log message. */
|
|
95
|
-
(log: Log):
|
|
138
|
+
(log: Log): unknown;
|
|
96
139
|
}
|
|
97
140
|
|
|
141
|
+
/**
|
|
142
|
+
* A type narrowing constraint designed for generic constraints. Controls
|
|
143
|
+
* what fields are allowed in `log.context`, sourcing from `CustomContext`
|
|
144
|
+
* first, then falling back to any JSON value.
|
|
145
|
+
*/
|
|
146
|
+
declare type StrictContext<Input> = {
|
|
147
|
+
[Key in keyof Input]: Key extends keyof CustomContext ? CustomContext[Key] : JsonPrimitive | ReadonlyArray<JsonPrimitive>;
|
|
148
|
+
};
|
|
149
|
+
|
|
98
150
|
export { }
|
package/dist/holz-core.js
CHANGED
|
@@ -1,60 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
const f = (n) => (r) => n.forEach((e) => e(r)), b = (n, r) => (e) => {
|
|
2
|
+
n(e) && r(e);
|
|
3
|
+
}, l = {
|
|
4
|
+
/** A critical failure happened and the program must exit. */
|
|
5
|
+
fatal: 60,
|
|
6
|
+
/** Something failed, but we can keep going. */
|
|
7
|
+
error: 50,
|
|
8
|
+
/** Cause for concern, but we can keep going. */
|
|
9
|
+
warn: 40,
|
|
10
|
+
/** High-level progress updates. */
|
|
11
|
+
info: 30,
|
|
12
|
+
/** Verbose update about events or control flow (usually hidden). */
|
|
13
|
+
debug: 20,
|
|
14
|
+
/** Extremely detailed progress updates (usually hidden). */
|
|
15
|
+
trace: 10
|
|
16
|
+
}, i = (n, r) => {
|
|
17
|
+
const e = (t, d, c = {}) => {
|
|
18
|
+
n({
|
|
19
|
+
message: d,
|
|
20
|
+
level: t,
|
|
21
|
+
origin: r,
|
|
22
|
+
context: c
|
|
18
23
|
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
/** Extend the logger to attach a class or module name to logs. */
|
|
42
|
-
namespace(r) {
|
|
43
|
-
return new i(this.processor, this.owner.concat(r));
|
|
44
|
-
}
|
|
45
|
-
forwardLog(r, o, n = {}) {
|
|
46
|
-
this.processor({
|
|
47
|
-
message: o,
|
|
48
|
-
level: r,
|
|
49
|
-
origin: this.owner,
|
|
50
|
-
context: n
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
const w = (e) => new i(e, []);
|
|
24
|
+
}, o = {
|
|
25
|
+
owner: r,
|
|
26
|
+
trace: e.bind(null, l.trace),
|
|
27
|
+
debug: e.bind(null, l.debug),
|
|
28
|
+
info: e.bind(null, l.info),
|
|
29
|
+
warn: e.bind(null, l.warn),
|
|
30
|
+
error: e.bind(null, l.error),
|
|
31
|
+
fatal: e.bind(null, l.fatal),
|
|
32
|
+
namespace: (t) => i(n, r.concat(t)),
|
|
33
|
+
withMiddleware: (t) => i(t(n), r)
|
|
34
|
+
}, a = { configurable: !1, enumerable: !1 };
|
|
35
|
+
return Object.defineProperties(o, {
|
|
36
|
+
withMiddleware: a,
|
|
37
|
+
namespace: a,
|
|
38
|
+
trace: a,
|
|
39
|
+
debug: a,
|
|
40
|
+
info: a,
|
|
41
|
+
warn: a,
|
|
42
|
+
error: a,
|
|
43
|
+
fatal: a
|
|
44
|
+
});
|
|
45
|
+
}, u = (n) => i(n, []);
|
|
55
46
|
export {
|
|
56
|
-
|
|
57
|
-
u as
|
|
58
|
-
|
|
59
|
-
|
|
47
|
+
f as combine,
|
|
48
|
+
u as createLogger,
|
|
49
|
+
b as filter,
|
|
50
|
+
l as level
|
|
60
51
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holz/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-rc.1",
|
|
4
4
|
"description": "A structured and composable logger",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@vitest/coverage-v8": "^3.0.8",
|
|
40
|
-
"typescript": "^5.
|
|
40
|
+
"typescript": "^5.8.2",
|
|
41
41
|
"vite": "^6.0.0",
|
|
42
42
|
"vite-plugin-dts": "^4.5.3",
|
|
43
43
|
"vite-tsconfig-paths": "^5.1.4",
|
|
44
44
|
"vitest": "^3.0.8"
|
|
45
45
|
},
|
|
46
|
-
"stableVersion": "0.
|
|
46
|
+
"stableVersion": "0.7.0"
|
|
47
47
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createLogger } from '../logger';
|
|
2
|
-
import {
|
|
2
|
+
import { level } from '../index';
|
|
3
3
|
|
|
4
4
|
describe('Logger', () => {
|
|
5
5
|
it('sends structured logs to the log processor', () => {
|
|
@@ -11,25 +11,27 @@ describe('Logger', () => {
|
|
|
11
11
|
expect(backend).toHaveBeenCalledOnce();
|
|
12
12
|
expect(backend).toHaveBeenCalledWith({
|
|
13
13
|
message: 'Hello world',
|
|
14
|
-
level:
|
|
14
|
+
level: level.info,
|
|
15
15
|
origin: [],
|
|
16
16
|
context: { audience: 'testers' },
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
it.each([
|
|
21
|
-
[
|
|
22
|
-
[
|
|
23
|
-
[
|
|
24
|
-
[
|
|
25
|
-
|
|
21
|
+
['trace' as const, 'Look, a dead fly', { urgency: 'high?' }],
|
|
22
|
+
['debug' as const, 'Made in Britain', { condition: 'fire' }],
|
|
23
|
+
['info' as const, 'I am not a window cleaner!', { state: 'panic' }],
|
|
24
|
+
['warn' as const, 'There are irregularities in the pension fund', {}],
|
|
25
|
+
['error' as const, 'Have you tried turning it off and on again?', {}],
|
|
26
|
+
['fatal' as const, 'Leadership has taken a dive', { windows: 'open' }],
|
|
27
|
+
])('correctly processes %s log messages', (method, message, context) => {
|
|
26
28
|
const backend = vi.fn();
|
|
27
29
|
const logger = createLogger(backend);
|
|
28
30
|
|
|
29
|
-
logger[
|
|
31
|
+
logger[method](message, context);
|
|
30
32
|
expect(backend).toHaveBeenCalledWith({
|
|
31
33
|
message,
|
|
32
|
-
level,
|
|
34
|
+
level: level[method],
|
|
33
35
|
context,
|
|
34
36
|
origin: [],
|
|
35
37
|
});
|
|
@@ -60,10 +62,51 @@ describe('Logger', () => {
|
|
|
60
62
|
expect(backend).toHaveBeenCalledOnce();
|
|
61
63
|
expect(backend).toHaveBeenCalledWith({
|
|
62
64
|
message: 'opening socket',
|
|
63
|
-
level:
|
|
65
|
+
level: level.info,
|
|
64
66
|
origin: ['signaling', 'socket'],
|
|
65
67
|
context: {},
|
|
66
68
|
});
|
|
67
69
|
});
|
|
68
70
|
});
|
|
71
|
+
|
|
72
|
+
describe('withMiddleware', () => {
|
|
73
|
+
it('allows middleware to be added', () => {
|
|
74
|
+
const backend = vi.fn();
|
|
75
|
+
const logger = createLogger(backend).withMiddleware((next) => (log) => {
|
|
76
|
+
next({ ...log, message: 'replaced' });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
logger.info('original message');
|
|
80
|
+
expect(backend).toHaveBeenCalledOnce();
|
|
81
|
+
expect(backend).toHaveBeenCalledWith({
|
|
82
|
+
message: 'replaced',
|
|
83
|
+
level: level.info,
|
|
84
|
+
origin: [],
|
|
85
|
+
context: {},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('inherits middleware from parent loggers', () => {
|
|
90
|
+
const backend = vi.fn();
|
|
91
|
+
const logger = createLogger(backend)
|
|
92
|
+
.withMiddleware((next) => (log) => {
|
|
93
|
+
log.context.parent = true;
|
|
94
|
+
next(log);
|
|
95
|
+
})
|
|
96
|
+
.namespace('child')
|
|
97
|
+
.withMiddleware((next) => (log) => {
|
|
98
|
+
log.context.child = true;
|
|
99
|
+
next(log);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
logger.info('original message');
|
|
103
|
+
expect(backend).toHaveBeenCalledOnce();
|
|
104
|
+
expect(backend).toHaveBeenCalledWith({
|
|
105
|
+
message: 'original message',
|
|
106
|
+
level: level.info,
|
|
107
|
+
origin: ['child'],
|
|
108
|
+
context: { parent: true, child: true },
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
69
112
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createLogger } from '../logger';
|
|
2
2
|
import { combine, filter } from '../operators';
|
|
3
|
-
import {
|
|
3
|
+
import { level } from '../types';
|
|
4
4
|
|
|
5
5
|
describe('operators', () => {
|
|
6
6
|
describe('combine', () => {
|
|
@@ -33,7 +33,7 @@ describe('operators', () => {
|
|
|
33
33
|
it('filters out logs that do not match the predicate', () => {
|
|
34
34
|
const backend = vi.fn();
|
|
35
35
|
const logger = createLogger(
|
|
36
|
-
filter((log) => log.level !==
|
|
36
|
+
filter((log) => log.level !== level.debug, backend),
|
|
37
37
|
);
|
|
38
38
|
|
|
39
39
|
logger.info('keep me');
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export type { LogProcessor, LogContext, Log } from './types';
|
|
2
|
-
export { LogLevel } from './types';
|
|
3
1
|
export { combine, filter } from './operators';
|
|
4
|
-
export { createLogger
|
|
2
|
+
export { createLogger } from './logger';
|
|
3
|
+
export { level } from './types';
|
|
4
|
+
export type {
|
|
5
|
+
LogProcessor,
|
|
6
|
+
LogContext,
|
|
7
|
+
Log,
|
|
8
|
+
CustomContext,
|
|
9
|
+
Logger,
|
|
10
|
+
LogLevel,
|
|
11
|
+
} from './types';
|
package/src/logger.ts
CHANGED
|
@@ -1,60 +1,56 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
warn: hidden,
|
|
16
|
-
error: hidden,
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Extend the logger to attach a class or module name to logs. */
|
|
21
|
-
namespace(owner: string): Logger {
|
|
22
|
-
return new Logger(this.processor, this.owner.concat(owner));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Log a frequent and verbose progress update. */
|
|
26
|
-
debug = (message: string, context?: LogContext) => {
|
|
27
|
-
this.forwardLog(LogLevel.Debug, message, context);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** Log a high-level progress update. */
|
|
31
|
-
info = (message: string, context?: LogContext) => {
|
|
32
|
-
this.forwardLog(LogLevel.Info, message, context);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/** Log something concerning. */
|
|
36
|
-
warn = (message: string, context?: LogContext) => {
|
|
37
|
-
this.forwardLog(LogLevel.Warn, message, context);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/** Log a critical failure. */
|
|
41
|
-
error = (message: string, context?: LogContext) => {
|
|
42
|
-
this.forwardLog(LogLevel.Error, message, context);
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
private forwardLog(
|
|
1
|
+
import {
|
|
2
|
+
level,
|
|
3
|
+
type LogContext,
|
|
4
|
+
type LogLevel,
|
|
5
|
+
type LogProcessor,
|
|
6
|
+
type StrictContext,
|
|
7
|
+
type Logger,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
const createNamespacedLogger = (
|
|
11
|
+
processor: LogProcessor,
|
|
12
|
+
namespace: ReadonlyArray<string>,
|
|
13
|
+
): Logger => {
|
|
14
|
+
const createAndSendLog = <Context extends StrictContext<Context>>(
|
|
46
15
|
level: LogLevel,
|
|
47
16
|
message: string,
|
|
48
|
-
context:
|
|
49
|
-
) {
|
|
50
|
-
|
|
17
|
+
context: Context = {} as Context,
|
|
18
|
+
) => {
|
|
19
|
+
processor({
|
|
51
20
|
message,
|
|
52
21
|
level,
|
|
53
|
-
origin:
|
|
54
|
-
context,
|
|
22
|
+
origin: namespace,
|
|
23
|
+
context: context as LogContext,
|
|
55
24
|
});
|
|
56
|
-
}
|
|
57
|
-
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const logger: Logger = {
|
|
28
|
+
owner: namespace,
|
|
29
|
+
trace: createAndSendLog.bind(null, level.trace),
|
|
30
|
+
debug: createAndSendLog.bind(null, level.debug),
|
|
31
|
+
info: createAndSendLog.bind(null, level.info),
|
|
32
|
+
warn: createAndSendLog.bind(null, level.warn),
|
|
33
|
+
error: createAndSendLog.bind(null, level.error),
|
|
34
|
+
fatal: createAndSendLog.bind(null, level.fatal),
|
|
35
|
+
namespace: (child: string) =>
|
|
36
|
+
createNamespacedLogger(processor, namespace.concat(child)),
|
|
37
|
+
withMiddleware: (middleware) =>
|
|
38
|
+
createNamespacedLogger(middleware(processor), namespace),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Non-enumerable to keep the repl clean.
|
|
42
|
+
const hidden = { configurable: false, enumerable: false };
|
|
43
|
+
return Object.defineProperties(logger, {
|
|
44
|
+
withMiddleware: hidden,
|
|
45
|
+
namespace: hidden,
|
|
46
|
+
trace: hidden,
|
|
47
|
+
debug: hidden,
|
|
48
|
+
info: hidden,
|
|
49
|
+
warn: hidden,
|
|
50
|
+
error: hidden,
|
|
51
|
+
fatal: hidden,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
58
54
|
|
|
59
55
|
/**
|
|
60
56
|
* Create a new logger. The processor is up to you. There are plugins for
|
|
@@ -63,6 +59,4 @@ class Logger {
|
|
|
63
59
|
* If you wish to use more than one processor, `combine(...)` them first.
|
|
64
60
|
*/
|
|
65
61
|
export const createLogger = (processor: LogProcessor): Logger =>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
export type { Logger };
|
|
62
|
+
createNamespacedLogger(processor, []);
|
package/src/operators.ts
CHANGED
|
@@ -5,24 +5,25 @@ import type { Log, LogProcessor } from './types';
|
|
|
5
5
|
* called in sequence. Useful for sending a single log to multiple log
|
|
6
6
|
* destinations.
|
|
7
7
|
*/
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
export const combine =
|
|
9
|
+
(processors: Array<LogProcessor>): LogProcessor =>
|
|
10
|
+
(log: Log) =>
|
|
11
|
+
processors.forEach((processor) => processor(log));
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Filter logs based on a filter function. If the function returns true, the
|
|
14
15
|
* log is kept and forwarded onto the next processor, otherwise it is
|
|
15
16
|
* discarded.
|
|
16
17
|
*/
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
export const filter =
|
|
19
|
+
(
|
|
20
|
+
/** Returns `true` to forward the log. */
|
|
21
|
+
predicate: (log: Log) => boolean,
|
|
22
|
+
/** Where to send the log if it passes the filter. */
|
|
23
|
+
processor: LogProcessor,
|
|
24
|
+
): LogProcessor =>
|
|
25
|
+
(log: Log) => {
|
|
24
26
|
if (predicate(log)) {
|
|
25
27
|
processor(log);
|
|
26
28
|
}
|
|
27
29
|
};
|
|
28
|
-
}
|
package/src/types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export interface LogProcessor {
|
|
7
7
|
/** Do something with a log message. */
|
|
8
|
-
(log: Log):
|
|
8
|
+
(log: Log): unknown;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/** A log message where variables are carried as structured data. */
|
|
@@ -13,7 +13,7 @@ export interface Log {
|
|
|
13
13
|
/** The verbatim log message. Should not contain interpolated data. */
|
|
14
14
|
readonly message: string;
|
|
15
15
|
|
|
16
|
-
/** Log severity. */
|
|
16
|
+
/** Log severity. One of `levels.*`. */
|
|
17
17
|
readonly level: LogLevel;
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -40,23 +40,119 @@ export interface Log {
|
|
|
40
40
|
readonly context: LogContext;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export
|
|
44
|
-
/**
|
|
45
|
-
|
|
43
|
+
export const level = {
|
|
44
|
+
/** A critical failure happened and the program must exit. */
|
|
45
|
+
fatal: 60,
|
|
46
46
|
|
|
47
|
-
/** Something
|
|
48
|
-
|
|
47
|
+
/** Something failed, but we can keep going. */
|
|
48
|
+
error: 50,
|
|
49
|
+
|
|
50
|
+
/** Cause for concern, but we can keep going. */
|
|
51
|
+
warn: 40,
|
|
49
52
|
|
|
50
53
|
/** High-level progress updates. */
|
|
51
|
-
|
|
54
|
+
info: 30,
|
|
52
55
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
+
/** Verbose update about events or control flow (usually hidden). */
|
|
57
|
+
debug: 20,
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
/** Extremely detailed progress updates (usually hidden). */
|
|
60
|
+
trace: 10,
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
export type LogLevel = (typeof level)[keyof typeof level];
|
|
61
64
|
|
|
62
65
|
type JsonPrimitive = string | number | boolean | null | undefined;
|
|
66
|
+
|
|
67
|
+
export interface JsonContext {
|
|
68
|
+
[key: string]: JsonPrimitive | ReadonlyArray<JsonPrimitive>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Custom values allowed in log context. These values don't have to be JSON.
|
|
73
|
+
* The most common case is errors, which support richer error tracking in
|
|
74
|
+
* backends that support it.
|
|
75
|
+
*
|
|
76
|
+
* This is part of the public API. Backends may use declaration merging to
|
|
77
|
+
* extend the standard attributes. It's recommended to use symbols as keys for
|
|
78
|
+
* custom features, but not required.
|
|
79
|
+
*/
|
|
80
|
+
export interface CustomContext {
|
|
81
|
+
/**
|
|
82
|
+
* Error instance associated with the log message. This supports error
|
|
83
|
+
* tracking and shows prominently in visual backends like TTY output.
|
|
84
|
+
*/
|
|
85
|
+
error: Error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A type narrowing constraint designed for generic constraints. Controls
|
|
90
|
+
* what fields are allowed in `log.context`, sourcing from `CustomContext`
|
|
91
|
+
* first, then falling back to any JSON value.
|
|
92
|
+
*/
|
|
93
|
+
export type StrictContext<Input> = {
|
|
94
|
+
[Key in keyof Input]: Key extends keyof CustomContext
|
|
95
|
+
? CustomContext[Key]
|
|
96
|
+
: JsonPrimitive | ReadonlyArray<JsonPrimitive>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A more general type expected by logging backends. Supports type narrowing,
|
|
101
|
+
* so if a known field exists, it will be used instead of the generic JSON
|
|
102
|
+
* type.
|
|
103
|
+
*/
|
|
104
|
+
export type LogContext = Partial<CustomContext> & JsonContext;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* This is the public API which generates and sends `Log` events through the
|
|
108
|
+
* user-defined processing pipeline.
|
|
109
|
+
*/
|
|
110
|
+
export interface Logger {
|
|
111
|
+
readonly owner: ReadonlyArray<string>;
|
|
112
|
+
|
|
113
|
+
/** Extend the logger to attach a class or module name to logs. */
|
|
114
|
+
namespace: (owner: string) => Logger;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a new logger which runs all logs through a middleware function.
|
|
118
|
+
* Useful for adding default context, doing ad-hoc filtering, or
|
|
119
|
+
* registering plugins. Only applies to this logger and its descendants.
|
|
120
|
+
*/
|
|
121
|
+
withMiddleware: (middleware: (next: LogProcessor) => LogProcessor) => Logger;
|
|
122
|
+
|
|
123
|
+
/** Log a verbose and frequent update. */
|
|
124
|
+
trace: <Context extends StrictContext<Context>>(
|
|
125
|
+
message: string,
|
|
126
|
+
context?: Context,
|
|
127
|
+
) => void;
|
|
128
|
+
|
|
129
|
+
/** Log a frequent and verbose progress update. */
|
|
130
|
+
debug: <Context extends StrictContext<Context>>(
|
|
131
|
+
message: string,
|
|
132
|
+
context?: Context,
|
|
133
|
+
) => void;
|
|
134
|
+
|
|
135
|
+
/** Log a high-level progress update. */
|
|
136
|
+
info: <Context extends StrictContext<Context>>(
|
|
137
|
+
message: string,
|
|
138
|
+
context?: Context,
|
|
139
|
+
) => void;
|
|
140
|
+
|
|
141
|
+
/** Log something concerning. */
|
|
142
|
+
warn: <Context extends StrictContext<Context>>(
|
|
143
|
+
message: string,
|
|
144
|
+
context?: Context,
|
|
145
|
+
) => void;
|
|
146
|
+
|
|
147
|
+
/** Log a failure. */
|
|
148
|
+
error: <Context extends StrictContext<Context>>(
|
|
149
|
+
message: string,
|
|
150
|
+
context?: Context,
|
|
151
|
+
) => void;
|
|
152
|
+
|
|
153
|
+
/** Log a catastrophic failure. */
|
|
154
|
+
fatal: <Context extends StrictContext<Context>>(
|
|
155
|
+
message: string,
|
|
156
|
+
context?: Context,
|
|
157
|
+
) => void;
|
|
158
|
+
}
|