@zintrust/trace 0.4.77 → 0.4.81
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/dist/dashboard/ui.js +10 -8
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2 -2
- package/dist/types.d.ts +1 -1
- package/dist/watchers/ExceptionWatcher.d.ts +8 -1
- package/dist/watchers/ExceptionWatcher.js +12 -7
- package/dist/watchers/HttpWatcher.js +11 -4
- package/dist/watchers/LogWatcher.js +9 -1
- package/package.json +3 -3
- package/src/dashboard/ui.ts +10 -8
- package/src/index.ts +5 -2
- package/src/types.ts +1 -1
- package/src/watchers/ExceptionWatcher.ts +21 -8
- package/src/watchers/HttpWatcher.ts +14 -6
- package/src/watchers/LogWatcher.ts +10 -2
package/dist/dashboard/ui.js
CHANGED
|
@@ -335,7 +335,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
335
335
|
].join('');
|
|
336
336
|
};
|
|
337
337
|
|
|
338
|
-
const highlightJson = (value) => {
|
|
338
|
+
const highlightJson = (value, label = 'JSON') => {
|
|
339
339
|
const source = prettyJson(value);
|
|
340
340
|
let output = '';
|
|
341
341
|
let lastIndex = 0;
|
|
@@ -354,7 +354,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
output += escapeHtml(source.slice(lastIndex));
|
|
357
|
-
return renderCodeCard(
|
|
357
|
+
return renderCodeCard(label, source, output, 'language-json');
|
|
358
358
|
};
|
|
359
359
|
|
|
360
360
|
const highlightSql = (sql) => {
|
|
@@ -384,11 +384,11 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
384
384
|
return renderCodeCard('SQL', source, output, 'language-sql');
|
|
385
385
|
};
|
|
386
386
|
|
|
387
|
-
const detailJson = (value) => highlightJson(value ?? {});
|
|
387
|
+
const detailJson = (value, label = 'JSON') => highlightJson(value ?? {}, label);
|
|
388
388
|
|
|
389
389
|
const entrySummaryText = (entry) => {
|
|
390
390
|
const content = entry && entry.content ? entry.content : {};
|
|
391
|
-
if (entry.type === 'request') return [content.method || '', content.uri || ''].filter(Boolean).join(' ');
|
|
391
|
+
if (entry.type === 'request') return [content.responseStatus || '', content.method || '', content.uri || ''].filter(Boolean).join(' ');
|
|
392
392
|
if (entry.type === 'query') return String(content.sql || '').slice(0, 160);
|
|
393
393
|
if (entry.type === 'exception') return [content.class || '', content.message || ''].filter(Boolean).join(': ');
|
|
394
394
|
if (entry.type === 'log') return '[' + String(content.level || 'log') + '] ' + String(content.message || '').slice(0, 160);
|
|
@@ -567,9 +567,9 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
567
567
|
]),
|
|
568
568
|
'</div>'
|
|
569
569
|
].join(''),
|
|
570
|
-
payload: detailJson(content.payload || {}),
|
|
571
|
-
headers: '<div class="detail-stack">' + detailJson(content.headers || {}) + detailJson(content.responseHeaders || {}) + '</div>',
|
|
572
|
-
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody)) + detailJson(content.responseHeaders || {}) + '</div>',
|
|
570
|
+
payload: detailJson(content.payload || {}, 'Payload Json'),
|
|
571
|
+
headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
572
|
+
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
573
573
|
queries: renderTraceItems(batchEntriesByType('query')),
|
|
574
574
|
logs: renderTraceItems(batchEntriesByType('log')),
|
|
575
575
|
exceptions: renderTraceItems(batchEntriesByType('exception')),
|
|
@@ -581,7 +581,9 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
581
581
|
main.innerHTML = [
|
|
582
582
|
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
583
583
|
'<section class="panel detail-card">',
|
|
584
|
-
'<div
|
|
584
|
+
'<div>' + (entry.type === 'request'
|
|
585
|
+
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> <span class="' + typeClass(entry) + '">' + escapeHtml(content.responseStatus || '') + '</span> <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
|
|
586
|
+
: '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
|
|
585
587
|
'<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
|
|
586
588
|
'<div class="trace-tabs">',
|
|
587
589
|
traceTabs.map((tab) => '<button type="button" class="trace-tab' + (tab.id === currentTab ? ' active' : '') + '" data-action="detail-tab" data-tab="' + escapeHtml(tab.id) + '">' + escapeHtml(tab.label) + (tab.count !== undefined ? ' (' + escapeHtml(tab.count) + ')' : '') + '</button>').join(''),
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,11 @@ export { QueryWatcher } from './watchers/QueryWatcher';
|
|
|
32
32
|
export { RedisWatcher } from './watchers/RedisWatcher';
|
|
33
33
|
export { ScheduleWatcher } from './watchers/ScheduleWatcher';
|
|
34
34
|
export { ViewWatcher } from './watchers/ViewWatcher';
|
|
35
|
-
export declare const captureTraceException: (error: unknown
|
|
35
|
+
export declare const captureTraceException: (error: unknown, context?: {
|
|
36
|
+
batchId?: string;
|
|
37
|
+
hostname?: string;
|
|
38
|
+
path?: string;
|
|
39
|
+
userId?: string;
|
|
40
|
+
}) => void;
|
|
36
41
|
export { EntryType } from './types';
|
|
37
42
|
export type { AuthContent, BatchContent, CacheContent, ClientRequestContent, CommandContent, DumpContent, EntryTypeValue, EventContent, ExceptionContent, GateContent, ITraceConfig, ITraceEntry, ITraceWatcher, ITraceWatcherConfig, JobContent, LogContent, MailContent, MiddlewareContent, ModelContent, NotificationContent, QueryContent, RedactionConfig, RedisContent, RequestContent, ScheduleContent, TraceConfigOverrides, ViewContent, WatcherToggles, } from './types';
|
package/dist/index.js
CHANGED
|
@@ -46,8 +46,8 @@ export { QueryWatcher } from './watchers/QueryWatcher.js';
|
|
|
46
46
|
export { RedisWatcher } from './watchers/RedisWatcher.js';
|
|
47
47
|
export { ScheduleWatcher } from './watchers/ScheduleWatcher.js';
|
|
48
48
|
export { ViewWatcher } from './watchers/ViewWatcher.js';
|
|
49
|
-
export const captureTraceException = (error) => {
|
|
50
|
-
ExceptionWatcherApi.capture(error);
|
|
49
|
+
export const captureTraceException = (error, context) => {
|
|
50
|
+
ExceptionWatcherApi.capture(error, context);
|
|
51
51
|
};
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
// Types
|
package/dist/types.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface RequestContent {
|
|
|
30
30
|
method: string;
|
|
31
31
|
uri: string;
|
|
32
32
|
headers: Record<string, string>;
|
|
33
|
-
payload:
|
|
33
|
+
payload: unknown;
|
|
34
34
|
responseStatus: number;
|
|
35
35
|
responseHeaders: Record<string, string>;
|
|
36
36
|
responseBody?: unknown;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { ITraceWatcher } from '../types';
|
|
2
|
+
type ExceptionCaptureContext = {
|
|
3
|
+
batchId?: string;
|
|
4
|
+
hostname?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
userId?: string;
|
|
7
|
+
};
|
|
2
8
|
export declare const ExceptionWatcher: ITraceWatcher & {
|
|
3
|
-
capture: (err: unknown) => void;
|
|
9
|
+
capture: (err: unknown, context?: ExceptionCaptureContext) => void;
|
|
4
10
|
};
|
|
11
|
+
export {};
|
|
@@ -13,7 +13,7 @@ import { parseStackFrameLine } from '../utils/stackFrame.js';
|
|
|
13
13
|
const getLinePreview = (_file, _line) => {
|
|
14
14
|
return {};
|
|
15
15
|
};
|
|
16
|
-
const buildContent = (err) => {
|
|
16
|
+
const buildContent = (err, context) => {
|
|
17
17
|
const stack = err.stack ?? '';
|
|
18
18
|
const trace = stack
|
|
19
19
|
.split('\n')
|
|
@@ -30,8 +30,8 @@ const buildContent = (err) => {
|
|
|
30
30
|
trace,
|
|
31
31
|
linePreview: firstFrame ? getLinePreview(firstFrame.file, firstFrame.line) : {},
|
|
32
32
|
occurrences: 1,
|
|
33
|
-
hostname: TraceContext.getHostname(),
|
|
34
|
-
userId: TraceContext.getUserId(),
|
|
33
|
+
hostname: context?.hostname ?? TraceContext.getHostname(),
|
|
34
|
+
userId: context?.userId ?? TraceContext.getUserId(),
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
let _storage = null;
|
|
@@ -55,21 +55,26 @@ const unregisterProcessListeners = () => {
|
|
|
55
55
|
process.off('uncaughtException', handleUncaughtException);
|
|
56
56
|
process.off('unhandledRejection', handleUnhandledRejection);
|
|
57
57
|
};
|
|
58
|
-
const captureException = (err) => {
|
|
58
|
+
const captureException = (err, context) => {
|
|
59
59
|
const storage = _storage;
|
|
60
60
|
if (!storage)
|
|
61
61
|
return;
|
|
62
62
|
if (!(err instanceof Error))
|
|
63
63
|
return;
|
|
64
|
-
if (
|
|
64
|
+
if (context?.path !== undefined) {
|
|
65
|
+
if (RequestFilter.matchesIgnoredPath(context.path, _ignoreRoutes))
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
else if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) {
|
|
65
69
|
return;
|
|
66
|
-
|
|
70
|
+
}
|
|
71
|
+
const content = buildContent(err, context);
|
|
67
72
|
const hash = familyHash(`${content.class}:${content.file}:${content.line}`);
|
|
68
73
|
const uuid = crypto.randomUUID();
|
|
69
74
|
storage
|
|
70
75
|
.writeEntry({
|
|
71
76
|
uuid,
|
|
72
|
-
batchId: TraceContext.getBatchId(),
|
|
77
|
+
batchId: context?.batchId ?? TraceContext.getBatchId(),
|
|
73
78
|
familyHash: hash,
|
|
74
79
|
type: EntryType.EXCEPTION,
|
|
75
80
|
content,
|
|
@@ -17,6 +17,16 @@ const normalizeHeaders = (headers) => {
|
|
|
17
17
|
const normalizeHeaderValue = (value) => {
|
|
18
18
|
return Array.isArray(value) ? value.join(', ') : value;
|
|
19
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
|
+
};
|
|
20
30
|
const registerCompletionHandler = (response, onComplete) => {
|
|
21
31
|
const raw = response.getRaw();
|
|
22
32
|
if (typeof raw.once !== 'function')
|
|
@@ -85,14 +95,11 @@ const buildEntry = (req, res, start, config, responseCapture) => {
|
|
|
85
95
|
...config.redaction.keys,
|
|
86
96
|
...config.redaction.headers,
|
|
87
97
|
]);
|
|
88
|
-
const payload = req.body
|
|
89
|
-
? redactObject(req.body, [...config.redaction.keys, ...config.redaction.body])
|
|
90
|
-
: {};
|
|
91
98
|
return {
|
|
92
99
|
method: req.getMethod(),
|
|
93
100
|
uri: req.getPath(),
|
|
94
101
|
headers,
|
|
95
|
-
payload,
|
|
102
|
+
payload: resolveRequestPayload(req, config),
|
|
96
103
|
responseStatus: res.getStatus(),
|
|
97
104
|
responseHeaders: redactHeaders(responseCapture.headers, [
|
|
98
105
|
...config.redaction.keys,
|
|
@@ -6,7 +6,6 @@ import { TraceContext } from '../context.js';
|
|
|
6
6
|
import { EntryType } from '../types.js';
|
|
7
7
|
import { AuthTag } from '../utils/authTag.js';
|
|
8
8
|
import { RequestFilter } from '../utils/requestFilter.js';
|
|
9
|
-
// type LoggerSink = (level: string, message: string, context?: Record<string, unknown>) => void;
|
|
10
9
|
const LEVEL_PRIORITY = {
|
|
11
10
|
debug: 0,
|
|
12
11
|
info: 1,
|
|
@@ -14,6 +13,13 @@ const LEVEL_PRIORITY = {
|
|
|
14
13
|
error: 3,
|
|
15
14
|
fatal: 4,
|
|
16
15
|
};
|
|
16
|
+
const TRACE_INFRASTRUCTURE_LOG_MESSAGES = new Set([
|
|
17
|
+
'[MySQLProxyAdapter] Proxy request failed',
|
|
18
|
+
'[trace] Trace storage write degraded',
|
|
19
|
+
]);
|
|
20
|
+
const shouldSkipTraceInfrastructureLog = (message) => {
|
|
21
|
+
return TRACE_INFRASTRUCTURE_LOG_MESSAGES.has(message.trim());
|
|
22
|
+
};
|
|
17
23
|
export const LogWatcher = Object.freeze({
|
|
18
24
|
register({ storage, config }) {
|
|
19
25
|
if (config.watchers.log === false)
|
|
@@ -28,6 +34,8 @@ export const LogWatcher = Object.freeze({
|
|
|
28
34
|
return;
|
|
29
35
|
if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes))
|
|
30
36
|
return;
|
|
37
|
+
if (shouldSkipTraceInfrastructureLog(message))
|
|
38
|
+
return;
|
|
31
39
|
const content = {
|
|
32
40
|
level,
|
|
33
41
|
message,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.81",
|
|
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.80"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
|
@@ -56,4 +56,4 @@
|
|
|
56
56
|
"build": "tsc -p tsconfig.json && tsc -p tsconfig.migrations.json && node ../../scripts/fix-dist-esm-imports.mjs dist",
|
|
57
57
|
"prepublishOnly": "npm run build"
|
|
58
58
|
}
|
|
59
|
-
}
|
|
59
|
+
}
|
package/src/dashboard/ui.ts
CHANGED
|
@@ -341,7 +341,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
341
341
|
].join('');
|
|
342
342
|
};
|
|
343
343
|
|
|
344
|
-
const highlightJson = (value) => {
|
|
344
|
+
const highlightJson = (value, label = 'JSON') => {
|
|
345
345
|
const source = prettyJson(value);
|
|
346
346
|
let output = '';
|
|
347
347
|
let lastIndex = 0;
|
|
@@ -360,7 +360,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
output += escapeHtml(source.slice(lastIndex));
|
|
363
|
-
return renderCodeCard(
|
|
363
|
+
return renderCodeCard(label, source, output, 'language-json');
|
|
364
364
|
};
|
|
365
365
|
|
|
366
366
|
const highlightSql = (sql) => {
|
|
@@ -390,11 +390,11 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
390
390
|
return renderCodeCard('SQL', source, output, 'language-sql');
|
|
391
391
|
};
|
|
392
392
|
|
|
393
|
-
const detailJson = (value) => highlightJson(value ?? {});
|
|
393
|
+
const detailJson = (value, label = 'JSON') => highlightJson(value ?? {}, label);
|
|
394
394
|
|
|
395
395
|
const entrySummaryText = (entry) => {
|
|
396
396
|
const content = entry && entry.content ? entry.content : {};
|
|
397
|
-
if (entry.type === 'request') return [content.method || '', content.uri || ''].filter(Boolean).join(' ');
|
|
397
|
+
if (entry.type === 'request') return [content.responseStatus || '', content.method || '', content.uri || ''].filter(Boolean).join(' ');
|
|
398
398
|
if (entry.type === 'query') return String(content.sql || '').slice(0, 160);
|
|
399
399
|
if (entry.type === 'exception') return [content.class || '', content.message || ''].filter(Boolean).join(': ');
|
|
400
400
|
if (entry.type === 'log') return '[' + String(content.level || 'log') + '] ' + String(content.message || '').slice(0, 160);
|
|
@@ -573,9 +573,9 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
573
573
|
]),
|
|
574
574
|
'</div>'
|
|
575
575
|
].join(''),
|
|
576
|
-
payload: detailJson(content.payload || {}),
|
|
577
|
-
headers: '<div class="detail-stack">' + detailJson(content.headers || {}) + detailJson(content.responseHeaders || {}) + '</div>',
|
|
578
|
-
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody)) + detailJson(content.responseHeaders || {}) + '</div>',
|
|
576
|
+
payload: detailJson(content.payload || {}, 'Payload Json'),
|
|
577
|
+
headers: '<div class="detail-stack">' + detailJson(content.headers || {}, 'Request Header Json') + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
578
|
+
response: '<div class="detail-stack"><div class="detail-grid">' + renderMetricBox('Status', [{ label: 'Response status', value: escapeHtml(content.responseStatus || '') }, { label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }]) + '</div>' + (content.responseBody === undefined ? '<p class="trace-note">No response body was captured for this request.</p>' : detailJson(content.responseBody, 'Response Body Json')) + detailJson(content.responseHeaders || {}, 'Response Header Json') + '</div>',
|
|
579
579
|
queries: renderTraceItems(batchEntriesByType('query')),
|
|
580
580
|
logs: renderTraceItems(batchEntriesByType('log')),
|
|
581
581
|
exceptions: renderTraceItems(batchEntriesByType('exception')),
|
|
@@ -587,7 +587,9 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
587
587
|
main.innerHTML = [
|
|
588
588
|
'<span class="back-link" data-action="close-detail"><- Back to entries</span>',
|
|
589
589
|
'<section class="panel detail-card">',
|
|
590
|
-
'<div
|
|
590
|
+
'<div>' + (entry.type === 'request'
|
|
591
|
+
? '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> <span class="' + typeClass(entry) + '">' + escapeHtml(content.responseStatus || '') + '</span> <span class="mono">' + escapeHtml(content.uri || '') + '</span> ' + tagsHtml(entry.tags)
|
|
592
|
+
: '<span class="' + typeClass(entry) + '">' + escapeHtml(entry.type) + '</span> ' + tagsHtml(entry.tags)) + '</div>',
|
|
591
593
|
'<div class="detail-meta"><span>UUID <span class="mono">' + escapeHtml(entry.uuid) + '</span></span><span>Batch <span class="mono">' + escapeHtml(entry.batchId || '-') + '</span></span><span>' + durationHtml(entry) + '</span><span>' + escapeHtml(new Date(Number(entry.createdAt)).toISOString()) + '</span></div>',
|
|
592
594
|
'<div class="trace-tabs">',
|
|
593
595
|
traceTabs.map((tab) => '<button type="button" class="trace-tab' + (tab.id === currentTab ? ' active' : '') + '" data-action="detail-tab" data-tab="' + escapeHtml(tab.id) + '">' + escapeHtml(tab.label) + (tab.count !== undefined ? ' (' + escapeHtml(tab.count) + ')' : '') + '</button>').join(''),
|
package/src/index.ts
CHANGED
|
@@ -55,8 +55,11 @@ export { RedisWatcher } from './watchers/RedisWatcher';
|
|
|
55
55
|
export { ScheduleWatcher } from './watchers/ScheduleWatcher';
|
|
56
56
|
export { ViewWatcher } from './watchers/ViewWatcher';
|
|
57
57
|
|
|
58
|
-
export const captureTraceException = (
|
|
59
|
-
|
|
58
|
+
export const captureTraceException = (
|
|
59
|
+
error: unknown,
|
|
60
|
+
context?: { batchId?: string; hostname?: string; path?: string; userId?: string }
|
|
61
|
+
): void => {
|
|
62
|
+
ExceptionWatcherApi.capture(error, context);
|
|
60
63
|
};
|
|
61
64
|
|
|
62
65
|
// ---------------------------------------------------------------------------
|
package/src/types.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface RequestContent {
|
|
|
41
41
|
method: string;
|
|
42
42
|
uri: string;
|
|
43
43
|
headers: Record<string, string>;
|
|
44
|
-
payload:
|
|
44
|
+
payload: unknown;
|
|
45
45
|
responseStatus: number;
|
|
46
46
|
responseHeaders: Record<string, string>;
|
|
47
47
|
responseBody?: unknown;
|
|
@@ -12,11 +12,18 @@ import { familyHash } from '../utils/familyHash';
|
|
|
12
12
|
import { RequestFilter } from '../utils/requestFilter';
|
|
13
13
|
import { parseStackFrameLine } from '../utils/stackFrame';
|
|
14
14
|
|
|
15
|
+
type ExceptionCaptureContext = {
|
|
16
|
+
batchId?: string;
|
|
17
|
+
hostname?: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
userId?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
15
22
|
const getLinePreview = (_file: string, _line: number): Record<string, string> => {
|
|
16
23
|
return {};
|
|
17
24
|
};
|
|
18
25
|
|
|
19
|
-
const buildContent = (err: Error): ExceptionContent => {
|
|
26
|
+
const buildContent = (err: Error, context?: ExceptionCaptureContext): ExceptionContent => {
|
|
20
27
|
const stack = err.stack ?? '';
|
|
21
28
|
const trace: ExceptionContent['trace'] = stack
|
|
22
29
|
.split('\n')
|
|
@@ -35,8 +42,8 @@ const buildContent = (err: Error): ExceptionContent => {
|
|
|
35
42
|
trace,
|
|
36
43
|
linePreview: firstFrame ? getLinePreview(firstFrame.file, firstFrame.line) : {},
|
|
37
44
|
occurrences: 1,
|
|
38
|
-
hostname: TraceContext.getHostname(),
|
|
39
|
-
userId: TraceContext.getUserId(),
|
|
45
|
+
hostname: context?.hostname ?? TraceContext.getHostname(),
|
|
46
|
+
userId: context?.userId ?? TraceContext.getUserId(),
|
|
40
47
|
};
|
|
41
48
|
};
|
|
42
49
|
|
|
@@ -64,20 +71,24 @@ const unregisterProcessListeners = (): void => {
|
|
|
64
71
|
process.off('unhandledRejection', handleUnhandledRejection);
|
|
65
72
|
};
|
|
66
73
|
|
|
67
|
-
const captureException = (err: unknown): void => {
|
|
74
|
+
const captureException = (err: unknown, context?: ExceptionCaptureContext): void => {
|
|
68
75
|
const storage = _storage;
|
|
69
76
|
if (!storage) return;
|
|
70
77
|
if (!(err instanceof Error)) return;
|
|
71
|
-
if (
|
|
78
|
+
if (context?.path !== undefined) {
|
|
79
|
+
if (RequestFilter.matchesIgnoredPath(context.path, _ignoreRoutes)) return;
|
|
80
|
+
} else if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
72
83
|
|
|
73
|
-
const content = buildContent(err);
|
|
84
|
+
const content = buildContent(err, context);
|
|
74
85
|
const hash = familyHash(`${content.class}:${content.file}:${content.line}`);
|
|
75
86
|
const uuid = crypto.randomUUID();
|
|
76
87
|
|
|
77
88
|
storage
|
|
78
89
|
.writeEntry({
|
|
79
90
|
uuid,
|
|
80
|
-
batchId: TraceContext.getBatchId(),
|
|
91
|
+
batchId: context?.batchId ?? TraceContext.getBatchId(),
|
|
81
92
|
familyHash: hash,
|
|
82
93
|
type: EntryType.EXCEPTION,
|
|
83
94
|
content,
|
|
@@ -89,7 +100,9 @@ const captureException = (err: unknown): void => {
|
|
|
89
100
|
.catch(() => undefined);
|
|
90
101
|
};
|
|
91
102
|
|
|
92
|
-
export const ExceptionWatcher: ITraceWatcher & {
|
|
103
|
+
export const ExceptionWatcher: ITraceWatcher & {
|
|
104
|
+
capture: (err: unknown, context?: ExceptionCaptureContext) => void;
|
|
105
|
+
} = Object.freeze({
|
|
93
106
|
capture: captureException,
|
|
94
107
|
|
|
95
108
|
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
@@ -26,6 +26,18 @@ const normalizeHeaderValue = (value: string | string[]): string => {
|
|
|
26
26
|
return Array.isArray(value) ? value.join(', ') : value;
|
|
27
27
|
};
|
|
28
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
|
+
|
|
29
41
|
type ResponseCapture = {
|
|
30
42
|
headers: Record<string, string>;
|
|
31
43
|
body?: unknown;
|
|
@@ -38,7 +50,7 @@ type RawResponseWithLifecycle = ReturnType<IResponse['getRaw']> & {
|
|
|
38
50
|
};
|
|
39
51
|
|
|
40
52
|
const registerCompletionHandler = (response: IResponse, onComplete: () => void): (() => void) => {
|
|
41
|
-
const raw = response.getRaw()
|
|
53
|
+
const raw: RawResponseWithLifecycle = response.getRaw();
|
|
42
54
|
if (typeof raw.once !== 'function') return () => undefined;
|
|
43
55
|
|
|
44
56
|
let completed = false;
|
|
@@ -125,15 +137,11 @@ const buildEntry = (
|
|
|
125
137
|
...config.redaction.headers,
|
|
126
138
|
]);
|
|
127
139
|
|
|
128
|
-
const payload = req.body
|
|
129
|
-
? redactObject(req.body, [...config.redaction.keys, ...config.redaction.body])
|
|
130
|
-
: {};
|
|
131
|
-
|
|
132
140
|
return {
|
|
133
141
|
method: req.getMethod(),
|
|
134
142
|
uri: req.getPath(),
|
|
135
143
|
headers,
|
|
136
|
-
payload,
|
|
144
|
+
payload: resolveRequestPayload(req, config),
|
|
137
145
|
responseStatus: res.getStatus(),
|
|
138
146
|
responseHeaders: redactHeaders(responseCapture.headers, [
|
|
139
147
|
...config.redaction.keys,
|
|
@@ -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,
|
|
@@ -18,6 +16,15 @@ const LEVEL_PRIORITY: Record<string, number> = {
|
|
|
18
16
|
fatal: 4,
|
|
19
17
|
};
|
|
20
18
|
|
|
19
|
+
const TRACE_INFRASTRUCTURE_LOG_MESSAGES = new Set<string>([
|
|
20
|
+
'[MySQLProxyAdapter] Proxy request failed',
|
|
21
|
+
'[trace] Trace storage write degraded',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const shouldSkipTraceInfrastructureLog = (message: string): boolean => {
|
|
25
|
+
return TRACE_INFRASTRUCTURE_LOG_MESSAGES.has(message.trim());
|
|
26
|
+
};
|
|
27
|
+
|
|
21
28
|
export const LogWatcher: ITraceWatcher = Object.freeze({
|
|
22
29
|
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
23
30
|
if (config.watchers.log === false) return () => undefined;
|
|
@@ -34,6 +41,7 @@ export const LogWatcher: ITraceWatcher = Object.freeze({
|
|
|
34
41
|
(level: string, message: string, context?: Record<string, unknown>) => {
|
|
35
42
|
if ((LEVEL_PRIORITY[level] ?? 0) < minPriority) return;
|
|
36
43
|
if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes)) return;
|
|
44
|
+
if (shouldSkipTraceInfrastructureLog(message)) return;
|
|
37
45
|
|
|
38
46
|
const content: LogContent = {
|
|
39
47
|
level,
|