@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.
Files changed (128) hide show
  1. package/README.md +288 -0
  2. package/dist/build-manifest.json +365 -0
  3. package/dist/cli-register.d.ts +9 -0
  4. package/dist/cli-register.js +32 -0
  5. package/dist/config.d.ts +9 -0
  6. package/dist/config.js +38 -0
  7. package/dist/context.d.ts +18 -0
  8. package/dist/context.js +86 -0
  9. package/dist/dashboard/handlers.d.ts +15 -0
  10. package/dist/dashboard/handlers.js +179 -0
  11. package/dist/dashboard/routes.d.ts +19 -0
  12. package/dist/dashboard/routes.js +50 -0
  13. package/dist/dashboard/ui.d.ts +2 -0
  14. package/dist/dashboard/ui.js +870 -0
  15. package/dist/index.d.ts +35 -0
  16. package/dist/index.js +50 -0
  17. package/dist/migrations/20260331000001_create_zin_debugger_entries_table.d.ts +10 -0
  18. package/dist/migrations/20260331000001_create_zin_debugger_entries_table.js +28 -0
  19. package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.d.ts +10 -0
  20. package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.js +21 -0
  21. package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.d.ts +10 -0
  22. package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.js +17 -0
  23. package/dist/migrations/index.d.ts +6 -0
  24. package/dist/migrations/index.js +4 -0
  25. package/dist/plugin.d.ts +1 -0
  26. package/dist/plugin.js +3 -0
  27. package/dist/register.d.ts +1 -0
  28. package/dist/register.js +140 -0
  29. package/dist/storage/DebuggerStorage.d.ts +13 -0
  30. package/dist/storage/DebuggerStorage.js +195 -0
  31. package/dist/storage/TraceStorage.d.ts +13 -0
  32. package/dist/storage/TraceStorage.js +195 -0
  33. package/dist/storage/index.d.ts +2 -0
  34. package/dist/storage/index.js +1 -0
  35. package/dist/types.d.ts +270 -0
  36. package/dist/types.js +25 -0
  37. package/dist/ui.d.ts +8 -0
  38. package/dist/ui.js +7 -0
  39. package/dist/utils/authTag.d.ts +5 -0
  40. package/dist/utils/authTag.js +18 -0
  41. package/dist/utils/familyHash.d.ts +1 -0
  42. package/dist/utils/familyHash.js +8 -0
  43. package/dist/utils/redact.d.ts +6 -0
  44. package/dist/utils/redact.js +49 -0
  45. package/dist/utils/requestFilter.d.ts +4 -0
  46. package/dist/utils/requestFilter.js +26 -0
  47. package/dist/utils/stackFrame.d.ts +6 -0
  48. package/dist/utils/stackFrame.js +38 -0
  49. package/dist/watchers/AuthWatcher.d.ts +6 -0
  50. package/dist/watchers/AuthWatcher.js +49 -0
  51. package/dist/watchers/BatchWatcher.d.ts +6 -0
  52. package/dist/watchers/BatchWatcher.js +46 -0
  53. package/dist/watchers/CacheWatcher.d.ts +6 -0
  54. package/dist/watchers/CacheWatcher.js +51 -0
  55. package/dist/watchers/CommandWatcher.d.ts +6 -0
  56. package/dist/watchers/CommandWatcher.js +49 -0
  57. package/dist/watchers/DumpWatcher.d.ts +7 -0
  58. package/dist/watchers/DumpWatcher.js +41 -0
  59. package/dist/watchers/EventWatcher.d.ts +6 -0
  60. package/dist/watchers/EventWatcher.js +42 -0
  61. package/dist/watchers/ExceptionWatcher.d.ts +4 -0
  62. package/dist/watchers/ExceptionWatcher.js +103 -0
  63. package/dist/watchers/GateWatcher.d.ts +6 -0
  64. package/dist/watchers/GateWatcher.js +45 -0
  65. package/dist/watchers/HttpClientWatcher.d.ts +6 -0
  66. package/dist/watchers/HttpClientWatcher.js +50 -0
  67. package/dist/watchers/HttpWatcher.d.ts +2 -0
  68. package/dist/watchers/HttpWatcher.js +71 -0
  69. package/dist/watchers/JobWatcher.d.ts +10 -0
  70. package/dist/watchers/JobWatcher.js +108 -0
  71. package/dist/watchers/LogWatcher.d.ts +2 -0
  72. package/dist/watchers/LogWatcher.js +50 -0
  73. package/dist/watchers/MailWatcher.d.ts +6 -0
  74. package/dist/watchers/MailWatcher.js +45 -0
  75. package/dist/watchers/MiddlewareWatcher.d.ts +6 -0
  76. package/dist/watchers/MiddlewareWatcher.js +41 -0
  77. package/dist/watchers/ModelWatcher.d.ts +6 -0
  78. package/dist/watchers/ModelWatcher.js +42 -0
  79. package/dist/watchers/NotificationWatcher.d.ts +6 -0
  80. package/dist/watchers/NotificationWatcher.js +42 -0
  81. package/dist/watchers/QueryWatcher.d.ts +2 -0
  82. package/dist/watchers/QueryWatcher.js +72 -0
  83. package/dist/watchers/RedisWatcher.d.ts +7 -0
  84. package/dist/watchers/RedisWatcher.js +38 -0
  85. package/dist/watchers/ScheduleWatcher.d.ts +6 -0
  86. package/dist/watchers/ScheduleWatcher.js +46 -0
  87. package/dist/watchers/ViewWatcher.d.ts +6 -0
  88. package/dist/watchers/ViewWatcher.js +36 -0
  89. package/package.json +59 -0
  90. package/src/cli-register.ts +63 -0
  91. package/src/config.ts +46 -0
  92. package/src/context.ts +101 -0
  93. package/src/dashboard/handlers.ts +197 -0
  94. package/src/dashboard/routes.ts +101 -0
  95. package/src/dashboard/ui.ts +879 -0
  96. package/src/dashboard/zintrust-debuger.svg +30 -0
  97. package/src/index.ts +88 -0
  98. package/src/plugin.ts +9 -0
  99. package/src/register.ts +219 -0
  100. package/src/storage/TraceStorage.ts +306 -0
  101. package/src/storage/index.ts +2 -0
  102. package/src/types.ts +317 -0
  103. package/src/ui.ts +9 -0
  104. package/src/utils/authTag.ts +20 -0
  105. package/src/utils/familyHash.ts +8 -0
  106. package/src/utils/redact.ts +64 -0
  107. package/src/utils/requestFilter.ts +33 -0
  108. package/src/utils/stackFrame.ts +44 -0
  109. package/src/watchers/AuthWatcher.ts +50 -0
  110. package/src/watchers/BatchWatcher.ts +52 -0
  111. package/src/watchers/CacheWatcher.ts +58 -0
  112. package/src/watchers/CommandWatcher.ts +55 -0
  113. package/src/watchers/DumpWatcher.ts +42 -0
  114. package/src/watchers/EventWatcher.ts +43 -0
  115. package/src/watchers/ExceptionWatcher.ts +114 -0
  116. package/src/watchers/GateWatcher.ts +50 -0
  117. package/src/watchers/HttpClientWatcher.ts +56 -0
  118. package/src/watchers/HttpWatcher.ts +94 -0
  119. package/src/watchers/JobWatcher.ts +121 -0
  120. package/src/watchers/LogWatcher.ts +61 -0
  121. package/src/watchers/MailWatcher.ts +47 -0
  122. package/src/watchers/MiddlewareWatcher.ts +42 -0
  123. package/src/watchers/ModelWatcher.ts +48 -0
  124. package/src/watchers/NotificationWatcher.ts +43 -0
  125. package/src/watchers/QueryWatcher.ts +85 -0
  126. package/src/watchers/RedisWatcher.ts +39 -0
  127. package/src/watchers/ScheduleWatcher.ts +54 -0
  128. 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
+ });