@dxos/log 0.8.4-main.fcfe5033a5 → 0.8.4-main.fdfb99ef29
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-V7FYKT4H.mjs +311 -0
- package/dist/lib/browser/chunk-V7FYKT4H.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +171 -62
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/platform/node/index.mjs.map +2 -2
- package/dist/lib/browser/processors/console-processor.mjs +5 -10
- package/dist/lib/browser/processors/console-processor.mjs.map +3 -3
- 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 +171 -62
- 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/node/index.mjs.map +2 -2
- package/dist/lib/node-esm/processors/console-processor.mjs +5 -10
- package/dist/lib/node-esm/processors/console-processor.mjs.map +3 -3
- package/dist/types/src/context.d.ts +78 -2
- package/dist/types/src/context.d.ts.map +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 +4 -1
- 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 +0 -20
- package/dist/types/src/log-buffer.d.ts.map +1 -1
- package/dist/types/src/log.d.ts +41 -0
- 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/platform/browser/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.map +1 -1
- package/dist/types/src/processors/file-processor.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 +3 -6
- package/src/context.ts +242 -2
- package/src/environment.test.ts +222 -0
- package/src/environment.ts +129 -0
- package/src/index.ts +4 -1
- package/src/jsonl.test.ts +121 -0
- package/src/jsonl.ts +104 -0
- package/src/log-buffer.test.ts +10 -7
- package/src/log-buffer.ts +21 -49
- package/src/log.ts +52 -10
- package/src/meta.ts +29 -1
- package/src/processors/browser-processor.ts +27 -28
- package/src/processors/console-processor.ts +4 -10
- package/src/processors/file-processor.ts +7 -8
- package/dist/lib/browser/chunk-M2YHSBML.mjs +0 -133
- package/dist/lib/browser/chunk-M2YHSBML.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-62VKC2WQ.mjs +0 -135
- package/dist/lib/node-esm/chunk-62VKC2WQ.mjs.map +0 -7
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { LogEntry, type LogEntryInit, LogLevel } from './index';
|
|
8
|
+
import { serializeToJsonl } from './jsonl';
|
|
9
|
+
|
|
10
|
+
const createEntry = (overrides: Partial<LogEntryInit> = {}): LogEntry =>
|
|
11
|
+
new LogEntry({
|
|
12
|
+
level: LogLevel.INFO,
|
|
13
|
+
message: 'test message',
|
|
14
|
+
...overrides,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const parseLine = (line: string | undefined) => {
|
|
18
|
+
if (line === undefined) {
|
|
19
|
+
throw new Error('expected line, got undefined');
|
|
20
|
+
}
|
|
21
|
+
return JSON.parse(line) as Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('serializeToJsonl', () => {
|
|
25
|
+
test('returns undefined for TRACE level entries', ({ expect }) => {
|
|
26
|
+
expect(serializeToJsonl(createEntry({ level: LogLevel.TRACE }))).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('emits compact record with t/l/m fields', ({ expect }) => {
|
|
30
|
+
const record = parseLine(serializeToJsonl(createEntry({ message: 'hello' })));
|
|
31
|
+
expect(record.t).toBeTypeOf('string');
|
|
32
|
+
expect(record.l).toBe('I');
|
|
33
|
+
expect(record.m).toBe('hello');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('uses level letters D V I W E', ({ expect }) => {
|
|
37
|
+
const levels: Array<[LogLevel, string]> = [
|
|
38
|
+
[LogLevel.DEBUG, 'D'],
|
|
39
|
+
[LogLevel.VERBOSE, 'V'],
|
|
40
|
+
[LogLevel.INFO, 'I'],
|
|
41
|
+
[LogLevel.WARN, 'W'],
|
|
42
|
+
[LogLevel.ERROR, 'E'],
|
|
43
|
+
];
|
|
44
|
+
for (const [level, letter] of levels) {
|
|
45
|
+
const record = parseLine(serializeToJsonl(createEntry({ level, message: '' })));
|
|
46
|
+
expect(record.l).toBe(letter);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('omits optional fields when not present', ({ expect }) => {
|
|
51
|
+
const record = parseLine(serializeToJsonl(createEntry({ message: 'plain' })));
|
|
52
|
+
expect(record.f).toBeUndefined();
|
|
53
|
+
expect(record.n).toBeUndefined();
|
|
54
|
+
expect(record.o).toBeUndefined();
|
|
55
|
+
expect(record.e).toBeUndefined();
|
|
56
|
+
expect(record.c).toBeUndefined();
|
|
57
|
+
expect(record.i).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('captures filename and line metadata', ({ expect }) => {
|
|
61
|
+
const record = parseLine(
|
|
62
|
+
serializeToJsonl(
|
|
63
|
+
createEntry({
|
|
64
|
+
meta: { F: '/home/user/project/packages/sdk/test.ts', L: 42, S: undefined },
|
|
65
|
+
}),
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
expect(record.f).toBe('packages/sdk/test.ts');
|
|
69
|
+
expect(record.n).toBe(42);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('captures error stack via computedError', ({ expect }) => {
|
|
73
|
+
const error = new Error('boom');
|
|
74
|
+
const record = parseLine(serializeToJsonl(createEntry({ error })));
|
|
75
|
+
expect(record.e).toContain('boom');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('captures context as flat JSON string', ({ expect }) => {
|
|
79
|
+
const record = parseLine(serializeToJsonl(createEntry({ context: { count: 3, name: 'x' } })));
|
|
80
|
+
expect(record.c).toBeTypeOf('string');
|
|
81
|
+
const ctx = JSON.parse(record.c as string);
|
|
82
|
+
expect(ctx.count).toBe(3);
|
|
83
|
+
expect(ctx.name).toBe('x');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('flattens nested objects in context (one level)', ({ expect }) => {
|
|
87
|
+
const record = parseLine(serializeToJsonl(createEntry({ context: { nested: { a: 1, b: 2 } } })));
|
|
88
|
+
const ctx = JSON.parse(record.c as string);
|
|
89
|
+
// computedContext converts nested objects to JSON strings via stringifyOneLevel.
|
|
90
|
+
expect(typeof ctx.nested).toBe('string');
|
|
91
|
+
expect(ctx.nested).toBe('{"a":1,"b":2}');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('does not truncate long context (idb-store style)', ({ expect }) => {
|
|
95
|
+
const longValue = 'x'.repeat(2_000);
|
|
96
|
+
const record = parseLine(serializeToJsonl(createEntry({ context: { data: longValue } })));
|
|
97
|
+
const ctx = JSON.parse(record.c as string);
|
|
98
|
+
expect(ctx.data).toBe(longValue);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('embeds env identifier in `i` field', ({ expect }) => {
|
|
102
|
+
const record = parseLine(
|
|
103
|
+
serializeToJsonl(createEntry({ message: 'm' }), { env: 'tab:http://localhost:5173:abc123' }),
|
|
104
|
+
);
|
|
105
|
+
expect(record.i).toBe('tab:http://localhost:5173:abc123');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('omits `i` when env is not provided', ({ expect }) => {
|
|
109
|
+
const record = parseLine(serializeToJsonl(createEntry({ message: 'm' })));
|
|
110
|
+
expect(record.i).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('output is valid single-line JSON (no embedded newlines)', ({ expect }) => {
|
|
114
|
+
const line = serializeToJsonl(createEntry({ message: 'multi\nline\nmessage' }));
|
|
115
|
+
expect(line).toBeDefined();
|
|
116
|
+
expect(line!.indexOf('\n')).toBe(-1);
|
|
117
|
+
// The newlines inside the message are escaped by JSON.stringify.
|
|
118
|
+
const record = JSON.parse(line!);
|
|
119
|
+
expect(record.m).toBe('multi\nline\nmessage');
|
|
120
|
+
});
|
|
121
|
+
});
|
package/src/jsonl.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { LogLevel, shortLevelName } from './config';
|
|
6
|
+
import { type LogEntry } from './context';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compact JSONL log record with short property names for small serialized size.
|
|
10
|
+
*
|
|
11
|
+
* Field names are intentionally one character to keep on-disk / on-wire size minimal —
|
|
12
|
+
* a single record can be emitted thousands of times per session.
|
|
13
|
+
*/
|
|
14
|
+
export type LogRecord = {
|
|
15
|
+
/** ISO timestamp. */
|
|
16
|
+
t: string;
|
|
17
|
+
/** Level letter (D, V, I, W, E). */
|
|
18
|
+
l: string;
|
|
19
|
+
/** Message. */
|
|
20
|
+
m: string;
|
|
21
|
+
/** File path. */
|
|
22
|
+
f?: string;
|
|
23
|
+
/** Line number. */
|
|
24
|
+
n?: number;
|
|
25
|
+
/** Object/scope from which the log was emitted. */
|
|
26
|
+
o?: string;
|
|
27
|
+
/** Error stack. */
|
|
28
|
+
e?: string;
|
|
29
|
+
/** Context JSON (already a string of pre-stringified flat key/values). */
|
|
30
|
+
c?: string;
|
|
31
|
+
/** Environment identifier (see {@link inferEnvironmentName}). */
|
|
32
|
+
i?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for {@link serializeToJsonl}.
|
|
37
|
+
*/
|
|
38
|
+
export type SerializeToJsonlOptions = {
|
|
39
|
+
/**
|
|
40
|
+
* Environment identifier embedded in the record's `i` field.
|
|
41
|
+
* Use {@link inferEnvironmentName} for a default that disambiguates tabs and workers.
|
|
42
|
+
*/
|
|
43
|
+
env?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Serialize a {@link LogEntry} to a single compact JSON line (no trailing newline).
|
|
48
|
+
*
|
|
49
|
+
* Returns `undefined` for entries at TRACE level — callers can use this to skip them.
|
|
50
|
+
*
|
|
51
|
+
* The output is compatible with newline-delimited JSON (NDJSON / JSONL): join multiple
|
|
52
|
+
* results with `\n` for a stream / file representation.
|
|
53
|
+
*
|
|
54
|
+
* Context is taken from {@link LogEntry.computedContext}, which has already flattened
|
|
55
|
+
* nested objects to strings (see `stringifyOneLevel` in `context.ts`), so the resulting
|
|
56
|
+
* `c` field is a JSON string of a flat `Record<string, primitive>` map. The function
|
|
57
|
+
* does **not** truncate context — callers that care about line size should set their own
|
|
58
|
+
* cap before calling, or trim post-hoc.
|
|
59
|
+
*/
|
|
60
|
+
export const serializeToJsonl = (entry: LogEntry, opts: SerializeToJsonlOptions = {}): string | undefined => {
|
|
61
|
+
if (entry.level <= LogLevel.TRACE) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { filename, line, context: scopeName } = entry.computedMeta;
|
|
66
|
+
|
|
67
|
+
const record: LogRecord = {
|
|
68
|
+
t: new Date(entry.timestamp).toISOString(),
|
|
69
|
+
l: shortLevelName[entry.level] ?? '?',
|
|
70
|
+
m: entry.message ?? '',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (filename !== undefined) {
|
|
74
|
+
record.f = filename;
|
|
75
|
+
}
|
|
76
|
+
if (line !== undefined) {
|
|
77
|
+
record.n = line;
|
|
78
|
+
}
|
|
79
|
+
if (scopeName !== undefined) {
|
|
80
|
+
record.o = scopeName;
|
|
81
|
+
}
|
|
82
|
+
if (entry.computedError !== undefined) {
|
|
83
|
+
record.e = entry.computedError;
|
|
84
|
+
}
|
|
85
|
+
if (opts.env !== undefined) {
|
|
86
|
+
record.i = opts.env;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const computedContext = entry.computedContext;
|
|
90
|
+
if (Object.keys(computedContext).length > 0) {
|
|
91
|
+
try {
|
|
92
|
+
record.c = JSON.stringify(computedContext);
|
|
93
|
+
} catch {
|
|
94
|
+
// Skip context that throws during serialization. `computedContext` is already flattened
|
|
95
|
+
// via `stringifyOneLevel`, so this is best-effort belt-and-suspenders.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
return JSON.stringify(record);
|
|
101
|
+
} catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
};
|
package/src/log-buffer.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, test } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import { type LogConfig, type
|
|
7
|
+
import { type LogConfig, LogEntry, type LogEntryInit, LogLevel } from './index';
|
|
8
8
|
import { LogBuffer } from './log-buffer';
|
|
9
9
|
|
|
10
10
|
const baseConfig: LogConfig = {
|
|
@@ -13,11 +13,12 @@ const baseConfig: LogConfig = {
|
|
|
13
13
|
processors: [],
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const createEntry = (overrides: Partial<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
const createEntry = (overrides: Partial<LogEntryInit> = {}): LogEntry =>
|
|
17
|
+
new LogEntry({
|
|
18
|
+
level: LogLevel.INFO,
|
|
19
|
+
message: 'test message',
|
|
20
|
+
...overrides,
|
|
21
|
+
});
|
|
21
22
|
|
|
22
23
|
describe('LogBuffer', () => {
|
|
23
24
|
test('pushes and serializes log entries', ({ expect }) => {
|
|
@@ -123,7 +124,9 @@ describe('LogBuffer', () => {
|
|
|
123
124
|
|
|
124
125
|
const lines = buffer.serialize().split('\n');
|
|
125
126
|
const record = JSON.parse(lines[0]);
|
|
126
|
-
|
|
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]');
|
|
127
130
|
});
|
|
128
131
|
|
|
129
132
|
test('serialize returns empty string for empty buffer', ({ expect }) => {
|
package/src/log-buffer.ts
CHANGED
|
@@ -2,36 +2,15 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { CircularBuffer
|
|
5
|
+
import { CircularBuffer } from '@dxos/util';
|
|
6
6
|
|
|
7
7
|
import { type LogConfig, LogLevel, shortLevelName } from './config';
|
|
8
8
|
import { type LogEntry, type LogProcessor } from './context';
|
|
9
|
+
import { type LogRecord } from './jsonl';
|
|
9
10
|
|
|
10
11
|
const DEFAULT_BUFFER_SIZE = 2_000;
|
|
11
12
|
const MAX_CONTEXT_LENGTH = 500;
|
|
12
13
|
|
|
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
14
|
/**
|
|
36
15
|
* Captures recent log entries in a circular buffer for debug log dump.
|
|
37
16
|
*/
|
|
@@ -51,39 +30,40 @@ export class LogBuffer {
|
|
|
51
30
|
return;
|
|
52
31
|
}
|
|
53
32
|
|
|
33
|
+
const { filename, line, context: scopeName } = entry.computedMeta;
|
|
34
|
+
|
|
54
35
|
const record: LogRecord = {
|
|
55
|
-
t: new Date().toISOString(),
|
|
36
|
+
t: new Date(entry.timestamp).toISOString(),
|
|
56
37
|
l: shortLevelName[entry.level] ?? '?',
|
|
57
38
|
m: entry.message ?? '',
|
|
58
39
|
};
|
|
59
40
|
|
|
60
|
-
if (
|
|
61
|
-
record.f =
|
|
62
|
-
|
|
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;
|
|
63
49
|
}
|
|
64
50
|
|
|
65
|
-
if (entry.
|
|
66
|
-
record.e = entry.
|
|
51
|
+
if (entry.computedError !== undefined) {
|
|
52
|
+
record.e = entry.computedError;
|
|
67
53
|
}
|
|
68
54
|
|
|
69
|
-
|
|
55
|
+
const computedContext = entry.computedContext;
|
|
56
|
+
if (Object.keys(computedContext).length > 0) {
|
|
70
57
|
try {
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
if (json.length > MAX_CONTEXT_LENGTH) {
|
|
75
|
-
json = json.slice(0, MAX_CONTEXT_LENGTH);
|
|
76
|
-
}
|
|
77
|
-
record.c = json;
|
|
58
|
+
let json = JSON.stringify(computedContext);
|
|
59
|
+
if (json.length > MAX_CONTEXT_LENGTH) {
|
|
60
|
+
json = json.slice(0, MAX_CONTEXT_LENGTH);
|
|
78
61
|
}
|
|
62
|
+
record.c = json;
|
|
79
63
|
} catch {
|
|
80
64
|
// Skip context that throws or is non-serializable.
|
|
81
65
|
}
|
|
82
66
|
}
|
|
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
67
|
|
|
88
68
|
this._buffer.push(record);
|
|
89
69
|
};
|
|
@@ -107,11 +87,3 @@ export class LogBuffer {
|
|
|
107
87
|
return lines.join('\n');
|
|
108
88
|
}
|
|
109
89
|
}
|
|
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.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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
9
|
import { createConfig } from './options';
|
|
@@ -29,15 +29,57 @@ export interface LogMethods {
|
|
|
29
29
|
config: (options?: LogOptions) => Log;
|
|
30
30
|
addProcessor: (processor: LogProcessor, addDefault?: boolean) => () => void;
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Log at `trace` level.
|
|
34
|
+
*
|
|
35
|
+
* Generally not surfaced to the developer and not captured in a log file.
|
|
36
|
+
*/
|
|
32
37
|
trace: LogFunction;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Log at `debug` level.
|
|
41
|
+
* Generally not surfaced to the developer and captured in a log file.
|
|
42
|
+
*/
|
|
33
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
|
+
*/
|
|
34
49
|
verbose: LogFunction;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Log at `info` level.
|
|
53
|
+
* Generally surfaced to the developer and captured in a log file.
|
|
54
|
+
*/
|
|
35
55
|
info: LogFunction;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Log at `warn` level.
|
|
59
|
+
* Generally surfaced to the developer and captured in a log file.
|
|
60
|
+
*/
|
|
36
61
|
warn: LogFunction;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Log at `error` level.
|
|
65
|
+
* Generally surfaced to the developer and captured in a log file.
|
|
66
|
+
*/
|
|
37
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
|
+
*/
|
|
38
73
|
catch: (error: Error | any, context?: LogContext, meta?: CallMetadata) => void;
|
|
39
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Decorator to log method parameters and return value at the `info` level.
|
|
77
|
+
*/
|
|
40
78
|
method: (arg0?: never, arg1?: never, meta?: CallMetadata) => MethodDecorator;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Wrapper to log function parameters and return value at the `info` level.
|
|
82
|
+
*/
|
|
41
83
|
function: <F extends (...args: any[]) => any>(
|
|
42
84
|
name: string,
|
|
43
85
|
fn: F,
|
|
@@ -46,7 +88,14 @@ export interface LogMethods {
|
|
|
46
88
|
},
|
|
47
89
|
) => F;
|
|
48
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Log a horizontal rule at the `info` level.
|
|
93
|
+
*/
|
|
49
94
|
break: () => void;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Log a stack trace at the `info` level.
|
|
98
|
+
*/
|
|
50
99
|
stack: (message?: string, context?: never, meta?: CallMetadata) => void;
|
|
51
100
|
}
|
|
52
101
|
|
|
@@ -98,15 +147,8 @@ export const createLog = (): LogImp => {
|
|
|
98
147
|
error?: Error,
|
|
99
148
|
) => {
|
|
100
149
|
// TODO(burdon): Do the filter matching upstream (here) rather than in each processor?
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
level,
|
|
104
|
-
message,
|
|
105
|
-
context,
|
|
106
|
-
meta,
|
|
107
|
-
error,
|
|
108
|
-
}),
|
|
109
|
-
);
|
|
150
|
+
const entry = new LogEntry({ level, message, context, meta, error });
|
|
151
|
+
log._config.processors.forEach((processor) => processor(log._config, entry));
|
|
110
152
|
};
|
|
111
153
|
|
|
112
154
|
/**
|
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
|
+
};
|
|
@@ -2,23 +2,11 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { safariCheck } from '@dxos/util';
|
|
6
6
|
|
|
7
7
|
import { LogLevel } from '../config';
|
|
8
8
|
import { type LogProcessor, getContextFromEntry, shouldLog } from '../context';
|
|
9
9
|
|
|
10
|
-
const getRelativeFilename = (filename: string) => {
|
|
11
|
-
// TODO(burdon): Hack uses "packages" as an anchor (pre-parse NX?)
|
|
12
|
-
// Including `packages/` part of the path so that excluded paths (e.g. from dist) are clickable in vscode.
|
|
13
|
-
const match = filename.match(/.+\/(packages\/.+\/.+)/);
|
|
14
|
-
if (match) {
|
|
15
|
-
const [, filePath] = match;
|
|
16
|
-
return filePath;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return filename;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
10
|
type Config = {
|
|
23
11
|
useTestProcessor: boolean;
|
|
24
12
|
printFileLinks: boolean;
|
|
@@ -47,23 +35,26 @@ const APP_BROWSER_PROCESSOR: LogProcessor = (config, entry) => {
|
|
|
47
35
|
// const LOG_BROWSER_CSS = ['color:gray; font-size:10px; padding-bottom: 4px', 'color:#B97852; font-size:14px;'];
|
|
48
36
|
const LOG_BROWSER_CSS: string[] = [];
|
|
49
37
|
|
|
38
|
+
const { filename, line: lineNumber, context: scopeDebugName } = entry.computedMeta;
|
|
39
|
+
|
|
50
40
|
let link = '';
|
|
51
|
-
if (
|
|
52
|
-
const filename = getRelativeFilename(entry.meta.F);
|
|
41
|
+
if (filename !== undefined && lineNumber !== undefined) {
|
|
53
42
|
const filepath = `${LOG_BROWSER_PREFIX.replace(/\/$/, '')}/${filename}`;
|
|
54
43
|
// TODO(burdon): Line numbers not working for app link, even with colons.
|
|
55
44
|
// https://stackoverflow.com/a/54459820/2804332
|
|
56
|
-
link = `${filepath}#L${
|
|
45
|
+
link = `${filepath}#L${lineNumber}`;
|
|
57
46
|
}
|
|
58
47
|
|
|
59
48
|
let args = [];
|
|
60
49
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const scopeName = scope.name ||
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
const scope = entry.meta?.S;
|
|
51
|
+
if (scope) {
|
|
52
|
+
const scopeName = scope.name || scopeDebugName;
|
|
53
|
+
if (scopeName) {
|
|
54
|
+
const processPrefix = scope.hostSessionId ? '[worker] ' : '';
|
|
55
|
+
// TODO(dmaretskyi): Those can be made clickable with a custom formatter.
|
|
56
|
+
args.push(`%c${processPrefix}${scopeName}`, 'color:#C026D3;font-weight:bold');
|
|
57
|
+
}
|
|
67
58
|
}
|
|
68
59
|
|
|
69
60
|
if (entry.message) {
|
|
@@ -73,9 +64,9 @@ const APP_BROWSER_PROCESSOR: LogProcessor = (config, entry) => {
|
|
|
73
64
|
const context = getContextFromEntry(entry);
|
|
74
65
|
if (context) {
|
|
75
66
|
if (Object.keys(context).length === 1 && 'error' in context) {
|
|
76
|
-
args.push(context.error);
|
|
67
|
+
args.push(unwrapEffectError(context.error));
|
|
77
68
|
} else if (Object.keys(context).length === 1 && 'err' in context) {
|
|
78
|
-
args.push(context.err);
|
|
69
|
+
args.push(unwrapEffectError(context.err));
|
|
79
70
|
} else {
|
|
80
71
|
args.push(context);
|
|
81
72
|
}
|
|
@@ -114,10 +105,8 @@ const TEST_BROWSER_PROCESSOR: LogProcessor = (config, entry) => {
|
|
|
114
105
|
return;
|
|
115
106
|
}
|
|
116
107
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
path = `${getRelativeFilename(entry.meta.F)}:${entry.meta.L}`;
|
|
120
|
-
}
|
|
108
|
+
const { filename, line: lineNumber } = entry.computedMeta;
|
|
109
|
+
const path = filename !== undefined && lineNumber !== undefined ? `${filename}:${lineNumber}` : '';
|
|
121
110
|
|
|
122
111
|
let args = [];
|
|
123
112
|
|
|
@@ -148,3 +137,13 @@ const TEST_BROWSER_PROCESSOR: LogProcessor = (config, entry) => {
|
|
|
148
137
|
};
|
|
149
138
|
|
|
150
139
|
export const BROWSER_PROCESSOR: LogProcessor = CONFIG.useTestProcessor ? TEST_BROWSER_PROCESSOR : APP_BROWSER_PROCESSOR;
|
|
140
|
+
|
|
141
|
+
// effect-specific
|
|
142
|
+
const originalSymbol = Symbol.for('effect/OriginalAnnotation');
|
|
143
|
+
|
|
144
|
+
const unwrapEffectError = (error: any) => {
|
|
145
|
+
if (typeof error === 'object' && error !== null && originalSymbol in error) {
|
|
146
|
+
return error[originalSymbol];
|
|
147
|
+
}
|
|
148
|
+
return error;
|
|
149
|
+
};
|
|
@@ -9,7 +9,6 @@ import { getPrototypeSpecificInstanceId, pickBy } from '@dxos/util';
|
|
|
9
9
|
|
|
10
10
|
import { type LogConfig, LogLevel, shortLevelName } from '../config';
|
|
11
11
|
import { type LogProcessor, getContextFromEntry, shouldLog } from '../context';
|
|
12
|
-
import { getRelativeFilename } from './common';
|
|
13
12
|
|
|
14
13
|
const LEVEL_COLORS: Record<LogLevel, typeof chalk.ForegroundColor> = {
|
|
15
14
|
[LogLevel.TRACE]: 'gray',
|
|
@@ -93,22 +92,17 @@ export const CONSOLE_PROCESSOR: LogProcessor = (config, entry) => {
|
|
|
93
92
|
return;
|
|
94
93
|
}
|
|
95
94
|
|
|
95
|
+
const { filename, line: lineNumber } = entry.computedMeta;
|
|
96
96
|
const parts: FormatParts = {
|
|
97
97
|
level,
|
|
98
98
|
message,
|
|
99
99
|
error,
|
|
100
|
-
path:
|
|
101
|
-
line:
|
|
102
|
-
scope:
|
|
100
|
+
path: filename,
|
|
101
|
+
line: lineNumber,
|
|
102
|
+
scope: meta?.S,
|
|
103
103
|
context: undefined,
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
if (meta) {
|
|
107
|
-
parts.path = getRelativeFilename(meta.F);
|
|
108
|
-
parts.line = meta.L;
|
|
109
|
-
parts.scope = meta.S;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
106
|
const context = getContextFromEntry(entry);
|
|
113
107
|
if (context) {
|
|
114
108
|
// Remove undefined fields.
|
|
@@ -5,11 +5,8 @@
|
|
|
5
5
|
import { appendFileSync, mkdirSync, openSync } from 'node:fs';
|
|
6
6
|
import { dirname } from 'node:path';
|
|
7
7
|
|
|
8
|
-
import { jsonlogify } from '@dxos/util';
|
|
9
|
-
|
|
10
8
|
import { type LogFilter, LogLevel } from '../config';
|
|
11
|
-
import { type LogProcessor,
|
|
12
|
-
import { getRelativeFilename } from './common';
|
|
9
|
+
import { type LogProcessor, shouldLog } from '../context';
|
|
13
10
|
|
|
14
11
|
// Amount of time to retry writing after encountering EAGAIN before giving up.
|
|
15
12
|
const EAGAIN_MAX_DURATION = 1000;
|
|
@@ -49,10 +46,12 @@ export const createFileProcessor = ({
|
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
const record = {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
level: entry.level,
|
|
50
|
+
message: entry.message,
|
|
51
|
+
timestamp: entry.timestamp,
|
|
52
|
+
meta: entry.computedMeta,
|
|
53
|
+
context: entry.computedContext,
|
|
54
|
+
error: entry.computedError,
|
|
56
55
|
};
|
|
57
56
|
let retryTS: number = 0;
|
|
58
57
|
|