@zintrust/trace 0.4.76 → 0.4.79

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 (47) hide show
  1. package/README.md +101 -15
  2. package/dist/build-manifest.json +78 -38
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.js +123 -4
  5. package/dist/dashboard/ui.js +88 -29
  6. package/dist/index.d.ts +7 -0
  7. package/dist/index.js +5 -0
  8. package/dist/migrations/20260331000001_create_zin_trace_entries_table.js +1 -1
  9. package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.d.ts +10 -0
  10. package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.js +34 -0
  11. package/dist/migrations/index.js +2 -1
  12. package/dist/register.js +107 -9
  13. package/dist/storage/TraceContentRedaction.d.ts +4 -0
  14. package/dist/storage/TraceContentRedaction.js +33 -0
  15. package/dist/storage/TraceEntryFiltering.d.ts +4 -0
  16. package/dist/storage/TraceEntryFiltering.js +13 -0
  17. package/dist/storage/TraceStorage.js +35 -5
  18. package/dist/storage/TraceWriteDiagnostics.d.ts +19 -0
  19. package/dist/storage/TraceWriteDiagnostics.js +98 -0
  20. package/dist/types.d.ts +38 -21
  21. package/dist/utils/entryFilter.d.ts +4 -0
  22. package/dist/utils/entryFilter.js +95 -0
  23. package/dist/utils/redact.d.ts +1 -0
  24. package/dist/utils/redact.js +43 -9
  25. package/dist/watchers/CommandWatcher.js +1 -1
  26. package/dist/watchers/ExceptionWatcher.d.ts +8 -1
  27. package/dist/watchers/ExceptionWatcher.js +12 -7
  28. package/dist/watchers/HttpClientWatcher.js +1 -1
  29. package/dist/watchers/HttpWatcher.js +112 -21
  30. package/package.json +2 -2
  31. package/src/config.ts +152 -5
  32. package/src/dashboard/routes.ts +6 -2
  33. package/src/dashboard/ui.ts +88 -29
  34. package/src/index.ts +10 -0
  35. package/src/register.ts +137 -10
  36. package/src/storage/TraceContentRedaction.ts +44 -0
  37. package/src/storage/TraceEntryFiltering.ts +14 -0
  38. package/src/storage/TraceStorage.ts +52 -5
  39. package/src/storage/TraceWriteDiagnostics.ts +174 -0
  40. package/src/types.ts +41 -21
  41. package/src/utils/entryFilter.ts +108 -0
  42. package/src/utils/redact.ts +57 -9
  43. package/src/watchers/CommandWatcher.ts +1 -1
  44. package/src/watchers/ExceptionWatcher.ts +21 -8
  45. package/src/watchers/HttpClientWatcher.ts +1 -1
  46. package/src/watchers/HttpWatcher.ts +142 -23
  47. package/src/watchers/LogWatcher.ts +26 -28
@@ -7,7 +7,7 @@ import { TraceContext } from '../context';
7
7
  import type { ITraceConfig, ITraceWatcher, ITraceWatcherConfig, RequestContent } from '../types';
8
8
  import { EntryType } from '../types';
9
9
  import { AuthTag } from '../utils/authTag';
10
- import { redactHeaders, redactObject } from '../utils/redact';
10
+ import { redactHeaders, redactObject, redactUnknown } from '../utils/redact';
11
11
  import { RequestFilter } from '../utils/requestFilter';
12
12
 
13
13
  const normalizeHeaders = (headers: IRequest['headers']): Record<string, string> => {
@@ -22,23 +22,132 @@ const normalizeHeaders = (headers: IRequest['headers']): Record<string, string>
22
22
  );
23
23
  };
24
24
 
