@dxos/log 0.8.4-main.f9ba587 → 0.8.4-main.fcfe5033a5
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/dist/lib/browser/chunk-IEP6GGEX.mjs +23 -0
- package/dist/lib/browser/chunk-IEP6GGEX.mjs.map +7 -0
- package/dist/lib/browser/chunk-M2YHSBML.mjs +133 -0
- package/dist/lib/browser/chunk-M2YHSBML.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +227 -184
- 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 +107 -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-62VKC2WQ.mjs +135 -0
- package/dist/lib/node-esm/chunk-62VKC2WQ.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +224 -266
- 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 +108 -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 +1 -1
- 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/index.d.ts +4 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/log-buffer.d.ts +40 -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 +14 -18
- package/dist/types/src/log.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/index.d.ts +1 -1
- package/dist/types/src/platform/index.d.ts.map +1 -1
- package/dist/types/src/processors/browser-processor.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/tsconfig.tsbuildinfo +1 -1
- package/package.json +32 -14
- package/src/config.ts +3 -2
- package/src/context.ts +38 -8
- package/src/dbg.ts +34 -0
- package/src/decorators.ts +3 -3
- package/src/experimental/classes.test.ts +1 -1
- package/src/index.ts +4 -4
- package/src/log-buffer.test.ts +155 -0
- package/src/log-buffer.ts +117 -0
- package/src/log.test.ts +58 -23
- package/src/log.ts +104 -59
- package/src/options.ts +27 -11
- package/src/platform/index.ts +1 -1
- package/src/processors/browser-processor.ts +6 -2
- package/src/processors/console-processor.ts +8 -6
- package/src/processors/file-processor.ts +3 -1
- package/src/processors/index.ts +3 -3
- package/src/scope.ts +1 -1
package/package.json
CHANGED
|
@@ -1,27 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/log",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.fcfe5033a5",
|
|
4
4
|
"description": "Logger",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
7
11
|
"license": "MIT",
|
|
8
12
|
"author": "DXOS.org",
|
|
9
13
|
"sideEffects": true,
|
|
10
14
|
"type": "module",
|
|
15
|
+
"imports": {
|
|
16
|
+
"#platform": {
|
|
17
|
+
"source": {
|
|
18
|
+
"browser": "./src/platform/browser/index.ts",
|
|
19
|
+
"default": "./src/platform/node/index.ts"
|
|
20
|
+
},
|
|
21
|
+
"types": "./dist/types/src/platform/node/index.d.ts",
|
|
22
|
+
"browser": "./dist/lib/browser/platform/browser/index.mjs",
|
|
23
|
+
"default": "./dist/lib/node-esm/platform/node/index.mjs"
|
|
24
|
+
},
|
|
25
|
+
"#console-processor": {
|
|
26
|
+
"source": {
|
|
27
|
+
"browser": "./src/processors/console-stub.ts",
|
|
28
|
+
"default": "./src/processors/console-processor.ts"
|
|
29
|
+
},
|
|
30
|
+
"types": "./dist/types/src/processors/console-processor.d.ts",
|
|
31
|
+
"browser": "./dist/lib/browser/processors/console-stub.mjs",
|
|
32
|
+
"default": "./dist/lib/node-esm/processors/console-processor.mjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
11
35
|
"exports": {
|
|
12
36
|
".": {
|
|
37
|
+
"types": "./dist/types/src/index.d.ts",
|
|
13
38
|
"browser": "./dist/lib/browser/index.mjs",
|
|
14
39
|
"node": {
|
|
15
40
|
"require": "./dist/lib/node/index.cjs",
|
|
16
41
|
"default": "./dist/lib/node-esm/index.mjs"
|
|
17
|
-
}
|
|
18
|
-
"types": "./dist/types/src/index.d.ts"
|
|
42
|
+
}
|
|
19
43
|
}
|
|
20
44
|
},
|
|
21
|
-
"browser": {
|
|
22
|
-
"./src/platform/node/index.ts": "./src/platform/browser/index.ts",
|
|
23
|
-
"./src/processors/console-processor.ts": "./src/processors/console-stub.ts"
|
|
24
|
-
},
|
|
25
45
|
"types": "dist/types/src/index.d.ts",
|
|
26
46
|
"typesVersions": {
|
|
27
47
|
"*": {}
|
|
@@ -31,17 +51,15 @@
|
|
|
31
51
|
"src"
|
|
32
52
|
],
|
|
33
53
|
"dependencies": {
|
|
34
|
-
"chalk": "^4.1.
|
|
35
|
-
"js-yaml": "
|
|
54
|
+
"chalk": "^4.1.2",
|
|
55
|
+
"js-yaml": "4.1.1",
|
|
36
56
|
"lodash.defaultsdeep": "^4.6.1",
|
|
37
|
-
"
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/util": "0.8.4-main.f9ba587"
|
|
57
|
+
"@dxos/node-std": "0.8.4-main.fcfe5033a5",
|
|
58
|
+
"@dxos/util": "0.8.4-main.fcfe5033a5"
|
|
40
59
|
},
|
|
41
60
|
"devDependencies": {
|
|
42
61
|
"@types/js-yaml": "^4.0.5",
|
|
43
|
-
"@types/lodash.defaultsdeep": "^4.6.6"
|
|
44
|
-
"@types/lodash.omit": "^4.5.7"
|
|
62
|
+
"@types/lodash.defaultsdeep": "^4.6.6"
|
|
45
63
|
},
|
|
46
64
|
"publishConfig": {
|
|
47
65
|
"access": "public"
|
package/src/config.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { type LogProcessor } from './context';
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Standard levels.
|
|
9
|
+
* NOTE: Keep aligned with LogLevel in @dxos/protocols.
|
|
9
10
|
*/
|
|
10
|
-
// NOTE: Keep aligned with LogLevel in @dxos/protocols.
|
|
11
11
|
// TODO(burdon): Update numbers?
|
|
12
12
|
export enum LogLevel {
|
|
13
13
|
TRACE = 5,
|
|
@@ -18,7 +18,8 @@ export enum LogLevel {
|
|
|
18
18
|
ERROR = 14,
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export const levels:
|
|
21
|
+
export const levels: Record<string, LogLevel> = {
|
|
22
|
+
'*': LogLevel.TRACE,
|
|
22
23
|
trace: LogLevel.TRACE,
|
|
23
24
|
debug: LogLevel.DEBUG,
|
|
24
25
|
verbose: LogLevel.VERBOSE,
|
package/src/context.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type LogContext = Record<string, any> | Error | any;
|
|
|
16
16
|
*/
|
|
17
17
|
export interface LogEntry {
|
|
18
18
|
level: LogLevel;
|
|
19
|
-
message
|
|
19
|
+
message?: string;
|
|
20
20
|
context?: LogContext;
|
|
21
21
|
meta?: CallMetadata;
|
|
22
22
|
error?: Error;
|
|
@@ -27,8 +27,33 @@ export interface LogEntry {
|
|
|
27
27
|
*/
|
|
28
28
|
export type LogProcessor = (config: LogConfig, entry: LogEntry) => void;
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Returns:
|
|
32
|
+
* true if the log entry matches the filter,
|
|
33
|
+
* false if should be excluded, or
|
|
34
|
+
* undefined if it the filter doesn't match the level.
|
|
35
|
+
*/
|
|
36
|
+
const matchFilter = (filter: LogFilter, level: LogLevel, path?: string): boolean | undefined => {
|
|
37
|
+
// TODO(burdon): Support regexp.
|
|
38
|
+
if (filter.pattern?.startsWith('-')) {
|
|
39
|
+
// Exclude.
|
|
40
|
+
if (path?.includes(filter.pattern.slice(1))) {
|
|
41
|
+
if (level >= filter.level) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
// Include.
|
|
47
|
+
if (filter.pattern?.length) {
|
|
48
|
+
if (path?.includes(filter.pattern)) {
|
|
49
|
+
return level >= filter.level;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
if (level >= filter.level) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
32
57
|
};
|
|
33
58
|
|
|
34
59
|
/**
|
|
@@ -36,10 +61,16 @@ const matchFilter = (filter: LogFilter, level: LogLevel, path: string) => {
|
|
|
36
61
|
*/
|
|
37
62
|
export const shouldLog = (entry: LogEntry, filters?: LogFilter[]): boolean => {
|
|
38
63
|
if (filters === undefined) {
|
|
39
|
-
return
|
|
40
|
-
} else {
|
|
41
|
-
return filters.some((filter) => matchFilter(filter, entry.level, entry.meta?.F ?? ''));
|
|
64
|
+
return false;
|
|
42
65
|
}
|
|
66
|
+
|
|
67
|
+
const results = filters
|
|
68
|
+
.map((filter) => matchFilter(filter, entry.level, entry.meta?.F))
|
|
69
|
+
.filter((result): result is boolean => result !== undefined);
|
|
70
|
+
|
|
71
|
+
// Skip if any are explicitely false.
|
|
72
|
+
// console.log({ level: entry.level, path: entry.meta?.F }, filters, results, results.length);
|
|
73
|
+
return results.length > 0 && !results.some((results) => results === false);
|
|
43
74
|
};
|
|
44
75
|
|
|
45
76
|
export const getContextFromEntry = (entry: LogEntry): Record<string, any> | undefined => {
|
|
@@ -64,8 +95,7 @@ export const getContextFromEntry = (entry: LogEntry): Record<string, any> | unde
|
|
|
64
95
|
}
|
|
65
96
|
|
|
66
97
|
if (entry.error) {
|
|
67
|
-
|
|
68
|
-
context = Object.assign(context ?? {}, { error: entry.error, ...errorContext });
|
|
98
|
+
context = Object.assign(context ?? {}, { error: entry.error });
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
return context && Object.keys(context).length > 0 ? context : undefined;
|
package/src/dbg.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type CallMetadata } from './meta';
|
|
6
|
+
/**
|
|
7
|
+
* Debug-log value to console.
|
|
8
|
+
* Log's the expression being evaluated.
|
|
9
|
+
*
|
|
10
|
+
* If only one argument is provided, it will also be returned.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* dbg(foo, bar);
|
|
15
|
+
* // foo = 1
|
|
16
|
+
* // bar = 2
|
|
17
|
+
*
|
|
18
|
+
* bar = dbg(foo * 2);
|
|
19
|
+
* // foo * 2 = 2
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* NOTE: The second argument is injected by the log transform plugin.
|
|
23
|
+
*/
|
|
24
|
+
export const dbg: {
|
|
25
|
+
<T>(value: T, _meta?: CallMetadata): T;
|
|
26
|
+
} = <T>(arg: T, meta?: CallMetadata): T => {
|
|
27
|
+
if (meta?.A) {
|
|
28
|
+
console.log(`${meta.A[0]} =`, arg);
|
|
29
|
+
} else {
|
|
30
|
+
console.log(arg);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return arg;
|
|
34
|
+
};
|
package/src/decorators.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { inspect } from 'node:util';
|
|
7
7
|
|
|
8
|
-
import type
|
|
8
|
+
import { type LogMethods } from './log';
|
|
9
9
|
import { type CallMetadata } from './meta';
|
|
10
10
|
|
|
11
11
|
let nextPromiseId = 0;
|
|
@@ -190,13 +190,13 @@ const logAsyncRejected = (
|
|
|
190
190
|
);
|
|
191
191
|
};
|
|
192
192
|
|
|
193
|
+
const COLOR_FUNCTION = [220, 220, 170] as const;
|
|
194
|
+
|
|
193
195
|
// https://github.com/dxos/dxos/issues/7286
|
|
194
196
|
const greenCheck = typeof chalk.green === 'function' ? chalk.green('✔') : '✔';
|
|
195
197
|
|
|
196
198
|
const formatTimeElapsed = (startTime: number) => chalk.gray(`${(performance.now() - startTime).toFixed(0)}ms`);
|
|
197
199
|
|
|
198
|
-
const COLOR_FUNCTION = [220, 220, 170] as const;
|
|
199
|
-
|
|
200
200
|
const formatFunction = (name: string) => chalk.bold(chalk.rgb(...COLOR_FUNCTION)(name));
|
|
201
201
|
|
|
202
202
|
const formatPromise = (id: number) => chalk.blue(`Promise#${id}`);
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, test } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import { debugInfo, ownershipClass } from './ownership';
|
|
8
7
|
import { log } from '../log';
|
|
8
|
+
import { debugInfo, ownershipClass } from './ownership';
|
|
9
9
|
|
|
10
10
|
describe('classes', function () {
|
|
11
11
|
test('field instance', function () {
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import omit from '
|
|
6
|
-
|
|
7
|
-
import { pick } from '@dxos/util';
|
|
5
|
+
import { omit, pick } from '@dxos/util';
|
|
8
6
|
|
|
9
7
|
export { omit, pick };
|
|
10
8
|
|
|
@@ -14,6 +12,8 @@ export * from './log';
|
|
|
14
12
|
export { parseFilter } from './options';
|
|
15
13
|
export * from './processors';
|
|
16
14
|
export * from './scope';
|
|
17
|
-
export * from './meta';
|
|
15
|
+
export type * from './meta';
|
|
16
|
+
export { dbg } from './dbg';
|
|
17
|
+
export * from './log-buffer';
|
|
18
18
|
|
|
19
19
|
export { getCurrentOwnershipScope } from './experimental/ownership';
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { type LogConfig, type LogEntry, 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<LogEntry> = {}): LogEntry => ({
|
|
17
|
+
level: LogLevel.INFO,
|
|
18
|
+
message: 'test message',
|
|
19
|
+
...overrides,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('LogBuffer', () => {
|
|
23
|
+
test('pushes and serializes log entries', ({ expect }) => {
|
|
24
|
+
const buffer = new LogBuffer(10);
|
|
25
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'hello' }));
|
|
26
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'world' }));
|
|
27
|
+
|
|
28
|
+
expect(buffer.size).toBe(2);
|
|
29
|
+
const lines = buffer.serialize().split('\n');
|
|
30
|
+
expect(lines).toHaveLength(2);
|
|
31
|
+
|
|
32
|
+
const first = JSON.parse(lines[0]);
|
|
33
|
+
expect(first.m).toBe('hello');
|
|
34
|
+
expect(first.l).toBe('I');
|
|
35
|
+
expect(first.t).toBeDefined();
|
|
36
|
+
|
|
37
|
+
const second = JSON.parse(lines[1]);
|
|
38
|
+
expect(second.m).toBe('world');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('evicts oldest entries when buffer is full', ({ expect }) => {
|
|
42
|
+
const buffer = new LogBuffer(3);
|
|
43
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'a' }));
|
|
44
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'b' }));
|
|
45
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'c' }));
|
|
46
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'd' }));
|
|
47
|
+
|
|
48
|
+
expect(buffer.size).toBe(3);
|
|
49
|
+
const lines = buffer.serialize().split('\n');
|
|
50
|
+
const messages = lines.map((line) => JSON.parse(line).m);
|
|
51
|
+
expect(messages).toEqual(['b', 'c', 'd']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('skips TRACE-level logs', ({ expect }) => {
|
|
55
|
+
const buffer = new LogBuffer(10);
|
|
56
|
+
buffer.logProcessor(baseConfig, createEntry({ level: LogLevel.TRACE, message: 'trace' }));
|
|
57
|
+
expect(buffer.size).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('captures DEBUG-level and above', ({ expect }) => {
|
|
61
|
+
const buffer = new LogBuffer(10);
|
|
62
|
+
buffer.logProcessor(baseConfig, createEntry({ level: LogLevel.DEBUG, message: 'debug' }));
|
|
63
|
+
buffer.logProcessor(baseConfig, createEntry({ level: LogLevel.WARN, message: 'warn' }));
|
|
64
|
+
buffer.logProcessor(baseConfig, createEntry({ level: LogLevel.ERROR, message: 'error' }));
|
|
65
|
+
|
|
66
|
+
expect(buffer.size).toBe(3);
|
|
67
|
+
const lines = buffer.serialize().split('\n');
|
|
68
|
+
expect(JSON.parse(lines[0]).l).toBe('D');
|
|
69
|
+
expect(JSON.parse(lines[1]).l).toBe('W');
|
|
70
|
+
expect(JSON.parse(lines[2]).l).toBe('E');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('captures file and line metadata', ({ expect }) => {
|
|
74
|
+
const buffer = new LogBuffer(10);
|
|
75
|
+
buffer.logProcessor(
|
|
76
|
+
baseConfig,
|
|
77
|
+
createEntry({
|
|
78
|
+
meta: { F: '/home/user/project/packages/sdk/test.ts', L: 42, S: undefined },
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const lines = buffer.serialize().split('\n');
|
|
83
|
+
const record = JSON.parse(lines[0]);
|
|
84
|
+
expect(record.f).toBe('packages/sdk/test.ts');
|
|
85
|
+
expect(record.n).toBe(42);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('captures error stack', ({ expect }) => {
|
|
89
|
+
const buffer = new LogBuffer(10);
|
|
90
|
+
const error = new Error('boom');
|
|
91
|
+
buffer.logProcessor(baseConfig, createEntry({ error }));
|
|
92
|
+
|
|
93
|
+
const lines = buffer.serialize().split('\n');
|
|
94
|
+
const record = JSON.parse(lines[0]);
|
|
95
|
+
expect(record.e).toContain('boom');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('truncates context to 500 chars', ({ expect }) => {
|
|
99
|
+
const buffer = new LogBuffer(10);
|
|
100
|
+
const longValue = 'x'.repeat(1000);
|
|
101
|
+
buffer.logProcessor(baseConfig, createEntry({ context: { data: longValue } }));
|
|
102
|
+
|
|
103
|
+
const lines = buffer.serialize().split('\n');
|
|
104
|
+
const record = JSON.parse(lines[0]);
|
|
105
|
+
expect(record.c).toBeDefined();
|
|
106
|
+
expect(record.c!.length).toBe(500);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('skips Error context objects', ({ expect }) => {
|
|
110
|
+
const buffer = new LogBuffer(10);
|
|
111
|
+
buffer.logProcessor(baseConfig, createEntry({ context: new Error('ctx error') }));
|
|
112
|
+
|
|
113
|
+
const lines = buffer.serialize().split('\n');
|
|
114
|
+
const record = JSON.parse(lines[0]);
|
|
115
|
+
expect(record.c).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('handles non-serializable context gracefully', ({ expect }) => {
|
|
119
|
+
const buffer = new LogBuffer(10);
|
|
120
|
+
const circular: Record<string, any> = {};
|
|
121
|
+
circular.self = circular;
|
|
122
|
+
buffer.logProcessor(baseConfig, createEntry({ context: circular }));
|
|
123
|
+
|
|
124
|
+
const lines = buffer.serialize().split('\n');
|
|
125
|
+
const record = JSON.parse(lines[0]);
|
|
126
|
+
expect(record.c).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('serialize returns empty string for empty buffer', ({ expect }) => {
|
|
130
|
+
const buffer = new LogBuffer(10);
|
|
131
|
+
expect(buffer.serialize()).toBe('');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('clear discards all entries', ({ expect }) => {
|
|
135
|
+
const buffer = new LogBuffer(10);
|
|
136
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'a' }));
|
|
137
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'b' }));
|
|
138
|
+
expect(buffer.size).toBe(2);
|
|
139
|
+
|
|
140
|
+
buffer.clear();
|
|
141
|
+
expect(buffer.size).toBe(0);
|
|
142
|
+
expect(buffer.serialize()).toBe('');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('clear allows new entries', ({ expect }) => {
|
|
146
|
+
const buffer = new LogBuffer(10);
|
|
147
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'old' }));
|
|
148
|
+
buffer.clear();
|
|
149
|
+
buffer.logProcessor(baseConfig, createEntry({ message: 'new' }));
|
|
150
|
+
|
|
151
|
+
expect(buffer.size).toBe(1);
|
|
152
|
+
const record = JSON.parse(buffer.serialize());
|
|
153
|
+
expect(record.m).toBe('new');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { CircularBuffer, getDebugName } from '@dxos/util';
|
|
6
|
+
|
|
7
|
+
import { type LogConfig, LogLevel, shortLevelName } from './config';
|
|
8
|
+
import { type LogEntry, type LogProcessor } from './context';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_BUFFER_SIZE = 2_000;
|
|
11
|
+
const MAX_CONTEXT_LENGTH = 500;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compact log record with short property names for small serialized size.
|
|
15
|
+
*/
|
|
16
|
+
export type LogRecord = {
|
|
17
|
+
/** ISO timestamp. */
|
|
18
|
+
t: string;
|
|
19
|
+
/** Level letter (D, V, I, W, E). */
|
|
20
|
+
l: string;
|
|
21
|
+
/** Message. */
|
|
22
|
+
m: string;
|
|
23
|
+
/** File path. */
|
|
24
|
+
f?: string;
|
|
25
|
+
/** Line number. */
|
|
26
|
+
n?: number;
|
|
27
|
+
/* Object from which the log was emitted. */
|
|
28
|
+
o?: string;
|
|
29
|
+
/** Error stack. */
|
|
30
|
+
e?: string;
|
|
31
|
+
/** Context JSON. */
|
|
32
|
+
c?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Captures recent log entries in a circular buffer for debug log dump.
|
|
37
|
+
*/
|
|
38
|
+
export class LogBuffer {
|
|
39
|
+
private readonly _buffer: CircularBuffer<LogRecord>;
|
|
40
|
+
|
|
41
|
+
constructor(size = DEFAULT_BUFFER_SIZE) {
|
|
42
|
+
this._buffer = new CircularBuffer<LogRecord>(size);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Log processor that can be registered with `log.runtimeConfig.processors`.
|
|
47
|
+
* Captures every level except TRACE (does not apply `shouldLog` / filter; use for full debug dumps).
|
|
48
|
+
*/
|
|
49
|
+
readonly logProcessor: LogProcessor = (_config: LogConfig, entry: LogEntry) => {
|
|
50
|
+
if (entry.level <= LogLevel.TRACE) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const record: LogRecord = {
|
|
55
|
+
t: new Date().toISOString(),
|
|
56
|
+
l: shortLevelName[entry.level] ?? '?',
|
|
57
|
+
m: entry.message ?? '',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (entry.meta) {
|
|
61
|
+
record.f = getRelativeFilename(entry.meta.F);
|
|
62
|
+
record.n = entry.meta.L;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (entry.error) {
|
|
66
|
+
record.e = entry.error.stack ?? entry.error.message;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (entry.context != null) {
|
|
70
|
+
try {
|
|
71
|
+
const ctx = typeof entry.context === 'function' ? entry.context() : entry.context;
|
|
72
|
+
if (ctx != null && !(ctx instanceof Error)) {
|
|
73
|
+
let json = JSON.stringify(ctx);
|
|
74
|
+
if (json.length > MAX_CONTEXT_LENGTH) {
|
|
75
|
+
json = json.slice(0, MAX_CONTEXT_LENGTH);
|
|
76
|
+
}
|
|
77
|
+
record.c = json;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Skip context that throws or is non-serializable.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const scope = entry.meta?.S;
|
|
84
|
+
if (typeof scope === 'object' && scope !== null && Object.getPrototypeOf(scope) !== Object.prototype) {
|
|
85
|
+
record.o = getDebugName(scope);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this._buffer.push(record);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Number of entries currently in the buffer. */
|
|
92
|
+
get size(): number {
|
|
93
|
+
return this._buffer.elementCount;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Discard all buffered entries. */
|
|
97
|
+
clear(): void {
|
|
98
|
+
this._buffer.clear();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Serialize buffer contents as NDJSON (newline-delimited JSON). */
|
|
102
|
+
serialize(): string {
|
|
103
|
+
const lines: string[] = [];
|
|
104
|
+
for (const record of this._buffer) {
|
|
105
|
+
lines.push(JSON.stringify(record));
|
|
106
|
+
}
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const getRelativeFilename = (filename: string): string => {
|
|
112
|
+
const match = filename.match(/.+\/(packages\/.+\/.+)/);
|
|
113
|
+
if (match) {
|
|
114
|
+
return match[1];
|
|
115
|
+
}
|
|
116
|
+
return filename;
|
|
117
|
+
};
|
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
|
});
|