@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TraceContext } from '../context.js';
|
|
2
2
|
import { EntryType } from '../types.js';
|
|
3
3
|
import { AuthTag } from '../utils/authTag.js';
|
|
4
|
-
import { redactHeaders, redactObject } from '../utils/redact.js';
|
|
4
|
+
import { redactHeaders, redactObject, redactUnknown } from '../utils/redact.js';
|
|
5
5
|
import { RequestFilter } from '../utils/requestFilter.js';
|
|
6
6
|
const normalizeHeaders = (headers) => {
|
|
7
7
|
if (!headers)
|
|
@@ -14,16 +14,98 @@ const normalizeHeaders = (headers) => {
|
|
|
14
14
|
return [];
|
|
15
15
|
}));
|
|
16
16
|
};
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const normalizeHeaderValue = (value) => {
|
|
18
|
+
return Array.isArray(value) ? value.join(', ') : value;
|
|
19
|
+
};
|
|
20
|
+
const resolveRequestPayload = (req, config) => {
|
|
21
|
+
const redactFields = [...config.redaction.keys, ...config.redaction.body];
|
|
22
|
+
const requestBody = typeof req.getBody === 'function' ? req.getBody() : req.body;
|
|
23
|
+
if (requestBody === undefined || requestBody === null)
|
|
24
|
+
return {};
|
|
25
|
+
if (typeof requestBody === 'object') {
|
|
26
|
+
return redactObject(requestBody, redactFields);
|
|
27
|
+
}
|
|
28
|
+
return redactUnknown(requestBody, redactFields);
|
|
29
|
+
};
|
|
30
|
+
const registerCompletionHandler = (response, onComplete) => {
|
|
31
|
+
const raw = response.getRaw();
|
|
32
|
+
if (typeof raw.once !== 'function')
|
|
33
|
+
return () => undefined;
|
|
34
|
+
let completed = false;
|
|
35
|
+
const cleanup = () => {
|
|
36
|
+
if (typeof raw.off === 'function') {
|
|
37
|
+
raw.off('finish', markCompleted);
|
|
38
|
+
raw.off('close', markCompleted);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const markCompleted = () => {
|
|
42
|
+
if (completed)
|
|
43
|
+
return;
|
|
44
|
+
completed = true;
|
|
45
|
+
cleanup();
|
|
46
|
+
onComplete();
|
|
47
|
+
};
|
|
48
|
+
raw.once('finish', markCompleted);
|
|
49
|
+
raw.once('close', markCompleted);
|
|
50
|
+
return cleanup;
|
|
51
|
+
};
|
|
52
|
+
const captureResponse = (response, config) => {
|
|
53
|
+
const headers = {};
|
|
54
|
+
const redactFields = [...config.redaction.keys, ...config.redaction.body];
|
|
55
|
+
const originalSetHeader = response.setHeader;
|
|
56
|
+
const originalJson = response.json;
|
|
57
|
+
const originalText = response.text;
|
|
58
|
+
const originalHtml = response.html;
|
|
59
|
+
const originalSend = response.send;
|
|
60
|
+
const capture = {
|
|
61
|
+
headers,
|
|
62
|
+
body: undefined,
|
|
63
|
+
restore() {
|
|
64
|
+
response.setHeader = originalSetHeader;
|
|
65
|
+
response.json = originalJson;
|
|
66
|
+
response.text = originalText;
|
|
67
|
+
response.html = originalHtml;
|
|
68
|
+
response.send = originalSend;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
response.setHeader = function setHeader(name, value) {
|
|
72
|
+
headers[name] = normalizeHeaderValue(value);
|
|
73
|
+
return originalSetHeader.call(this, name, value);
|
|
74
|
+
};
|
|
75
|
+
response.json = function json(data) {
|
|
76
|
+
capture.body = redactUnknown(data, redactFields);
|
|
77
|
+
originalJson.call(this, data);
|
|
78
|
+
};
|
|
79
|
+
response.text = function text(value) {
|
|
80
|
+
capture.body = value;
|
|
81
|
+
originalText.call(this, value);
|
|
82
|
+
};
|
|
83
|
+
response.html = function html(value) {
|
|
84
|
+
capture.body = value;
|
|
85
|
+
originalHtml.call(this, value);
|
|
86
|
+
};
|
|
87
|
+
response.send = function send(data) {
|
|
88
|
+
capture.body = typeof data === 'string' ? data : `[binary ${data.length} bytes]`;
|
|
89
|
+
originalSend.call(this, data);
|
|
90
|
+
};
|
|
91
|
+
return capture;
|
|
92
|
+
};
|
|
93
|
+
const buildEntry = (req, res, start, config, responseCapture) => {
|
|
94
|
+
const headers = redactHeaders(normalizeHeaders(req.headers), [
|
|
95
|
+
...config.redaction.keys,
|
|
96
|
+
...config.redaction.headers,
|
|
97
|
+
]);
|
|
20
98
|
return {
|
|
21
99
|
method: req.getMethod(),
|
|
22
100
|
uri: req.getPath(),
|
|
23
101
|
headers,
|
|
24
|
-
payload,
|
|
102
|
+
payload: resolveRequestPayload(req, config),
|
|
25
103
|
responseStatus: res.getStatus(),
|
|
26
|
-
responseHeaders:
|
|
104
|
+
responseHeaders: redactHeaders(responseCapture.headers, [
|
|
105
|
+
...config.redaction.keys,
|
|
106
|
+
...config.redaction.headers,
|
|
107
|
+
]),
|
|
108
|
+
responseBody: responseCapture.body,
|
|
27
109
|
duration: Date.now() - start,
|
|
28
110
|
memory: TraceContext.getMemory(),
|
|
29
111
|
middleware: [],
|
|
@@ -48,22 +130,31 @@ export const HttpWatcher = Object.freeze({
|
|
|
48
130
|
return next();
|
|
49
131
|
const start = TraceContext.now();
|
|
50
132
|
const batchId = TraceContext.getBatchId();
|
|
133
|
+
const responseCapture = captureResponse(response, config);
|
|
134
|
+
let didPersist = false;
|
|
135
|
+
const persistEntry = () => {
|
|
136
|
+
if (didPersist)
|
|
137
|
+
return;
|
|
138
|
+
didPersist = true;
|
|
139
|
+
const content = buildEntry(request, response, start, config, responseCapture);
|
|
140
|
+
const tags = AuthTag.append([]);
|
|
141
|
+
if (content.responseStatus >= 500)
|
|
142
|
+
tags.push('failed');
|
|
143
|
+
responseCapture.restore();
|
|
144
|
+
storage
|
|
145
|
+
.writeEntry({
|
|
146
|
+
uuid: crypto.randomUUID(),
|
|
147
|
+
batchId,
|
|
148
|
+
type: EntryType.REQUEST,
|
|
149
|
+
content,
|
|
150
|
+
tags,
|
|
151
|
+
isLatest: true,
|
|
152
|
+
createdAt: TraceContext.now(),
|
|
153
|
+
})
|
|
154
|
+
.catch(() => undefined); // fire-and-forget
|
|
155
|
+
};
|
|
156
|
+
registerCompletionHandler(response, persistEntry);
|
|
51
157
|
await next();
|
|
52
|
-
const content = buildEntry(request, response, start, config);
|
|
53
|
-
const tags = AuthTag.append([]);
|
|
54
|
-
if (content.responseStatus >= 500)
|
|
55
|
-
tags.push('failed');
|
|
56
|
-
storage
|
|
57
|
-
.writeEntry({
|
|
58
|
-
uuid: crypto.randomUUID(),
|
|
59
|
-
batchId,
|
|
60
|
-
type: EntryType.REQUEST,
|
|
61
|
-
content,
|
|
62
|
-
tags,
|
|
63
|
-
isLatest: true,
|
|
64
|
-
createdAt: TraceContext.now(),
|
|
65
|
-
})
|
|
66
|
-
.catch(() => undefined); // fire-and-forget
|
|
67
158
|
};
|
|
68
159
|
registerMiddleware(middleware);
|
|
69
160
|
return () => undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.79",
|
|
4
4
|
"description": "Trace assistant for ZinTrust: logs requests, queries, exceptions, jobs, and more.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=20.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@zintrust/core": "^0.4.
|
|
43
|
+
"@zintrust/core": "^0.4.77"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TraceConfig — defaults and merge helper for @zintrust/trace
|
|
3
3
|
*/
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ITraceConfig,
|
|
6
|
+
TraceConfigOverrides,
|
|
7
|
+
TraceFilterRule,
|
|
8
|
+
TraceRequestWatcherConfig,
|
|
9
|
+
TraceWatcherToggle,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
const mergeStringLists = (base: string[], override?: string[]): string[] => {
|
|
13
|
+
const merged = new Set<string>();
|
|
14
|
+
|
|
15
|
+
for (const value of [...base, ...(override ?? [])]) {
|
|
16
|
+
if (typeof value !== 'string') continue;
|
|
17
|
+
const normalized = value.trim();
|
|
18
|
+
if (normalized !== '') merged.add(normalized);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return [...merged];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isObjectValue = (value: unknown): value is Record<string, unknown> => {
|
|
25
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mergeFilterRule = (
|
|
29
|
+
base?: TraceFilterRule,
|
|
30
|
+
override?: TraceFilterRule
|
|
31
|
+
): TraceFilterRule | undefined => {
|
|
32
|
+
const include = mergeStringLists(base?.include ?? [], override?.include);
|
|
33
|
+
const exclude = mergeStringLists(base?.exclude ?? [], override?.exclude);
|
|
34
|
+
|
|
35
|
+
if (include.length === 0 && exclude.length === 0) return undefined;
|
|
36
|
+
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
...(include.length > 0 ? { include } : {}),
|
|
39
|
+
...(exclude.length > 0 ? { exclude } : {}),
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const mergeWatcherToggle = (
|
|
44
|
+
base?: TraceWatcherToggle,
|
|
45
|
+
override?: TraceWatcherToggle
|
|
46
|
+
): TraceWatcherToggle | undefined => {
|
|
47
|
+
if (override === undefined) return base;
|
|
48
|
+
if (override === false || override === true) return override;
|
|
49
|
+
|
|
50
|
+
const baseRule = isObjectValue(base) ? base : undefined;
|
|
51
|
+
return mergeFilterRule(baseRule, override);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const REQUEST_METHOD_KEYS = ['all', 'get', 'post', 'put', 'patch', 'delete'] as const;
|
|
55
|
+
|
|
56
|
+
const mergeRequestWatcherToggle = (
|
|
57
|
+
base?: ITraceConfig['watchers']['request'],
|
|
58
|
+
override?: ITraceConfig['watchers']['request']
|
|
59
|
+
): ITraceConfig['watchers']['request'] | undefined => {
|
|
60
|
+
if (override === undefined) return base;
|
|
61
|
+
if (override === false || override === true) return override;
|
|
62
|
+
|
|
63
|
+
const baseConfig = isObjectValue(base) ? base : undefined;
|
|
64
|
+
const merged: TraceRequestWatcherConfig = mergeFilterRule(baseConfig, override) ?? {};
|
|
65
|
+
|
|
66
|
+
for (const key of REQUEST_METHOD_KEYS) {
|
|
67
|
+
const rule = mergeFilterRule(baseConfig?.[key], override[key]);
|
|
68
|
+
if (rule !== undefined) merged[key] = rule;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return merged;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const mergeWatchers = (
|
|
75
|
+
base: ITraceConfig['watchers'],
|
|
76
|
+
override?: TraceConfigOverrides['watchers']
|
|
77
|
+
): ITraceConfig['watchers'] => {
|
|
78
|
+
if (override === undefined) return { ...base };
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
...base,
|
|
82
|
+
...override,
|
|
83
|
+
request: mergeRequestWatcherToggle(base.request, override.request),
|
|
84
|
+
query: mergeWatcherToggle(base.query, override.query),
|
|
85
|
+
exception: mergeWatcherToggle(base.exception, override.exception),
|
|
86
|
+
log: mergeWatcherToggle(base.log, override.log),
|
|
87
|
+
job: mergeWatcherToggle(base.job, override.job),
|
|
88
|
+
cache: mergeWatcherToggle(base.cache, override.cache),
|
|
89
|
+
schedule: mergeWatcherToggle(base.schedule, override.schedule),
|
|
90
|
+
mail: mergeWatcherToggle(base.mail, override.mail),
|
|
91
|
+
auth: mergeWatcherToggle(base.auth, override.auth),
|
|
92
|
+
event: mergeWatcherToggle(base.event, override.event),
|
|
93
|
+
model: mergeWatcherToggle(base.model, override.model),
|
|
94
|
+
notification: mergeWatcherToggle(base.notification, override.notification),
|
|
95
|
+
redis: mergeWatcherToggle(base.redis, override.redis),
|
|
96
|
+
gate: mergeWatcherToggle(base.gate, override.gate),
|
|
97
|
+
middleware: mergeWatcherToggle(base.middleware, override.middleware),
|
|
98
|
+
command: mergeWatcherToggle(base.command, override.command),
|
|
99
|
+
batch: mergeWatcherToggle(base.batch, override.batch),
|
|
100
|
+
dump: mergeWatcherToggle(base.dump, override.dump),
|
|
101
|
+
view: mergeWatcherToggle(base.view, override.view),
|
|
102
|
+
clientRequest: mergeWatcherToggle(base.clientRequest, override.clientRequest),
|
|
103
|
+
};
|
|
104
|
+
};
|
|
5
105
|
|
|
6
106
|
const DEFAULTS: ITraceConfig = Object.freeze({
|
|
7
107
|
enabled: false,
|
|
@@ -12,6 +112,37 @@ const DEFAULTS: ITraceConfig = Object.freeze({
|
|
|
12
112
|
logMinLevel: 'info',
|
|
13
113
|
watchers: {},
|
|
14
114
|
redaction: {
|
|
115
|
+
keys: [
|
|
116
|
+
'password',
|
|
117
|
+
'pass',
|
|
118
|
+
'passwd',
|
|
119
|
+
'token',
|
|
120
|
+
'accessToken',
|
|
121
|
+
'access_token',
|
|
122
|
+
'refreshToken',
|
|
123
|
+
'refresh_token',
|
|
124
|
+
'secret',
|
|
125
|
+
'secretKey',
|
|
126
|
+
'secret_key',
|
|
127
|
+
'apiKey',
|
|
128
|
+
'api_key',
|
|
129
|
+
'auth',
|
|
130
|
+
'authToken',
|
|
131
|
+
'auth_token',
|
|
132
|
+
'authorization',
|
|
133
|
+
'cookie',
|
|
134
|
+
'session',
|
|
135
|
+
'sessionId',
|
|
136
|
+
'session_id',
|
|
137
|
+
'card',
|
|
138
|
+
'cardNumber',
|
|
139
|
+
'card_number',
|
|
140
|
+
'cardToken',
|
|
141
|
+
'card_token',
|
|
142
|
+
'cvv',
|
|
143
|
+
'cvc',
|
|
144
|
+
'pan',
|
|
145
|
+
],
|
|
15
146
|
headers: ['authorization', 'cookie', 'x-api-key', 'x-auth-token'],
|
|
16
147
|
body: ['password', 'token', 'secret', 'apiKey', 'api_key', 'jwt', 'bearer'],
|
|
17
148
|
query: [],
|
|
@@ -20,7 +151,20 @@ const DEFAULTS: ITraceConfig = Object.freeze({
|
|
|
20
151
|
|
|
21
152
|
const isWatcherEnabled = (config: ITraceConfig, key: keyof ITraceConfig['watchers']): boolean => {
|
|
22
153
|
const override = config.watchers[key];
|
|
23
|
-
|
|
154
|
+
if (override === false) return false;
|
|
155
|
+
if (isObjectValue(override) && override.enabled === false) return false;
|
|
156
|
+
return true; // undefined = enabled by default; explicit false = disabled
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const getRedactionFields = (
|
|
160
|
+
config: ITraceConfig,
|
|
161
|
+
key: keyof ITraceConfig['redaction']
|
|
162
|
+
): string[] => {
|
|
163
|
+
if (key === 'keys') {
|
|
164
|
+
return mergeStringLists([], config.redaction.keys);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return mergeStringLists(config.redaction.keys, config.redaction[key]);
|
|
24
168
|
};
|
|
25
169
|
|
|
26
170
|
export const TraceConfig = Object.freeze({
|
|
@@ -33,14 +177,17 @@ export const TraceConfig = Object.freeze({
|
|
|
33
177
|
return Object.freeze({
|
|
34
178
|
...DEFAULTS,
|
|
35
179
|
...overrides,
|
|
36
|
-
watchers:
|
|
180
|
+
watchers: mergeWatchers(DEFAULTS.watchers, overrides.watchers),
|
|
37
181
|
redaction: {
|
|
38
|
-
|
|
39
|
-
|
|
182
|
+
keys: mergeStringLists(DEFAULTS.redaction.keys, overrides.redaction?.keys),
|
|
183
|
+
headers: mergeStringLists(DEFAULTS.redaction.headers, overrides.redaction?.headers),
|
|
184
|
+
body: mergeStringLists(DEFAULTS.redaction.body, overrides.redaction?.body),
|
|
185
|
+
query: mergeStringLists(DEFAULTS.redaction.query, overrides.redaction?.query),
|
|
40
186
|
},
|
|
41
187
|
ignoreRoutes: overrides.ignoreRoutes ?? DEFAULTS.ignoreRoutes,
|
|
42
188
|
});
|
|
43
189
|
},
|
|
44
190
|
|
|
191
|
+
getRedactionFields,
|
|
45
192
|
isWatcherEnabled,
|
|
46
193
|
});
|
package/src/dashboard/routes.ts
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
} from './handlers';
|
|
21
21
|
import { buildDashboardHtml } from './ui';
|
|
22
22
|
|
|
23
|
+
type HtmlResponse = {
|
|
24
|
+
html(body: string): void;
|
|
25
|
+
};
|
|
26
|
+
|
|
23
27
|
export type TraceDashboardOptions = {
|
|
24
28
|
/** Base path for the dashboard, e.g. '/trace'. Defaults to '/trace'. */
|
|
25
29
|
basePath?: string;
|
|
@@ -61,7 +65,7 @@ export const registerTraceRoutes = (
|
|
|
61
65
|
Router.get(
|
|
62
66
|
router,
|
|
63
67
|
base,
|
|
64
|
-
(_req, res) => {
|
|
68
|
+
(_req: unknown, res: HtmlResponse) => {
|
|
65
69
|
res.html(buildDashboardHtml(base, appConfig.name));
|
|
66
70
|
},
|
|
67
71
|
routeOptions
|
|
@@ -70,7 +74,7 @@ export const registerTraceRoutes = (
|
|
|
70
74
|
Router.get(
|
|
71
75
|
router,
|
|
72
76
|
`${base}/*`,
|
|
73
|
-
(_req, res) => {
|
|
77
|
+
(_req: unknown, res: HtmlResponse) => {
|
|
74
78
|
res.html(buildDashboardHtml(base, appConfig.name));
|
|
75
79
|
},
|
|
76
80
|
routeOptions
|