@zintrust/trace 0.4.75
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 +288 -0
- package/dist/build-manifest.json +365 -0
- package/dist/cli-register.d.ts +9 -0
- package/dist/cli-register.js +32 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +38 -0
- package/dist/context.d.ts +18 -0
- package/dist/context.js +86 -0
- package/dist/dashboard/handlers.d.ts +15 -0
- package/dist/dashboard/handlers.js +179 -0
- package/dist/dashboard/routes.d.ts +19 -0
- package/dist/dashboard/routes.js +50 -0
- package/dist/dashboard/ui.d.ts +2 -0
- package/dist/dashboard/ui.js +870 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +50 -0
- package/dist/migrations/20260331000001_create_zin_debugger_entries_table.d.ts +10 -0
- package/dist/migrations/20260331000001_create_zin_debugger_entries_table.js +28 -0
- package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.d.ts +10 -0
- package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.js +21 -0
- package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.d.ts +10 -0
- package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.js +17 -0
- package/dist/migrations/index.d.ts +6 -0
- package/dist/migrations/index.js +4 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +3 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +140 -0
- package/dist/storage/DebuggerStorage.d.ts +13 -0
- package/dist/storage/DebuggerStorage.js +195 -0
- package/dist/storage/TraceStorage.d.ts +13 -0
- package/dist/storage/TraceStorage.js +195 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.js +25 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +7 -0
- package/dist/utils/authTag.d.ts +5 -0
- package/dist/utils/authTag.js +18 -0
- package/dist/utils/familyHash.d.ts +1 -0
- package/dist/utils/familyHash.js +8 -0
- package/dist/utils/redact.d.ts +6 -0
- package/dist/utils/redact.js +49 -0
- package/dist/utils/requestFilter.d.ts +4 -0
- package/dist/utils/requestFilter.js +26 -0
- package/dist/utils/stackFrame.d.ts +6 -0
- package/dist/utils/stackFrame.js +38 -0
- package/dist/watchers/AuthWatcher.d.ts +6 -0
- package/dist/watchers/AuthWatcher.js +49 -0
- package/dist/watchers/BatchWatcher.d.ts +6 -0
- package/dist/watchers/BatchWatcher.js +46 -0
- package/dist/watchers/CacheWatcher.d.ts +6 -0
- package/dist/watchers/CacheWatcher.js +51 -0
- package/dist/watchers/CommandWatcher.d.ts +6 -0
- package/dist/watchers/CommandWatcher.js +49 -0
- package/dist/watchers/DumpWatcher.d.ts +7 -0
- package/dist/watchers/DumpWatcher.js +41 -0
- package/dist/watchers/EventWatcher.d.ts +6 -0
- package/dist/watchers/EventWatcher.js +42 -0
- package/dist/watchers/ExceptionWatcher.d.ts +4 -0
- package/dist/watchers/ExceptionWatcher.js +103 -0
- package/dist/watchers/GateWatcher.d.ts +6 -0
- package/dist/watchers/GateWatcher.js +45 -0
- package/dist/watchers/HttpClientWatcher.d.ts +6 -0
- package/dist/watchers/HttpClientWatcher.js +50 -0
- package/dist/watchers/HttpWatcher.d.ts +2 -0
- package/dist/watchers/HttpWatcher.js +71 -0
- package/dist/watchers/JobWatcher.d.ts +10 -0
- package/dist/watchers/JobWatcher.js +108 -0
- package/dist/watchers/LogWatcher.d.ts +2 -0
- package/dist/watchers/LogWatcher.js +50 -0
- package/dist/watchers/MailWatcher.d.ts +6 -0
- package/dist/watchers/MailWatcher.js +45 -0
- package/dist/watchers/MiddlewareWatcher.d.ts +6 -0
- package/dist/watchers/MiddlewareWatcher.js +41 -0
- package/dist/watchers/ModelWatcher.d.ts +6 -0
- package/dist/watchers/ModelWatcher.js +42 -0
- package/dist/watchers/NotificationWatcher.d.ts +6 -0
- package/dist/watchers/NotificationWatcher.js +42 -0
- package/dist/watchers/QueryWatcher.d.ts +2 -0
- package/dist/watchers/QueryWatcher.js +72 -0
- package/dist/watchers/RedisWatcher.d.ts +7 -0
- package/dist/watchers/RedisWatcher.js +38 -0
- package/dist/watchers/ScheduleWatcher.d.ts +6 -0
- package/dist/watchers/ScheduleWatcher.js +46 -0
- package/dist/watchers/ViewWatcher.d.ts +6 -0
- package/dist/watchers/ViewWatcher.js +36 -0
- package/package.json +59 -0
- package/src/cli-register.ts +63 -0
- package/src/config.ts +46 -0
- package/src/context.ts +101 -0
- package/src/dashboard/handlers.ts +197 -0
- package/src/dashboard/routes.ts +101 -0
- package/src/dashboard/ui.ts +879 -0
- package/src/dashboard/zintrust-debuger.svg +30 -0
- package/src/index.ts +88 -0
- package/src/plugin.ts +9 -0
- package/src/register.ts +219 -0
- package/src/storage/TraceStorage.ts +306 -0
- package/src/storage/index.ts +2 -0
- package/src/types.ts +317 -0
- package/src/ui.ts +9 -0
- package/src/utils/authTag.ts +20 -0
- package/src/utils/familyHash.ts +8 -0
- package/src/utils/redact.ts +64 -0
- package/src/utils/requestFilter.ts +33 -0
- package/src/utils/stackFrame.ts +44 -0
- package/src/watchers/AuthWatcher.ts +50 -0
- package/src/watchers/BatchWatcher.ts +52 -0
- package/src/watchers/CacheWatcher.ts +58 -0
- package/src/watchers/CommandWatcher.ts +55 -0
- package/src/watchers/DumpWatcher.ts +42 -0
- package/src/watchers/EventWatcher.ts +43 -0
- package/src/watchers/ExceptionWatcher.ts +114 -0
- package/src/watchers/GateWatcher.ts +50 -0
- package/src/watchers/HttpClientWatcher.ts +56 -0
- package/src/watchers/HttpWatcher.ts +94 -0
- package/src/watchers/JobWatcher.ts +121 -0
- package/src/watchers/LogWatcher.ts +61 -0
- package/src/watchers/MailWatcher.ts +47 -0
- package/src/watchers/MiddlewareWatcher.ts +42 -0
- package/src/watchers/ModelWatcher.ts +48 -0
- package/src/watchers/NotificationWatcher.ts +43 -0
- package/src/watchers/QueryWatcher.ts +85 -0
- package/src/watchers/RedisWatcher.ts +39 -0
- package/src/watchers/ScheduleWatcher.ts +54 -0
- package/src/watchers/ViewWatcher.ts +37 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { TraceContext } from '../context';
|
|
2
|
+
import type { CommandContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
|
|
3
|
+
import { EntryType } from '../types';
|
|
4
|
+
import { redactObject } from '../utils/redact';
|
|
5
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
6
|
+
|
|
7
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
8
|
+
let _redactKeys: string[] = [];
|
|
9
|
+
let _ignoreRoutes: string[] = [];
|
|
10
|
+
|
|
11
|
+
const emit = (
|
|
12
|
+
name: string,
|
|
13
|
+
args: Record<string, unknown>,
|
|
14
|
+
exitCode: number,
|
|
15
|
+
duration: number,
|
|
16
|
+
output?: string
|
|
17
|
+
): void => {
|
|
18
|
+
if (!_storage) return;
|
|
19
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
20
|
+
const tags = [name];
|
|
21
|
+
if (exitCode !== 0) tags.push('failed');
|
|
22
|
+
const content: CommandContent = {
|
|
23
|
+
name,
|
|
24
|
+
arguments: redactObject(args, _redactKeys),
|
|
25
|
+
exitCode,
|
|
26
|
+
duration,
|
|
27
|
+
output,
|
|
28
|
+
hostname: TraceContext.getHostname(),
|
|
29
|
+
};
|
|
30
|
+
_storage
|
|
31
|
+
.writeEntry({
|
|
32
|
+
uuid: crypto.randomUUID(),
|
|
33
|
+
batchId: TraceContext.getBatchId(),
|
|
34
|
+
type: EntryType.COMMAND,
|
|
35
|
+
content,
|
|
36
|
+
tags,
|
|
37
|
+
isLatest: true,
|
|
38
|
+
createdAt: TraceContext.now(),
|
|
39
|
+
})
|
|
40
|
+
.catch(() => undefined);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const CommandWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
|
|
44
|
+
emit,
|
|
45
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
46
|
+
if (config.watchers.command === false) return () => undefined;
|
|
47
|
+
_storage = storage;
|
|
48
|
+
_redactKeys = config.redaction?.body ?? [];
|
|
49
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
50
|
+
return () => {
|
|
51
|
+
_storage = null;
|
|
52
|
+
_ignoreRoutes = [];
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TraceContext } from '../context';
|
|
2
|
+
import type { DumpContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
|
|
3
|
+
import { EntryType } from '../types';
|
|
4
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
5
|
+
|
|
6
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
7
|
+
let _enabled = false;
|
|
8
|
+
let _ignoreRoutes: string[] = [];
|
|
9
|
+
|
|
10
|
+
/** Explicitly opt-in (enabled only when config.watchers.dump === true, not just non-false). */
|
|
11
|
+
const emit = (value: unknown, file?: string, line?: number): void => {
|
|
12
|
+
if (!_storage || !_enabled) return;
|
|
13
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
14
|
+
const content: DumpContent = { value, file, line, hostname: TraceContext.getHostname() };
|
|
15
|
+
_storage
|
|
16
|
+
.writeEntry({
|
|
17
|
+
uuid: crypto.randomUUID(),
|
|
18
|
+
batchId: TraceContext.getBatchId(),
|
|
19
|
+
type: EntryType.DUMP,
|
|
20
|
+
content,
|
|
21
|
+
tags: [],
|
|
22
|
+
isLatest: true,
|
|
23
|
+
createdAt: TraceContext.now(),
|
|
24
|
+
})
|
|
25
|
+
.catch(() => undefined);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const DumpWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
|
|
29
|
+
emit,
|
|
30
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
31
|
+
// DumpWatcher requires explicit opt-in (=== true), not just absence of false
|
|
32
|
+
if (config.watchers.dump !== true) return () => undefined;
|
|
33
|
+
_storage = storage;
|
|
34
|
+
_enabled = true;
|
|
35
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
36
|
+
return () => {
|
|
37
|
+
_storage = null;
|
|
38
|
+
_enabled = false;
|
|
39
|
+
_ignoreRoutes = [];
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { TraceContext } from '../context';
|
|
2
|
+
import type { EventContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
|
|
3
|
+
import { EntryType } from '../types';
|
|
4
|
+
import { AuthTag } from '../utils/authTag';
|
|
5
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
6
|
+
|
|
7
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
8
|
+
let _ignoreRoutes: string[] = [];
|
|
9
|
+
|
|
10
|
+
const emit = (name: string, listenerCount: number, payload?: unknown): void => {
|
|
11
|
+
if (!_storage) return;
|
|
12
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
13
|
+
const content: EventContent = {
|
|
14
|
+
name,
|
|
15
|
+
payload,
|
|
16
|
+
listenerCount,
|
|
17
|
+
hostname: TraceContext.getHostname(),
|
|
18
|
+
};
|
|
19
|
+
_storage
|
|
20
|
+
.writeEntry({
|
|
21
|
+
uuid: crypto.randomUUID(),
|
|
22
|
+
batchId: TraceContext.getBatchId(),
|
|
23
|
+
type: EntryType.EVENT,
|
|
24
|
+
content,
|
|
25
|
+
tags: AuthTag.append([name]),
|
|
26
|
+
isLatest: true,
|
|
27
|
+
createdAt: TraceContext.now(),
|
|
28
|
+
})
|
|
29
|
+
.catch(() => undefined);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const EventWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
|
|
33
|
+
emit,
|
|
34
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
35
|
+
if (config.watchers.event === false) return () => undefined;
|
|
36
|
+
_storage = storage;
|
|
37
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
38
|
+
return () => {
|
|
39
|
+
_storage = null;
|
|
40
|
+
_ignoreRoutes = [];
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExceptionWatcher — captures unhandled exceptions by hooking into the
|
|
3
|
+
* framework error middleware. Core must call ExceptionWatcher.capture()
|
|
4
|
+
* from within its error handler, or the register() side-effect adds a
|
|
5
|
+
* process-level unhandledRejection/uncaughtException listener as fallback.
|
|
6
|
+
*/
|
|
7
|
+
import { TraceContext } from '../context';
|
|
8
|
+
import type { ExceptionContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
|
|
9
|
+
import { EntryType } from '../types';
|
|
10
|
+
import { AuthTag } from '../utils/authTag';
|
|
11
|
+
import { familyHash } from '../utils/familyHash';
|
|
12
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
13
|
+
import { parseStackFrameLine } from '../utils/stackFrame';
|
|
14
|
+
|
|
15
|
+
const getLinePreview = (_file: string, _line: number): Record<string, string> => {
|
|
16
|
+
return {};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const buildContent = (err: Error): ExceptionContent => {
|
|
20
|
+
const stack = err.stack ?? '';
|
|
21
|
+
const trace: ExceptionContent['trace'] = stack
|
|
22
|
+
.split('\n')
|
|
23
|
+
.slice(1)
|
|
24
|
+
.map(parseStackFrameLine)
|
|
25
|
+
.filter((x): x is { file: string; line: number } => x !== null)
|
|
26
|
+
.slice(0, 20);
|
|
27
|
+
|
|
28
|
+
const firstFrame = trace[0];
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
class: err.constructor?.name ?? 'Error',
|
|
32
|
+
file: firstFrame?.file ?? 'unknown',
|
|
33
|
+
line: firstFrame?.line ?? 0,
|
|
34
|
+
message: err.message,
|
|
35
|
+
trace,
|
|
36
|
+
linePreview: firstFrame ? getLinePreview(firstFrame.file, firstFrame.line) : {},
|
|
37
|
+
occurrences: 1,
|
|
38
|
+
hostname: TraceContext.getHostname(),
|
|
39
|
+
userId: TraceContext.getUserId(),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
44
|
+
let _listenerRefCount = 0;
|
|
45
|
+
let _ignoreRoutes: string[] = [];
|
|
46
|
+
|
|
47
|
+
const handleUncaughtException = (error: unknown): void => {
|
|
48
|
+
captureException(error);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleUnhandledRejection = (reason: unknown): void => {
|
|
52
|
+
captureException(reason);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const registerProcessListeners = (): void => {
|
|
56
|
+
if (typeof process === 'undefined') return;
|
|
57
|
+
process.on('uncaughtException', handleUncaughtException);
|
|
58
|
+
process.on('unhandledRejection', handleUnhandledRejection);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const unregisterProcessListeners = (): void => {
|
|
62
|
+
if (typeof process === 'undefined') return;
|
|
63
|
+
process.off('uncaughtException', handleUncaughtException);
|
|
64
|
+
process.off('unhandledRejection', handleUnhandledRejection);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const captureException = (err: unknown): void => {
|
|
68
|
+
const storage = _storage;
|
|
69
|
+
if (!storage) return;
|
|
70
|
+
if (!(err instanceof Error)) return;
|
|
71
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
72
|
+
|
|
73
|
+
const content = buildContent(err);
|
|
74
|
+
const hash = familyHash(`${content.class}:${content.file}:${content.line}`);
|
|
75
|
+
const uuid = crypto.randomUUID();
|
|
76
|
+
|
|
77
|
+
storage
|
|
78
|
+
.writeEntry({
|
|
79
|
+
uuid,
|
|
80
|
+
batchId: TraceContext.getBatchId(),
|
|
81
|
+
familyHash: hash,
|
|
82
|
+
type: EntryType.EXCEPTION,
|
|
83
|
+
content,
|
|
84
|
+
tags: AuthTag.append([content.class]),
|
|
85
|
+
isLatest: true,
|
|
86
|
+
createdAt: TraceContext.now(),
|
|
87
|
+
})
|
|
88
|
+
.then(() => storage.markFamilyStale(hash, uuid))
|
|
89
|
+
.catch(() => undefined);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const ExceptionWatcher: ITraceWatcher & { capture: (err: unknown) => void } = Object.freeze({
|
|
93
|
+
capture: captureException,
|
|
94
|
+
|
|
95
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
96
|
+
if (config.watchers.exception === false) return () => undefined;
|
|
97
|
+
_storage = storage;
|
|
98
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
99
|
+
|
|
100
|
+
if (_listenerRefCount === 0) {
|
|
101
|
+
registerProcessListeners();
|
|
102
|
+
}
|
|
103
|
+
_listenerRefCount += 1;
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
_listenerRefCount = Math.max(0, _listenerRefCount - 1);
|
|
107
|
+
if (_listenerRefCount === 0) {
|
|
108
|
+
unregisterProcessListeners();
|
|
109
|
+
}
|
|
110
|
+
_storage = null;
|
|
111
|
+
_ignoreRoutes = [];
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { TraceContext } from '../context';
|
|
2
|
+
import type { GateContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
|
|
3
|
+
import { EntryType } from '../types';
|
|
4
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
5
|
+
|
|
6
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
7
|
+
let _ignoreRoutes: string[] = [];
|
|
8
|
+
|
|
9
|
+
const emit = (
|
|
10
|
+
ability: string,
|
|
11
|
+
result: GateContent['result'],
|
|
12
|
+
userId?: string,
|
|
13
|
+
subject?: string
|
|
14
|
+
): void => {
|
|
15
|
+
if (!_storage) return;
|
|
16
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
17
|
+
const tags: string[] = [ability, result];
|
|
18
|
+
if (userId) tags.push(`Auth:${userId}`);
|
|
19
|
+
const content: GateContent = {
|
|
20
|
+
ability,
|
|
21
|
+
result,
|
|
22
|
+
userId,
|
|
23
|
+
subject,
|
|
24
|
+
hostname: TraceContext.getHostname(),
|
|
25
|
+
};
|
|
26
|
+
_storage
|
|
27
|
+
.writeEntry({
|
|
28
|
+
uuid: crypto.randomUUID(),
|
|
29
|
+
batchId: TraceContext.getBatchId(),
|
|
30
|
+
type: EntryType.GATE,
|
|
31
|
+
content,
|
|
32
|
+
tags,
|
|
33
|
+
isLatest: true,
|
|
34
|
+
createdAt: TraceContext.now(),
|
|
35
|
+
})
|
|
36
|
+
.catch(() => undefined);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const GateWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
|
|
40
|
+
emit,
|
|
41
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
42
|
+
if (config.watchers.gate === false) return () => undefined;
|
|
43
|
+
_storage = storage;
|
|
44
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
45
|
+
return () => {
|
|
46
|
+
_storage = null;
|
|
47
|
+
_ignoreRoutes = [];
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { TraceContext } from '../context';
|
|
2
|
+
import type { ClientRequestContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
|
|
3
|
+
import { EntryType } from '../types';
|
|
4
|
+
import { AuthTag } from '../utils/authTag';
|
|
5
|
+
import { redactHeaders } from '../utils/redact';
|
|
6
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
7
|
+
|
|
8
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
9
|
+
let _redactHeaderNames: string[] = [];
|
|
10
|
+
let _ignoreRoutes: string[] = [];
|
|
11
|
+
|
|
12
|
+
const emit = (
|
|
13
|
+
method: string,
|
|
14
|
+
url: string,
|
|
15
|
+
requestHeaders: Record<string, string>,
|
|
16
|
+
responseStatus: number,
|
|
17
|
+
duration: number
|
|
18
|
+
): void => {
|
|
19
|
+
if (!_storage) return;
|
|
20
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
21
|
+
const tags = AuthTag.append([method.toUpperCase()]);
|
|
22
|
+
if (responseStatus >= 400) tags.push('failed');
|
|
23
|
+
const content: ClientRequestContent = {
|
|
24
|
+
method: method.toUpperCase(),
|
|
25
|
+
url,
|
|
26
|
+
requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames),
|
|
27
|
+
responseStatus,
|
|
28
|
+
duration,
|
|
29
|
+
hostname: TraceContext.getHostname(),
|
|
30
|
+
};
|
|
31
|
+
_storage
|
|
32
|
+
.writeEntry({
|
|
33
|
+
uuid: crypto.randomUUID(),
|
|
34
|
+
batchId: TraceContext.getBatchId(),
|
|
35
|
+
type: EntryType.CLIENT_REQUEST,
|
|
36
|
+
content,
|
|
37
|
+
tags,
|
|
38
|
+
isLatest: true,
|
|
39
|
+
createdAt: TraceContext.now(),
|
|
40
|
+
})
|
|
41
|
+
.catch(() => undefined);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const HttpClientWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
|
|
45
|
+
emit,
|
|
46
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
47
|
+
if (config.watchers.clientRequest === false) return () => undefined;
|
|
48
|
+
_storage = storage;
|
|
49
|
+
_redactHeaderNames = config.redaction?.headers ?? [];
|
|
50
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
51
|
+
return () => {
|
|
52
|
+
_storage = null;
|
|
53
|
+
_ignoreRoutes = [];
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HttpWatcher — records inbound HTTP requests as trace entries.
|
|
3
|
+
* Registers as a global middleware via Kernel.registerGlobalMiddleware().
|
|
4
|
+
*/
|
|
5
|
+
import type { IRequest, IResponse } from '@zintrust/core';
|
|
6
|
+
import { TraceContext } from '../context';
|
|
7
|
+
import type { ITraceConfig, ITraceWatcher, ITraceWatcherConfig, RequestContent } from '../types';
|
|
8
|
+
import { EntryType } from '../types';
|
|
9
|
+
import { AuthTag } from '../utils/authTag';
|
|
10
|
+
import { redactHeaders, redactObject } from '../utils/redact';
|
|
11
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
12
|
+
|
|
13
|
+
const normalizeHeaders = (headers: IRequest['headers']): Record<string, string> => {
|
|
14
|
+
if (!headers) return {};
|
|
15
|
+
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
Object.entries(headers).flatMap(([key, value]) => {
|
|
18
|
+
if (typeof value === 'string') return [[key, value]];
|
|
19
|
+
if (Array.isArray(value)) return [[key, value.join(', ')]];
|
|
20
|
+
return [];
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const buildEntry = (
|
|
26
|
+
req: IRequest,
|
|
27
|
+
res: IResponse,
|
|
28
|
+
start: number,
|
|
29
|
+
config: ITraceConfig
|
|
30
|
+
): RequestContent => {
|
|
31
|
+
const headers = redactHeaders(normalizeHeaders(req.headers), config.redaction.headers);
|
|
32
|
+
|
|
33
|
+
const payload = req.body ? redactObject(req.body, config.redaction.body) : {};
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
method: req.getMethod(),
|
|
37
|
+
uri: req.getPath(),
|
|
38
|
+
headers,
|
|
39
|
+
payload,
|
|
40
|
+
responseStatus: res.getStatus(),
|
|
41
|
+
responseHeaders: {},
|
|
42
|
+
duration: Date.now() - start,
|
|
43
|
+
memory: TraceContext.getMemory(),
|
|
44
|
+
middleware: [],
|
|
45
|
+
hostname: TraceContext.getHostname(),
|
|
46
|
+
userId: TraceContext.getUserId(),
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const shouldIgnore = (req: IRequest, config: ITraceConfig): boolean => {
|
|
51
|
+
return RequestFilter.matchesIgnoredPath(req.getPath(), config.ignoreRoutes);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const isWatcherEnabled = (config: ITraceConfig): boolean => config.watchers.request !== false;
|
|
55
|
+
|
|
56
|
+
export const HttpWatcher: ITraceWatcher = Object.freeze({
|
|
57
|
+
register({ storage, config, registerMiddleware }: ITraceWatcherConfig): () => void {
|
|
58
|
+
if (!isWatcherEnabled(config)) return () => undefined;
|
|
59
|
+
if (!registerMiddleware) return () => undefined;
|
|
60
|
+
|
|
61
|
+
const middleware: Parameters<
|
|
62
|
+
NonNullable<ITraceWatcherConfig['registerMiddleware']>
|
|
63
|
+
>[0] = async (req: unknown, res: unknown, next: () => Promise<void>): Promise<void> => {
|
|
64
|
+
const request = req as IRequest;
|
|
65
|
+
const response = res as IResponse;
|
|
66
|
+
|
|
67
|
+
if (shouldIgnore(request, config)) return next();
|
|
68
|
+
|
|
69
|
+
const start = TraceContext.now();
|
|
70
|
+
const batchId = TraceContext.getBatchId();
|
|
71
|
+
|
|
72
|
+
await next();
|
|
73
|
+
|
|
74
|
+
const content = buildEntry(request, response, start, config);
|
|
75
|
+
const tags = AuthTag.append([]);
|
|
76
|
+
if (content.responseStatus >= 500) tags.push('failed');
|
|
77
|
+
|
|
78
|
+
storage
|
|
79
|
+
.writeEntry({
|
|
80
|
+
uuid: crypto.randomUUID(),
|
|
81
|
+
batchId,
|
|
82
|
+
type: EntryType.REQUEST,
|
|
83
|
+
content,
|
|
84
|
+
tags,
|
|
85
|
+
isLatest: true,
|
|
86
|
+
createdAt: TraceContext.now(),
|
|
87
|
+
})
|
|
88
|
+
.catch(() => undefined); // fire-and-forget
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
registerMiddleware(middleware);
|
|
92
|
+
return () => undefined;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JobWatcher — records job dispatch, completion, and failure.
|
|
3
|
+
* Subsystems must call JobWatcher.onDispatch / onProcess / onFail from
|
|
4
|
+
* within their queue implementation for full tracking.
|
|
5
|
+
*/
|
|
6
|
+
import { TraceContext } from '../context';
|
|
7
|
+
import type { ITraceWatcher, ITraceWatcherConfig, JobContent } from '../types';
|
|
8
|
+
import { EntryType } from '../types';
|
|
9
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
10
|
+
import { parseStackFrameLine } from '../utils/stackFrame';
|
|
11
|
+
|
|
12
|
+
// Module-level storage ref so emit helpers can be called from outside.
|
|
13
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
14
|
+
let _ignoreRoutes: string[] = [];
|
|
15
|
+
const MAX_TRACKED_JOBS = 1000;
|
|
16
|
+
|
|
17
|
+
type PendingJob = { uuid: string; content: JobContent };
|
|
18
|
+
|
|
19
|
+
const pendingJobs = new Map<string, PendingJob[]>();
|
|
20
|
+
|
|
21
|
+
const trackPendingJob = (name: string, job: PendingJob): void => {
|
|
22
|
+
const jobs = pendingJobs.get(name) ?? [];
|
|
23
|
+
jobs.push(job);
|
|
24
|
+
if (jobs.length > MAX_TRACKED_JOBS) {
|
|
25
|
+
jobs.shift();
|
|
26
|
+
}
|
|
27
|
+
pendingJobs.set(name, jobs);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const takePendingJob = (name: string): PendingJob | null => {
|
|
31
|
+
const jobs = pendingJobs.get(name);
|
|
32
|
+
if (!jobs || jobs.length === 0) return null;
|
|
33
|
+
|
|
34
|
+
const job = jobs.shift() ?? null;
|
|
35
|
+
if (jobs.length === 0) {
|
|
36
|
+
pendingJobs.delete(name);
|
|
37
|
+
} else {
|
|
38
|
+
pendingJobs.set(name, jobs);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return job;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const emitDispatch = (name: string, queue: string, connection: string, data?: unknown): void => {
|
|
45
|
+
if (!_storage) return;
|
|
46
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
47
|
+
const uuid = crypto.randomUUID();
|
|
48
|
+
const content: JobContent = {
|
|
49
|
+
status: 'pending',
|
|
50
|
+
connection,
|
|
51
|
+
queue,
|
|
52
|
+
name,
|
|
53
|
+
data,
|
|
54
|
+
hostname: TraceContext.getHostname(),
|
|
55
|
+
};
|
|
56
|
+
_storage
|
|
57
|
+
.writeEntry({
|
|
58
|
+
uuid,
|
|
59
|
+
batchId: TraceContext.getBatchId(),
|
|
60
|
+
type: EntryType.JOB,
|
|
61
|
+
content,
|
|
62
|
+
tags: [name],
|
|
63
|
+
isLatest: true,
|
|
64
|
+
createdAt: TraceContext.now(),
|
|
65
|
+
})
|
|
66
|
+
.catch(() => undefined);
|
|
67
|
+
|
|
68
|
+
trackPendingJob(name, { uuid, content });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const emitProcessed = (name: string): void => {
|
|
72
|
+
if (!_storage) return;
|
|
73
|
+
const pendingJob = takePendingJob(name);
|
|
74
|
+
if (pendingJob === null) return;
|
|
75
|
+
|
|
76
|
+
const patch: JobContent = { ...pendingJob.content, status: 'processed' };
|
|
77
|
+
void _storage.updateEntry(pendingJob.uuid, { content: patch }).catch(() => undefined);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const emitFailed = (name: string, error: Error): void => {
|
|
81
|
+
if (!_storage) return;
|
|
82
|
+
const pendingJob = takePendingJob(name);
|
|
83
|
+
if (pendingJob === null) return;
|
|
84
|
+
|
|
85
|
+
const patch: JobContent = {
|
|
86
|
+
...pendingJob.content,
|
|
87
|
+
status: 'failed',
|
|
88
|
+
exception: {
|
|
89
|
+
message: error.message,
|
|
90
|
+
trace: (error.stack ?? '')
|
|
91
|
+
.split('\n')
|
|
92
|
+
.slice(1)
|
|
93
|
+
.map(parseStackFrameLine)
|
|
94
|
+
.filter((trace): trace is { file: string; line: number } => trace !== null)
|
|
95
|
+
.slice(0, 10),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
void _storage.updateEntry(pendingJob.uuid, { content: patch }).catch(() => undefined);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const JobWatcher: ITraceWatcher & {
|
|
103
|
+
onDispatch: typeof emitDispatch;
|
|
104
|
+
onProcessed: typeof emitProcessed;
|
|
105
|
+
onFailed: typeof emitFailed;
|
|
106
|
+
} = Object.freeze({
|
|
107
|
+
onDispatch: emitDispatch,
|
|
108
|
+
onProcessed: emitProcessed,
|
|
109
|
+
onFailed: emitFailed,
|
|
110
|
+
|
|
111
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
112
|
+
if (config.watchers.job === false) return () => undefined;
|
|
113
|
+
_storage = storage;
|
|
114
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
115
|
+
return () => {
|
|
116
|
+
_storage = null;
|
|
117
|
+
_ignoreRoutes = [];
|
|
118
|
+
pendingJobs.clear();
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LogWatcher — captures Logger output via Logger.addSink().
|
|
3
|
+
*/
|
|
4
|
+
import { Logger } from '@zintrust/core';
|
|
5
|
+
import { TraceContext } from '../context';
|
|
6
|
+
import type { ITraceWatcher, ITraceWatcherConfig, LogContent } from '../types';
|
|
7
|
+
import { EntryType } from '../types';
|
|
8
|
+
import { AuthTag } from '../utils/authTag';
|
|
9
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
10
|
+
|
|
11
|
+
type LoggerSink = (level: string, message: string, context?: Record<string, unknown>) => void;
|
|
12
|
+
|
|
13
|
+
const LEVEL_PRIORITY: Record<string, number> = {
|
|
14
|
+
debug: 0,
|
|
15
|
+
info: 1,
|
|
16
|
+
warn: 2,
|
|
17
|
+
error: 3,
|
|
18
|
+
fatal: 4,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const LogWatcher: ITraceWatcher = Object.freeze({
|
|
22
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
23
|
+
if (config.watchers.log === false) return () => undefined;
|
|
24
|
+
|
|
25
|
+
const minPriority = LEVEL_PRIORITY[config.logMinLevel] ?? 1;
|
|
26
|
+
|
|
27
|
+
const loggerWithSink = Logger as typeof Logger & {
|
|
28
|
+
addSink?: (fn: LoggerSink) => () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (typeof loggerWithSink.addSink !== 'function') {
|
|
32
|
+
return () => undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const unsubscribe = loggerWithSink.addSink((level, message, context) => {
|
|
36
|
+
if ((LEVEL_PRIORITY[level] ?? 0) < minPriority) return;
|
|
37
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes)) return;
|
|
38
|
+
|
|
39
|
+
const content: LogContent = {
|
|
40
|
+
level,
|
|
41
|
+
message,
|
|
42
|
+
context: context ?? undefined,
|
|
43
|
+
hostname: TraceContext.getHostname(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
storage
|
|
47
|
+
.writeEntry({
|
|
48
|
+
uuid: crypto.randomUUID(),
|
|
49
|
+
batchId: TraceContext.getBatchId(),
|
|
50
|
+
type: EntryType.LOG,
|
|
51
|
+
content,
|
|
52
|
+
tags: AuthTag.append([]),
|
|
53
|
+
isLatest: true,
|
|
54
|
+
createdAt: TraceContext.now(),
|
|
55
|
+
})
|
|
56
|
+
.catch(() => undefined);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return unsubscribe;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MailWatcher — records mail dispatch intent.
|
|
3
|
+
* Body is never captured; only to/subject/template.
|
|
4
|
+
*/
|
|
5
|
+
import { TraceContext } from '../context';
|
|
6
|
+
import type { ITraceWatcher, ITraceWatcherConfig, MailContent } from '../types';
|
|
7
|
+
import { EntryType } from '../types';
|
|
8
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
9
|
+
|
|
10
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
11
|
+
let _ignoreRoutes: string[] = [];
|
|
12
|
+
|
|
13
|
+
const emit = (to: string, subject: string, template?: string): void => {
|
|
14
|
+
if (!_storage) return;
|
|
15
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
16
|
+
const content: MailContent = {
|
|
17
|
+
to,
|
|
18
|
+
subject,
|
|
19
|
+
template,
|
|
20
|
+
hostname: TraceContext.getHostname(),
|
|
21
|
+
};
|
|
22
|
+
_storage
|
|
23
|
+
.writeEntry({
|
|
24
|
+
uuid: crypto.randomUUID(),
|
|
25
|
+
batchId: TraceContext.getBatchId(),
|
|
26
|
+
type: EntryType.MAIL,
|
|
27
|
+
content,
|
|
28
|
+
tags: [],
|
|
29
|
+
isLatest: true,
|
|
30
|
+
createdAt: TraceContext.now(),
|
|
31
|
+
})
|
|
32
|
+
.catch(() => undefined);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const MailWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
|
|
36
|
+
emit,
|
|
37
|
+
|
|
38
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
39
|
+
if (config.watchers.mail === false) return () => undefined;
|
|
40
|
+
_storage = storage;
|
|
41
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
42
|
+
return () => {
|
|
43
|
+
_storage = null;
|
|
44
|
+
_ignoreRoutes = [];
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|