@dxos/log 0.8.4-main.84f28bd → 0.8.4-main.8baae0fced
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/LICENSE +102 -5
- package/dist/lib/browser/chunk-IEP6GGEX.mjs +23 -0
- package/dist/lib/browser/chunk-IEP6GGEX.mjs.map +7 -0
- package/dist/lib/browser/chunk-V7FYKT4H.mjs +311 -0
- package/dist/lib/browser/chunk-V7FYKT4H.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +352 -200
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/platform/browser/index.mjs +26 -0
- package/dist/lib/browser/platform/browser/index.mjs.map +7 -0
- package/dist/lib/browser/platform/node/index.mjs +21 -0
- package/dist/lib/browser/platform/node/index.mjs.map +7 -0
- package/dist/lib/browser/processors/console-processor.mjs +102 -0
- package/dist/lib/browser/processors/console-processor.mjs.map +7 -0
- package/dist/lib/browser/processors/console-stub.mjs +9 -0
- package/dist/lib/browser/processors/console-stub.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-2SZHAWBN.mjs +24 -0
- package/dist/lib/node-esm/chunk-2SZHAWBN.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-5TBDXMQF.mjs +313 -0
- package/dist/lib/node-esm/chunk-5TBDXMQF.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +354 -287
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/platform/browser/index.mjs +27 -0
- package/dist/lib/node-esm/platform/browser/index.mjs.map +7 -0
- package/dist/lib/node-esm/platform/node/index.mjs +22 -0
- package/dist/lib/node-esm/platform/node/index.mjs.map +7 -0
- package/dist/lib/node-esm/processors/console-processor.mjs +103 -0
- package/dist/lib/node-esm/processors/console-processor.mjs.map +7 -0
- package/dist/lib/node-esm/processors/console-stub.mjs +10 -0
- package/dist/lib/node-esm/processors/console-stub.mjs.map +7 -0
- package/dist/types/src/config.d.ts +2 -3
- package/dist/types/src/config.d.ts.map +1 -1
- package/dist/types/src/context.d.ts +79 -3
- package/dist/types/src/context.d.ts.map +1 -1
- package/dist/types/src/dbg.d.ts +23 -0
- package/dist/types/src/dbg.d.ts.map +1 -0
- package/dist/types/src/decorators.d.ts +1 -1
- package/dist/types/src/decorators.d.ts.map +1 -1
- package/dist/types/src/environment.d.ts +24 -0
- package/dist/types/src/environment.d.ts.map +1 -0
- package/dist/types/src/environment.test.d.ts +2 -0
- package/dist/types/src/environment.test.d.ts.map +1 -0
- package/dist/types/src/experimental/ownership.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +7 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/jsonl.d.ts +53 -0
- package/dist/types/src/jsonl.d.ts.map +1 -0
- package/dist/types/src/jsonl.test.d.ts +2 -0
- package/dist/types/src/jsonl.test.d.ts.map +1 -0
- package/dist/types/src/log-buffer.d.ts +20 -0
- package/dist/types/src/log-buffer.d.ts.map +1 -0
- package/dist/types/src/log-buffer.test.d.ts +2 -0
- package/dist/types/src/log-buffer.test.d.ts.map +1 -0
- package/dist/types/src/log.d.ts +55 -18
- package/dist/types/src/log.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts +20 -1
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/options.d.ts +1 -6
- package/dist/types/src/options.d.ts.map +1 -1
- package/dist/types/src/platform/browser/index.d.ts.map +1 -1
- package/dist/types/src/platform/index.d.ts +1 -1
- package/dist/types/src/platform/index.d.ts.map +1 -1
- package/dist/types/src/platform/node/index.d.ts.map +1 -1
- package/dist/types/src/processors/browser-processor.d.ts.map +1 -1
- package/dist/types/src/processors/common.d.ts.map +1 -1
- package/dist/types/src/processors/console-processor.d.ts +1 -1
- package/dist/types/src/processors/console-processor.d.ts.map +1 -1
- package/dist/types/src/processors/file-processor.d.ts.map +1 -1
- package/dist/types/src/processors/index.d.ts +3 -3
- package/dist/types/src/processors/index.d.ts.map +1 -1
- package/dist/types/src/scope.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +33 -18
- package/src/config.ts +3 -2
- package/src/context.ts +280 -10
- package/src/dbg.ts +34 -0
- package/src/decorators.ts +3 -3
- package/src/environment.test.ts +222 -0
- package/src/environment.ts +129 -0
- package/src/experimental/classes.test.ts +1 -1
- package/src/index.ts +7 -4
- package/src/jsonl.test.ts +121 -0
- package/src/jsonl.ts +104 -0
- package/src/log-buffer.test.ts +158 -0
- package/src/log-buffer.ts +89 -0
- package/src/log.test.ts +58 -23
- package/src/log.ts +147 -60
- package/src/meta.ts +29 -1
- package/src/options.ts +27 -11
- package/src/platform/index.ts +1 -1
- package/src/processors/browser-processor.ts +32 -29
- package/src/processors/console-processor.ts +11 -15
- package/src/processors/file-processor.ts +9 -8
- package/src/processors/index.ts +3 -3
- package/src/scope.ts +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { type LogConfig, LogEntry, type LogEntryInit, LogLevel } from './index';
|
|
8
|
+
import { LogBuffer } from './log-buffer';
|
|
9
|
+
|
|
10
|
+
const baseConfig: LogConfig = {
|
|
11
|
+
options: {},
|
|
12
|
+
filters: [{ level: LogLevel.DEBUG }],
|
|
13
|
+
processors: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const createEntry = (overrides: Partial<LogEntryInit> = {}): LogEntry =>
|
|
17
|
+
new LogEntry({
|
|
18
|
+
level: LogLevel.INFO,
|
|
19
|
+
message: 'test message',
|
|
20
|
+
...overrides,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('LogBuffer', () => {
|
|
24
|
+
test('pushes and serializes log entries', ({ expect }) => {
|
|
25
|
+
const buffer = new LogBuffer(10);
|
|
26
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'hello' }));
|
|
27
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'world' }));
|
|
28
|
+
|
|
29
|
+
expect(buffer.size).toBe(2);
|
|
30
|
+
const lines = buffer.serialize().split('\n');
|
|
31
|
+
expect(lines).toHaveLength(2);
|
|
32
|
+
|
|
33
|
+
const first = JSON.parse(lines[0]);
|
|
34
|
+
expect(first.m).toBe('hello');
|
|
35
|
+
expect(first.l).toBe('I');
|
|
36
|
+
expect(first.t).toBeDefined();
|
|
37
|
+
|
|
38
|
+
const second = JSON.parse(lines[1]);
|
|
39
|
+
expect(second.m).toBe('world');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('evicts oldest entries when buffer is full', ({ expect }) => {
|
|
43
|
+
const buffer = new LogBuffer(3);
|
|
44
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'a' }));
|
|
45
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'b' }));
|
|
46
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'c' }));
|
|
47
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'd' }));
|
|
48
|
+
|
|
49
|
+
expect(buffer.size).toBe(3);
|
|
50
|
+
const lines = buffer.serialize().split('\n');
|
|
51
|
+
const messages = lines.map((line) => JSON.parse(line).m);
|
|
52
|
+
expect(messages).toEqual(['b', 'c', 'd']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('skips TRACE-level logs', ({ expect }) => {
|
|
56
|
+
const buffer = new LogBuffer(10);
|
|
57
|
+
buffer.logProcessor(baseConfig, createEntry({ level: LogLevel.TRACE, message: 'trace' }));
|
|
58
|
+
expect(buffer.size).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('captures DEBUG-level and above', ({ expect }) => {
|
|
62
|
+
const buffer = new LogBuffer(10);
|
|
63
|
+
buffer.logProcessor(baseConfig, createEntry({ level: LogLevel.DEBUG, message: 'debug' }));
|
|
64
|
+
buffer.logProcessor(baseConfig, createEntry({ level: LogLevel.WARN, message: 'warn' }));
|
|
65
|
+
buffer.logProcessor(baseConfig, createEntry({ level: LogLevel.ERROR, message: 'error' }));
|
|
66
|
+
|
|
67
|
+
expect(buffer.size).toBe(3);
|
|
68
|
+
const lines = buffer.serialize().split('\n');
|
|
69
|
+
expect(JSON.parse(lines[0]).l).toBe('D');
|
|
70
|
+
expect(JSON.parse(lines[1]).l).toBe('W');
|
|
71
|
+
expect(JSON.parse(lines[2]).l).toBe('E');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('captures file and line metadata', ({ expect }) => {
|
|
75
|
+
const buffer = new LogBuffer(10);
|
|
76
|
+
buffer.logProcessor(
|
|
77
|
+
baseConfig,
|
|
78
|
+
createEntry({
|
|
79
|
+
meta: { F: '/home/user/project/packages/sdk/test.ts', L: 42, S: undefined },
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const lines = buffer.serialize().split('\n');
|
|
84
|
+
const record = JSON.parse(lines[0]);
|
|
85
|
+
expect(record.f).toBe('packages/sdk/test.ts');
|
|
86
|
+
expect(record.n).toBe(42);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('captures error stack', ({ expect }) => {
|
|
90
|
+
const buffer = new LogBuffer(10);
|
|
91
|
+
const error = new Error('boom');
|
|
92
|
+
buffer.logProcessor(baseConfig, createEntry({ error }));
|
|
93
|
+
|
|
94
|
+
const lines = buffer.serialize().split('\n');
|
|
95
|
+
const record = JSON.parse(lines[0]);
|
|
96
|
+
expect(record.e).toContain('boom');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('truncates context to 500 chars', ({ expect }) => {
|
|
100
|
+
const buffer = new LogBuffer(10);
|
|
101
|
+
const longValue = 'x'.repeat(1000);
|
|
102
|
+
buffer.logProcessor(baseConfig, createEntry({ context: { data: longValue } }));
|
|
103
|
+
|
|
104
|
+
const lines = buffer.serialize().split('\n');
|
|
105
|
+
const record = JSON.parse(lines[0]);
|
|
106
|
+
expect(record.c).toBeDefined();
|
|
107
|
+
expect(record.c!.length).toBe(500);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('skips Error context objects', ({ expect }) => {
|
|
111
|
+
const buffer = new LogBuffer(10);
|
|
112
|
+
buffer.logProcessor(baseConfig, createEntry({ context: new Error('ctx error') }));
|
|
113
|
+
|
|
114
|
+
const lines = buffer.serialize().split('\n');
|
|
115
|
+
const record = JSON.parse(lines[0]);
|
|
116
|
+
expect(record.c).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('handles non-serializable context gracefully', ({ expect }) => {
|
|
120
|
+
const buffer = new LogBuffer(10);
|
|
121
|
+
const circular: Record<string, any> = {};
|
|
122
|
+
circular.self = circular;
|
|
123
|
+
buffer.logProcessor(baseConfig, createEntry({ context: circular }));
|
|
124
|
+
|
|
125
|
+
const lines = buffer.serialize().split('\n');
|
|
126
|
+
const record = JSON.parse(lines[0]);
|
|
127
|
+
// Circular values fall back to String(value) rather than dropping the entry.
|
|
128
|
+
expect(record.c).toBeDefined();
|
|
129
|
+
expect(JSON.parse(record.c!).self).toBe('[object Object]');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('serialize returns empty string for empty buffer', ({ expect }) => {
|
|
133
|
+
const buffer = new LogBuffer(10);
|
|
134
|
+
expect(buffer.serialize()).toBe('');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('clear discards all entries', ({ expect }) => {
|
|
138
|
+
const buffer = new LogBuffer(10);
|
|
139
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'a' }));
|
|
140
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'b' }));
|
|
141
|
+
expect(buffer.size).toBe(2);
|
|
142
|
+
|
|
143
|
+
buffer.clear();
|
|
144
|
+
expect(buffer.size).toBe(0);
|
|
145
|
+
expect(buffer.serialize()).toBe('');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('clear allows new entries', ({ expect }) => {
|
|
149
|
+
const buffer = new LogBuffer(10);
|
|
150
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'old' }));
|
|
151
|
+
buffer.clear();
|
|
152
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'new' }));
|
|
153
|
+
|
|
154
|
+
expect(buffer.size).toBe(1);
|
|
155
|
+
const record = JSON.parse(buffer.serialize());
|
|
156
|
+
expect(record.m).toBe('new');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { CircularBuffer } from '@dxos/util';
|
|
6
|
+
|
|
7
|
+
import { type LogConfig, LogLevel, shortLevelName } from './config';
|
|
8
|
+
import { type LogEntry, type LogProcessor } from './context';
|
|
9
|
+
import { type LogRecord } from './jsonl';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_BUFFER_SIZE = 2_000;
|
|
12
|
+
const MAX_CONTEXT_LENGTH = 500;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Captures recent log entries in a circular buffer for debug log dump.
|
|
16
|
+
*/
|
|
17
|
+
export class LogBuffer {
|
|
18
|
+
private readonly _buffer: CircularBuffer<LogRecord>;
|
|
19
|
+
|
|
20
|
+
constructor(size = DEFAULT_BUFFER_SIZE) {
|
|
21
|
+
this._buffer = new CircularBuffer<LogRecord>(size);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Log processor that can be registered with `log.runtimeConfig.processors`.
|
|
26
|
+
* Captures every level except TRACE (does not apply `shouldLog` / filter; use for full debug dumps).
|
|
27
|
+
*/
|
|
28
|
+
readonly logProcessor: LogProcessor = (_config: LogConfig, entry: LogEntry) => {
|
|
29
|
+
if (entry.level <= LogLevel.TRACE) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { filename, line, context: scopeName } = entry.computedMeta;
|
|
34
|
+
|
|
35
|
+
const record: LogRecord = {
|
|
36
|
+
t: new Date(entry.timestamp).toISOString(),
|
|
37
|
+
l: shortLevelName[entry.level] ?? '?',
|
|
38
|
+
m: entry.message ?? '',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (filename !== undefined) {
|
|
42
|
+
record.f = filename;
|
|
43
|
+
}
|
|
44
|
+
if (line !== undefined) {
|
|
45
|
+
record.n = line;
|
|
46
|
+
}
|
|
47
|
+
if (scopeName !== undefined) {
|
|
48
|
+
record.o = scopeName;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (entry.computedError !== undefined) {
|
|
52
|
+
record.e = entry.computedError;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const computedContext = entry.computedContext;
|
|
56
|
+
if (Object.keys(computedContext).length > 0) {
|
|
57
|
+
try {
|
|
58
|
+
let json = JSON.stringify(computedContext);
|
|
59
|
+
if (json.length > MAX_CONTEXT_LENGTH) {
|
|
60
|
+
json = json.slice(0, MAX_CONTEXT_LENGTH);
|
|
61
|
+
}
|
|
62
|
+
record.c = json;
|
|
63
|
+
} catch {
|
|
64
|
+
// Skip context that throws or is non-serializable.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this._buffer.push(record);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Number of entries currently in the buffer. */
|
|
72
|
+
get size(): number {
|
|
73
|
+
return this._buffer.elementCount;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Discard all buffered entries. */
|
|
77
|
+
clear(): void {
|
|
78
|
+
this._buffer.clear();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Serialize buffer contents as NDJSON (newline-delimited JSON). */
|
|
82
|
+
serialize(): string {
|
|
83
|
+
const lines: string[] = [];
|
|
84
|
+
for (const record of this._buffer) {
|
|
85
|
+
lines.push(JSON.stringify(record));
|
|
86
|
+
}
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/log.test.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { describe, test } from 'vitest';
|
|
6
|
+
import { beforeEach, describe, test } from 'vitest';
|
|
7
7
|
|
|
8
8
|
import { LogLevel } from './config';
|
|
9
|
-
import {
|
|
9
|
+
import { shouldLog } from './context';
|
|
10
|
+
import { type Log, createLog } from './log';
|
|
10
11
|
|
|
11
12
|
class LogError extends Error {
|
|
12
13
|
constructor(
|
|
@@ -24,14 +25,53 @@ class LogError extends Error {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
log
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
describe('log', () => {
|
|
29
|
+
let log!: Log;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
log = createLog();
|
|
33
|
+
log.config({
|
|
34
|
+
filter: LogLevel.DEBUG,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
test('filters', ({ expect }) => {
|
|
39
|
+
const tests = [
|
|
40
|
+
{ expected: 0, filter: 'ERROR' },
|
|
41
|
+
{ expected: 2, filter: 'INFO' },
|
|
42
|
+
{ expected: 1, filter: 'foo:INFO' },
|
|
43
|
+
{ expected: 4, filter: 'DEBUG' },
|
|
44
|
+
{ expected: 2, filter: 'DEBUG,-foo:*' },
|
|
45
|
+
{ expected: 1, filter: 'INFO,-foo:*' },
|
|
46
|
+
{ expected: 3, filter: 'DEBUG,-foo:INFO' },
|
|
47
|
+
{ expected: 3, filter: 'foo:DEBUG,bar:INFO' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
for (const test of tests) {
|
|
51
|
+
let count = 0;
|
|
52
|
+
const log = createLog();
|
|
53
|
+
const remove = log.addProcessor((config, entry) => {
|
|
54
|
+
if (shouldLog(entry, config.filters)) {
|
|
55
|
+
count++;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
log.config({
|
|
59
|
+
filter: test.filter,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.group(`Filter: "${test.filter}"`);
|
|
63
|
+
log.debug('line 1', {}, { F: 'foo.ts', L: 1, S: undefined });
|
|
64
|
+
log.info('line 2', {}, { F: 'foo.ts', L: 2, S: undefined });
|
|
65
|
+
log.debug('line 3', {}, { F: 'bar.ts', L: 3, S: undefined });
|
|
66
|
+
log.info('line 4', {}, { F: 'bar.ts', L: 4, S: undefined });
|
|
67
|
+
console.groupEnd();
|
|
68
|
+
|
|
69
|
+
expect(count, `Filter: "${test.filter}"`).toBe(test.expected);
|
|
70
|
+
remove();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
32
73
|
|
|
33
|
-
|
|
34
|
-
test('throws an error', function () {
|
|
74
|
+
test('throws an error', () => {
|
|
35
75
|
try {
|
|
36
76
|
throw new LogError('Test failed', { value: 1 });
|
|
37
77
|
} catch (err: any) {
|
|
@@ -39,7 +79,7 @@ describe('log', function () {
|
|
|
39
79
|
}
|
|
40
80
|
});
|
|
41
81
|
|
|
42
|
-
test('throws an error showing stacktrace',
|
|
82
|
+
test('throws an error showing stacktrace', () => {
|
|
43
83
|
try {
|
|
44
84
|
throw new LogError('Test failed', { value: 2 });
|
|
45
85
|
} catch (err: any) {
|
|
@@ -47,7 +87,7 @@ describe('log', function () {
|
|
|
47
87
|
}
|
|
48
88
|
});
|
|
49
89
|
|
|
50
|
-
test('catches an error',
|
|
90
|
+
test('catches an error', () => {
|
|
51
91
|
try {
|
|
52
92
|
throw new LogError('ERROR ON LINE 21', { value: 3 });
|
|
53
93
|
} catch (err: any) {
|
|
@@ -55,17 +95,7 @@ describe('log', function () {
|
|
|
55
95
|
}
|
|
56
96
|
});
|
|
57
97
|
|
|
58
|
-
test('config',
|
|
59
|
-
log.config({
|
|
60
|
-
filter: LogLevel.INFO,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
log.debug('Debug level log message');
|
|
64
|
-
log.info('Info level log message');
|
|
65
|
-
log.warn('Warn level log message');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('config file', function () {
|
|
98
|
+
test('config file', () => {
|
|
69
99
|
log.config({
|
|
70
100
|
file: path.join('packages/common/log/test-config.yml'),
|
|
71
101
|
});
|
|
@@ -75,7 +105,7 @@ describe('log', function () {
|
|
|
75
105
|
log.warn('Warn level log message');
|
|
76
106
|
});
|
|
77
107
|
|
|
78
|
-
test('levels',
|
|
108
|
+
test('levels', () => {
|
|
79
109
|
log('Default level log message');
|
|
80
110
|
log.debug('Debug level log message');
|
|
81
111
|
log.info('Info level log message');
|
|
@@ -83,10 +113,15 @@ describe('log', function () {
|
|
|
83
113
|
log.error('Error level log message');
|
|
84
114
|
});
|
|
85
115
|
|
|
86
|
-
test('context',
|
|
116
|
+
test('context', () => {
|
|
87
117
|
log.info('Message with context', {
|
|
88
118
|
title: 'test',
|
|
89
119
|
context: 123,
|
|
90
120
|
});
|
|
91
121
|
});
|
|
122
|
+
|
|
123
|
+
test('error', () => {
|
|
124
|
+
const myError = new Error('Test error', { cause: new Error('Cause') });
|
|
125
|
+
log.catch(myError);
|
|
126
|
+
});
|
|
92
127
|
});
|
package/src/log.ts
CHANGED
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type LogConfig, LogLevel, type LogOptions } from './config';
|
|
6
|
-
import { type LogContext, type LogProcessor } from './context';
|
|
6
|
+
import { type LogContext, LogEntry, type LogProcessor } from './context';
|
|
7
7
|
import { createFunctionLogDecorator, createMethodLogDecorator } from './decorators';
|
|
8
8
|
import { type CallMetadata } from './meta';
|
|
9
|
-
import {
|
|
9
|
+
import { createConfig } from './options';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Accessible from browser console.
|
|
13
|
+
* Example: `DX_LOG.config({ filter: 'ERROR' })`
|
|
14
|
+
* NOTE: File level filtering isn't supported in storybooks.
|
|
15
|
+
*/
|
|
16
|
+
declare global {
|
|
17
|
+
const DX_LOG: Log;
|
|
18
|
+
}
|
|
10
19
|
|
|
11
20
|
/**
|
|
12
21
|
* Logging function.
|
|
@@ -17,99 +26,185 @@ type LogFunction = (message: string, context?: LogContext, meta?: CallMetadata)
|
|
|
17
26
|
* Logging methods.
|
|
18
27
|
*/
|
|
19
28
|
export interface LogMethods {
|
|
29
|
+
config: (options?: LogOptions) => Log;
|
|
30
|
+
addProcessor: (processor: LogProcessor, addDefault?: boolean) => () => void;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Log at `trace` level.
|
|
34
|
+
*
|
|
35
|
+
* Generally not surfaced to the developer and not captured in a log file.
|
|
36
|
+
*/
|
|
20
37
|
trace: LogFunction;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Log at `debug` level.
|
|
41
|
+
* Generally not surfaced to the developer and captured in a log file.
|
|
42
|
+
*/
|
|
21
43
|
debug: LogFunction;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Log at `verbose` level.
|
|
47
|
+
* Generally not surfaced to the developer and not captured in a log file.
|
|
48
|
+
*/
|
|
22
49
|
verbose: LogFunction;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Log at `info` level.
|
|
53
|
+
* Generally surfaced to the developer and captured in a log file.
|
|
54
|
+
*/
|
|
23
55
|
info: LogFunction;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Log at `warn` level.
|
|
59
|
+
* Generally surfaced to the developer and captured in a log file.
|
|
60
|
+
*/
|
|
24
61
|
warn: LogFunction;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Log at `error` level.
|
|
65
|
+
* Generally surfaced to the developer and captured in a log file.
|
|
66
|
+
*/
|
|
25
67
|
error: LogFunction;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Log an error and its stack trace at an `error` level.
|
|
71
|
+
* Generally surfaced to the developer and captured in a log file.
|
|
72
|
+
*/
|
|
26
73
|
catch: (error: Error | any, context?: LogContext, meta?: CallMetadata) => void;
|
|
27
|
-
|
|
28
|
-
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Decorator to log method parameters and return value at the `info` level.
|
|
77
|
+
*/
|
|
29
78
|
method: (arg0?: never, arg1?: never, meta?: CallMetadata) => MethodDecorator;
|
|
30
|
-
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wrapper to log function parameters and return value at the `info` level.
|
|
82
|
+
*/
|
|
83
|
+
function: <F extends (...args: any[]) => any>(
|
|
31
84
|
name: string,
|
|
32
85
|
fn: F,
|
|
33
|
-
opts?: {
|
|
86
|
+
opts?: {
|
|
87
|
+
transformOutput?: (result: ReturnType<F>) => Promise<any> | any;
|
|
88
|
+
},
|
|
34
89
|
) => F;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Log a horizontal rule at the `info` level.
|
|
93
|
+
*/
|
|
94
|
+
break: () => void;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Log a stack trace at the `info` level.
|
|
98
|
+
*/
|
|
99
|
+
stack: (message?: string, context?: never, meta?: CallMetadata) => void;
|
|
35
100
|
}
|
|
36
101
|
|
|
37
102
|
/**
|
|
38
103
|
* Properties accessible on the logging function.
|
|
104
|
+
* @internal
|
|
39
105
|
*/
|
|
40
|
-
interface Log extends
|
|
41
|
-
|
|
42
|
-
addProcessor: (processor: LogProcessor) => void;
|
|
43
|
-
runtimeConfig: LogConfig;
|
|
106
|
+
export interface Log extends LogFunction, LogMethods {
|
|
107
|
+
readonly runtimeConfig: LogConfig;
|
|
44
108
|
}
|
|
45
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @internal
|
|
112
|
+
*/
|
|
46
113
|
interface LogImp extends Log {
|
|
114
|
+
_id: string;
|
|
47
115
|
_config: LogConfig;
|
|
48
116
|
}
|
|
49
117
|
|
|
50
|
-
|
|
51
|
-
const log: LogImp = ((...params) => processLog(LogLevel.DEBUG, ...params)) as LogImp;
|
|
52
|
-
|
|
53
|
-
log._config = getConfig();
|
|
54
|
-
Object.defineProperty(log, 'runtimeConfig', { get: () => log._config });
|
|
55
|
-
|
|
56
|
-
log.addProcessor = (processor: LogProcessor) => {
|
|
57
|
-
if (DEFAULT_PROCESSORS.filter((p) => p === processor).length === 0) {
|
|
58
|
-
DEFAULT_PROCESSORS.push(processor);
|
|
59
|
-
}
|
|
60
|
-
if (log._config.processors.filter((p) => p === processor).length === 0) {
|
|
61
|
-
log._config.processors.push(processor);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
118
|
+
let logCount = 0;
|
|
64
119
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
log.trace = (...params) => processLog(LogLevel.TRACE, ...params);
|
|
74
|
-
log.debug = (...params) => processLog(LogLevel.DEBUG, ...params);
|
|
75
|
-
log.verbose = (...params) => processLog(LogLevel.VERBOSE, ...params);
|
|
76
|
-
log.info = (...params) => processLog(LogLevel.INFO, ...params);
|
|
77
|
-
log.warn = (...params) => processLog(LogLevel.WARN, ...params);
|
|
78
|
-
log.error = (...params) => processLog(LogLevel.ERROR, ...params);
|
|
79
|
-
|
|
80
|
-
// Catch only shows error message, not stacktrace.
|
|
81
|
-
log.catch = (error: Error | any, context, meta) =>
|
|
82
|
-
processLog(LogLevel.ERROR, error?.message ?? String(error), context, meta, error);
|
|
83
|
-
|
|
84
|
-
// Show break.
|
|
85
|
-
log.break = () => log.info('——————————————————————————————————————————————————');
|
|
120
|
+
/**
|
|
121
|
+
* Create a logging function with properties.
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
export const createLog = (): LogImp => {
|
|
125
|
+
// Default function.
|
|
126
|
+
const log: LogImp = ((...params) => processLog(LogLevel.DEBUG, ...params)) as LogImp;
|
|
86
127
|
|
|
87
|
-
|
|
88
|
-
|
|
128
|
+
// Add private properties.
|
|
129
|
+
Object.assign<LogImp, Partial<LogImp>>(log, {
|
|
130
|
+
_id: `log-${++logCount}`,
|
|
131
|
+
_config: createConfig(),
|
|
132
|
+
});
|
|
89
133
|
|
|
90
|
-
|
|
91
|
-
log
|
|
134
|
+
// TODO(burdon): Document.
|
|
135
|
+
Object.defineProperty(log, 'runtimeConfig', {
|
|
136
|
+
get: () => log._config,
|
|
137
|
+
});
|
|
92
138
|
|
|
93
139
|
/**
|
|
94
140
|
* Process the current log call.
|
|
95
141
|
*/
|
|
96
142
|
const processLog = (
|
|
97
143
|
level: LogLevel,
|
|
98
|
-
message: string,
|
|
144
|
+
message: string | undefined,
|
|
99
145
|
context: LogContext = {},
|
|
100
146
|
meta?: CallMetadata,
|
|
101
147
|
error?: Error,
|
|
102
148
|
) => {
|
|
103
|
-
|
|
149
|
+
// TODO(burdon): Do the filter matching upstream (here) rather than in each processor?
|
|
150
|
+
const entry = new LogEntry({ level, message, context, meta, error });
|
|
151
|
+
log._config.processors.forEach((processor) => processor(log._config, entry));
|
|
104
152
|
};
|
|
105
153
|
|
|
154
|
+
/**
|
|
155
|
+
* API.
|
|
156
|
+
*/
|
|
157
|
+
Object.assign<Log, LogMethods>(log, {
|
|
158
|
+
/**
|
|
159
|
+
* Update config.
|
|
160
|
+
* NOTE: Preserves any processors that were already added to this logger instance
|
|
161
|
+
* unless an explicit processor option is provided.
|
|
162
|
+
*/
|
|
163
|
+
config: ({ processor, ...options } = {}) => {
|
|
164
|
+
const config = createConfig(options);
|
|
165
|
+
// TODO(burdon): This could be buggy since the behavior is not reentrant.
|
|
166
|
+
const processors = processor ? config.processors : log._config.processors;
|
|
167
|
+
log._config = { ...config, processors };
|
|
168
|
+
return log;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Adds a processor to the logger.
|
|
173
|
+
*/
|
|
174
|
+
addProcessor: (processor) => {
|
|
175
|
+
if (log._config.processors.filter((p) => p === processor).length === 0) {
|
|
176
|
+
log._config.processors.push(processor);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return () => {
|
|
180
|
+
log._config.processors = log._config.processors.filter((p) => p !== processor);
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
trace: (...params) => processLog(LogLevel.TRACE, ...params),
|
|
185
|
+
debug: (...params) => processLog(LogLevel.DEBUG, ...params),
|
|
186
|
+
verbose: (...params) => processLog(LogLevel.VERBOSE, ...params),
|
|
187
|
+
info: (...params) => processLog(LogLevel.INFO, ...params),
|
|
188
|
+
warn: (...params) => processLog(LogLevel.WARN, ...params),
|
|
189
|
+
error: (...params) => processLog(LogLevel.ERROR, ...params),
|
|
190
|
+
catch: (error, context, meta) => processLog(LogLevel.ERROR, undefined, context, meta, error),
|
|
191
|
+
|
|
192
|
+
method: createMethodLogDecorator(log),
|
|
193
|
+
function: createFunctionLogDecorator(log),
|
|
194
|
+
|
|
195
|
+
break: () => log.info('-'.repeat(80)),
|
|
196
|
+
stack: (message, context, meta) => {
|
|
197
|
+
return processLog(LogLevel.INFO, `${message ?? 'Stack Dump'}\n${getFormattedStackTrace()}`, context, meta);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
106
201
|
return log;
|
|
107
202
|
};
|
|
108
203
|
|
|
109
204
|
/**
|
|
110
205
|
* Global logging function.
|
|
111
206
|
*/
|
|
112
|
-
export const log: Log = ((globalThis as any).
|
|
207
|
+
export const log: Log = ((globalThis as any).DX_LOG ??= createLog());
|
|
113
208
|
|
|
114
209
|
const start = Date.now();
|
|
115
210
|
let last = start;
|
|
@@ -129,12 +224,4 @@ export const debug = (label?: any, args?: any) => {
|
|
|
129
224
|
last = Date.now();
|
|
130
225
|
};
|
|
131
226
|
|
|
132
|
-
/**
|
|
133
|
-
* Accessible from browser console.
|
|
134
|
-
*/
|
|
135
|
-
declare global {
|
|
136
|
-
// eslint-disable-next-line camelcase
|
|
137
|
-
const dx_log: Log;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
227
|
const getFormattedStackTrace = () => new Error().stack!.split('\n').slice(3).join('\n');
|
package/src/meta.ts
CHANGED
|
@@ -3,11 +3,27 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Marker key + value injected on every {@link CallMetadata} object by the log transform plugin.
|
|
7
|
+
* Used by {@link isLogMeta} to detect a log-meta argument at runtime (e.g. for variadic
|
|
8
|
+
* `param_index: 'last'` callees that don't have a fixed meta slot).
|
|
9
|
+
*/
|
|
10
|
+
export const LOG_META_MARKER = '~LogMeta';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Metadata injected by the log transform plugin.
|
|
7
14
|
*
|
|
8
15
|
* Field names are intentionally short to reduce the size of the generated code.
|
|
9
16
|
*/
|
|
10
17
|
export interface CallMetadata {
|
|
18
|
+
/**
|
|
19
|
+
* Marker tag — when present, equal to {@link LOG_META_MARKER} ({@link `'~LogMeta'`}).
|
|
20
|
+
* Injected by the log transform plugin on every emitted meta object so that {@link isLogMeta}
|
|
21
|
+
* can distinguish a meta argument from a regular user-supplied value at runtime.
|
|
22
|
+
* Optional because hand-written `CallMetadata` literals (decorators, RPC mappers, tests)
|
|
23
|
+
* don't need the marker — they are recognized by position in the call signature.
|
|
24
|
+
*/
|
|
25
|
+
'~LogMeta'?: typeof LOG_META_MARKER;
|
|
26
|
+
|
|
11
27
|
/**
|
|
12
28
|
* File name.
|
|
13
29
|
*/
|
|
@@ -35,3 +51,15 @@ export interface CallMetadata {
|
|
|
35
51
|
*/
|
|
36
52
|
A?: string[];
|
|
37
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Type guard: `true` when `value` is a {@link CallMetadata} object emitted by the log transform plugin.
|
|
57
|
+
* Detection is based on the presence of the {@link LOG_META_MARKER} marker key/value.
|
|
58
|
+
*/
|
|
59
|
+
export const isLogMeta = (value: unknown): value is CallMetadata => {
|
|
60
|
+
return (
|
|
61
|
+
value != null &&
|
|
62
|
+
typeof value === 'object' &&
|
|
63
|
+
(value as Record<string, unknown>)[LOG_META_MARKER] === LOG_META_MARKER
|
|
64
|
+
);
|
|
65
|
+
};
|