25
+ const normalizeHeaderValue = (value: string | string[]): string => {
26
+ return Array.isArray(value) ? value.join(', ') : value;
27
+ };
28
+
29
+ const resolveRequestPayload = (req: IRequest, config: ITraceConfig): unknown => {
30
+ const redactFields = [...config.redaction.keys, ...config.redaction.body];
31
+ const requestBody = typeof req.getBody === 'function' ? req.getBody() : req.body;
32
+
33
+ if (requestBody === undefined || requestBody === null) return {};
34
+ if (typeof requestBody === 'object') {
35
+ return redactObject(requestBody as Record<string, unknown>, redactFields);
36
+ }
37
+
38
+ return redactUnknown(requestBody, redactFields);
39
+ };
40
+
41
+ type ResponseCapture = {
42
+ headers: Record<string, string>;
43
+ body?: unknown;
44
+ restore(): void;
45
+ };
46
+
47
+ type RawResponseWithLifecycle = ReturnType<IResponse['getRaw']> & {
48
+ once?: (event: 'finish' | 'close', listener: () => void) => unknown;
49
+ off?: (event: 'finish' | 'close', listener: () => void) => unknown;
50
+ };
51
+
52
+ const registerCompletionHandler = (response: IResponse, onComplete: () => void): (() => void) => {
53
+ const raw: RawResponseWithLifecycle = response.getRaw();
54
+ if (typeof raw.once !== 'function') return () => undefined;
55
+
56
+ let completed = false;
57
+
58
+ const cleanup = (): void => {
59
+ if (typeof raw.off === 'function') {
60
+ raw.off('finish', markCompleted);
61
+ raw.off('close', markCompleted);
62
+ }
63
+ };
64
+
65
+ const markCompleted = (): void => {
66
+ if (completed) return;
67
+ completed = true;
68
+ cleanup();
69
+ onComplete();
70
+ };
71
+
72
+ raw.once('finish', markCompleted);
73
+ raw.once('close', markCompleted);
74
+
75
+ return cleanup;
76
+ };
77
+
78
+ const captureResponse = (response: IResponse, config: ITraceConfig): ResponseCapture => {
79
+ const headers: Record<string, string> = {};
80
+ const redactFields = [...config.redaction.keys, ...config.redaction.body];
81
+
82
+ const originalSetHeader = response.setHeader;
83
+ const originalJson = response.json;
84
+ const originalText = response.text;
85
+ const originalHtml = response.html;
86
+ const originalSend = response.send;
87
+
88
+ const capture: ResponseCapture = {
89
+ headers,
90
+ body: undefined,
91
+ restore(): void {
92
+ response.setHeader = originalSetHeader;
93
+ response.json = originalJson;
94
+ response.text = originalText;
95
+ response.html = originalHtml;
96
+ response.send = originalSend;
97
+ },
98
+ };
99
+
100
+ response.setHeader = function setHeader(name: string, value: string | string[]): IResponse {
101
+ headers[name] = normalizeHeaderValue(value);
102
+ return originalSetHeader.call(this, name, value);
103
+ };
104
+
105
+ response.json = function json(data: unknown): void {
106
+ capture.body = redactUnknown(data, redactFields);
107
+ originalJson.call(this, data);
108
+ };
109
+
110
+ response.text = function text(value: string): void {
111
+ capture.body = value;
112
+ originalText.call(this, value);
113
+ };
114
+
115
+ response.html = function html(value: string): void {
116
+ capture.body = value;
117
+ originalHtml.call(this, value);
118
+ };
119
+
120
+ response.send = function send(data: string | Buffer): void {
121
+ capture.body = typeof data === 'string' ? data : `[binary ${data.length} bytes]`;
122
+ originalSend.call(this, data);
123
+ };
124
+
125
+ return capture;
126
+ };
127
+
25
128
  const buildEntry = (
26
129
  req: IRequest,
27
130
  res: IResponse,
28
131
  start: number,
29
- config: ITraceConfig
132
+ config: ITraceConfig,
133
+ responseCapture: ResponseCapture
30
134
  ): RequestContent => {
31
- const headers = redactHeaders(normalizeHeaders(req.headers), config.redaction.headers);
32
-
33
- const payload = req.body ? redactObject(req.body, config.redaction.body) : {};
135
+ const headers = redactHeaders(normalizeHeaders(req.headers), [
136
+ ...config.redaction.keys,
137
+ ...config.redaction.headers,
138
+ ]);
34
139
 
35
140
  return {
36
141
  method: req.getMethod(),
37
142
  uri: req.getPath(),
38
143
  headers,
39
- payload,
144
+ payload: resolveRequestPayload(req, config),
40
145
  responseStatus: res.getStatus(),
41
- responseHeaders: {},
146
+ responseHeaders: redactHeaders(responseCapture.headers, [
147
+ ...config.redaction.keys,
148
+ ...config.redaction.headers,
149
+ ]),
150
+ responseBody: responseCapture.body,
42
151
  duration: Date.now() - start,
43
152
  memory: TraceContext.getMemory(),
44
153
  middleware: [],
@@ -68,24 +177,34 @@ export const HttpWatcher: ITraceWatcher = Object.freeze({
68
177
 
69
178
  const start = TraceContext.now();
70
179
  const batchId = TraceContext.getBatchId();
180
+ const responseCapture = captureResponse(response, config);
181
+ let didPersist = false;
71
182
 
72
- await next();
183
+ const persistEntry = (): void => {
184
+ if (didPersist) return;
185
+ didPersist = true;
186
+
187
+ const content = buildEntry(request, response, start, config, responseCapture);
188
+ const tags = AuthTag.append([]);
189
+ if (content.responseStatus >= 500) tags.push('failed');
190
+
191
+ responseCapture.restore();
73
192
 
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
193
+ storage
194
+ .writeEntry({
195
+ uuid: crypto.randomUUID(),
196
+ batchId,
197
+ type: EntryType.REQUEST,
198
+ content,
199
+ tags,
200
+ isLatest: true,
201
+ createdAt: TraceContext.now(),
202
+ })
203
+ .catch(() => undefined); // fire-and-forget
204
+ };
205
+
206
+ registerCompletionHandler(response, persistEntry);
207
+ await next();
89
208
  };
90
209
 
91
210
  registerMiddleware(middleware);
@@ -8,8 +8,6 @@ import { EntryType } from '../types';
8
8
  import { AuthTag } from '../utils/authTag';
9
9
  import { RequestFilter } from '../utils/requestFilter';
10
10
 
11
- type LoggerSink = (level: string, message: string, context?: Record<string, unknown>) => void;
12
-
13
11
  const LEVEL_PRIORITY: Record<string, number> = {
14
12
  debug: 0,
15
13
  info: 1,
@@ -24,37 +22,37 @@ export const LogWatcher: ITraceWatcher = Object.freeze({
24
22
 
25
23
  const minPriority = LEVEL_PRIORITY[config.logMinLevel] ?? 1;
26
24
 
27
- const loggerWithSink = Logger as typeof Logger & {
28
- addSink?: (fn: LoggerSink) => () => void;
29
- };
25
+ const loggerWithSink = Logger;
30
26
 
31
27
  if (typeof loggerWithSink.addSink !== 'function') {
32
28
  return () => undefined;
33
29
  }
34
30
 
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
- });
31
+ const unsubscribe = loggerWithSink.addSink(
32
+ (level: string, message: string, context?: Record<string, unknown>) => {
33
+ if ((LEVEL_PRIORITY[level] ?? 0) < minPriority) return;
34
+ if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes)) return;
35
+
36
+ const content: LogContent = {
37
+ level,
38
+ message,
39
+ context: context ?? undefined,
40
+ hostname: TraceContext.getHostname(),
41
+ };
42
+
43
+ storage
44
+ .writeEntry({
45
+ uuid: crypto.randomUUID(),
46
+ batchId: TraceContext.getBatchId(),
47
+ type: EntryType.LOG,
48
+ content,
49
+ tags: AuthTag.append([]),
50
+ isLatest: true,
51
+ createdAt: TraceContext.now(),
52
+ })
53
+ .catch(() => undefined);
54
+ }
55
+ );
58
56
 
59
57
  return unsubscribe;
60
58
  },