@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.
- package/README.md +101 -15
- package/dist/build-manifest.json +78 -38
- package/dist/config.d.ts +1 -0
- package/dist/config.js +123 -4
- package/dist/dashboard/ui.js +88 -29
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/migrations/20260331000001_create_zin_trace_entries_table.js +1 -1
- package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.d.ts +10 -0
- package/dist/migrations/20260407193000_widen_trace_created_at_for_sql.js +34 -0
- package/dist/migrations/index.js +2 -1
- package/dist/register.js +107 -9
- package/dist/storage/TraceContentRedaction.d.ts +4 -0
- package/dist/storage/TraceContentRedaction.js +33 -0
- package/dist/storage/TraceEntryFiltering.d.ts +4 -0
- package/dist/storage/TraceEntryFiltering.js +13 -0
- package/dist/storage/TraceStorage.js +35 -5
- package/dist/storage/TraceWriteDiagnostics.d.ts +19 -0
- package/dist/storage/TraceWriteDiagnostics.js +98 -0
- package/dist/types.d.ts +38 -21
- package/dist/utils/entryFilter.d.ts +4 -0
- package/dist/utils/entryFilter.js +95 -0
- package/dist/utils/redact.d.ts +1 -0
- package/dist/utils/redact.js +43 -9
- package/dist/watchers/CommandWatcher.js +1 -1
- package/dist/watchers/ExceptionWatcher.d.ts +8 -1
- package/dist/watchers/ExceptionWatcher.js +12 -7
- package/dist/watchers/HttpClientWatcher.js +1 -1
- package/dist/watchers/HttpWatcher.js +112 -21
- package/package.json +2 -2
- package/src/config.ts +152 -5
- package/src/dashboard/routes.ts +6 -2
- package/src/dashboard/ui.ts +88 -29
- package/src/index.ts +10 -0
- package/src/register.ts +137 -10
- package/src/storage/TraceContentRedaction.ts +44 -0
- package/src/storage/TraceEntryFiltering.ts +14 -0
- package/src/storage/TraceStorage.ts +52 -5
- package/src/storage/TraceWriteDiagnostics.ts +174 -0
- package/src/types.ts +41 -21
- package/src/utils/entryFilter.ts +108 -0
- package/src/utils/redact.ts +57 -9
- package/src/watchers/CommandWatcher.ts +1 -1
- package/src/watchers/ExceptionWatcher.ts +21 -8
- package/src/watchers/HttpClientWatcher.ts +1 -1
- package/src/watchers/HttpWatcher.ts +142 -23
- 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),
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
},
|