@zintrust/trace 0.4.79 → 0.4.82
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/build-manifest.json +43 -43
- package/dist/config.js +2 -0
- package/dist/dashboard/ui.js +103 -8
- package/dist/register.js +16 -0
- package/dist/types.d.ts +29 -1
- package/dist/watchers/CacheWatcher.d.ts +1 -1
- package/dist/watchers/CacheWatcher.js +10 -2
- package/dist/watchers/HttpClientWatcher.d.ts +2 -2
- package/dist/watchers/HttpClientWatcher.js +17 -4
- package/dist/watchers/LogWatcher.js +9 -0
- package/dist/watchers/MailWatcher.d.ts +1 -1
- package/dist/watchers/MailWatcher.js +12 -3
- package/dist/watchers/NotificationWatcher.d.ts +1 -1
- package/dist/watchers/NotificationWatcher.js +9 -1
- package/dist/watchers/QueryWatcher.js +5 -1
- package/package.json +3 -3
- package/src/config.ts +2 -0
- package/src/dashboard/ui.ts +103 -8
- package/src/register.ts +16 -0
- package/src/types.ts +30 -1
- package/src/watchers/CacheWatcher.ts +13 -2
- package/src/watchers/HttpClientWatcher.ts +33 -11
- package/src/watchers/LogWatcher.ts +10 -0
- package/src/watchers/MailWatcher.ts +18 -3
- package/src/watchers/NotificationWatcher.ts +15 -1
- package/src/watchers/QueryWatcher.ts +5 -1
|
@@ -13,6 +13,13 @@ const LEVEL_PRIORITY = {
|
|
|
13
13
|
error: 3,
|
|
14
14
|
fatal: 4,
|
|
15
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
|
+
};
|
|
16
23
|
export const LogWatcher = Object.freeze({
|
|
17
24
|
register({ storage, config }) {
|
|
18
25
|
if (config.watchers.log === false)
|
|
@@ -27,6 +34,8 @@ export const LogWatcher = Object.freeze({
|
|
|
27
34
|
return;
|
|
28
35
|
if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes))
|
|
29
36
|
return;
|
|
37
|
+
if (shouldSkipTraceInfrastructureLog(message))
|
|
38
|
+
return;
|
|
30
39
|
const content = {
|
|
31
40
|
level,
|
|
32
41
|
message,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ITraceWatcher } from '../types';
|
|
2
|
-
declare const emit: (to: string, subject: string, template?: string) => void;
|
|
2
|
+
declare const emit: (to: string, subject: string, template?: string, text?: string, html?: string) => void;
|
|
3
3
|
export declare const MailWatcher: ITraceWatcher & {
|
|
4
4
|
emit: typeof emit;
|
|
5
5
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MailWatcher — records mail dispatch intent.
|
|
3
|
-
* Body is never captured; only to/subject/template.
|
|
2
|
+
* MailWatcher — records mail dispatch intent and rendered content.
|
|
4
3
|
*/
|
|
5
4
|
import { TraceContext } from '../context.js';
|
|
6
5
|
import { EntryType } from '../types.js';
|
|
6
|
+
import { redactUnknown } from '../utils/redact.js';
|
|
7
7
|
import { RequestFilter } from '../utils/requestFilter.js';
|
|
8
8
|
let _storage = null;
|
|
9
|
+
let _redactionFields = [];
|
|
9
10
|
let _ignoreRoutes = [];
|
|
10
|
-
const emit = (to, subject, template) => {
|
|
11
|
+
const emit = (to, subject, template, text, html) => {
|
|
11
12
|
if (!_storage)
|
|
12
13
|
return;
|
|
13
14
|
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes))
|
|
@@ -16,6 +17,12 @@ const emit = (to, subject, template) => {
|
|
|
16
17
|
to,
|
|
17
18
|
subject,
|
|
18
19
|
template,
|
|
20
|
+
...(typeof text === 'string' && text !== ''
|
|
21
|
+
? { text: redactUnknown(text, _redactionFields) }
|
|
22
|
+
: {}),
|
|
23
|
+
...(typeof html === 'string' && html !== ''
|
|
24
|
+
? { html: redactUnknown(html, _redactionFields) }
|
|
25
|
+
: {}),
|
|
19
26
|
hostname: TraceContext.getHostname(),
|
|
20
27
|
};
|
|
21
28
|
_storage
|
|
@@ -36,9 +43,11 @@ export const MailWatcher = Object.freeze({
|
|
|
36
43
|
if (config.watchers.mail === false)
|
|
37
44
|
return () => undefined;
|
|
38
45
|
_storage = storage;
|
|
46
|
+
_redactionFields = [...config.redaction.keys, ...config.redaction.body];
|
|
39
47
|
_ignoreRoutes = config.ignoreRoutes;
|
|
40
48
|
return () => {
|
|
41
49
|
_storage = null;
|
|
50
|
+
_redactionFields = [];
|
|
42
51
|
_ignoreRoutes = [];
|
|
43
52
|
};
|
|
44
53
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ITraceWatcher } from '../types';
|
|
2
|
-
declare const emit: (notification: string, channels: string[], notifiable?: string) => void;
|
|
2
|
+
declare const emit: (notification: string, channels: string[], notifiable?: string, message?: string, payload?: unknown) => void;
|
|
3
3
|
export declare const NotificationWatcher: ITraceWatcher & {
|
|
4
4
|
emit: typeof emit;
|
|
5
5
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { TraceContext } from '../context.js';
|
|
2
2
|
import { EntryType } from '../types.js';
|
|
3
3
|
import { AuthTag } from '../utils/authTag.js';
|
|
4
|
+
import { redactUnknown } from '../utils/redact.js';
|
|
4
5
|
import { RequestFilter } from '../utils/requestFilter.js';
|
|
5
6
|
let _storage = null;
|
|
7
|
+
let _redactionFields = [];
|
|
6
8
|
let _ignoreRoutes = [];
|
|
7
|
-
const emit = (notification, channels, notifiable) => {
|
|
9
|
+
const emit = (notification, channels, notifiable, message, payload) => {
|
|
8
10
|
if (!_storage)
|
|
9
11
|
return;
|
|
10
12
|
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes))
|
|
@@ -13,6 +15,10 @@ const emit = (notification, channels, notifiable) => {
|
|
|
13
15
|
notification,
|
|
14
16
|
channels,
|
|
15
17
|
notifiable,
|
|
18
|
+
...(typeof message === 'string' && message !== ''
|
|
19
|
+
? { message: redactUnknown(message, _redactionFields) }
|
|
20
|
+
: {}),
|
|
21
|
+
...(payload === undefined ? {} : { payload: redactUnknown(payload, _redactionFields) }),
|
|
16
22
|
hostname: TraceContext.getHostname(),
|
|
17
23
|
};
|
|
18
24
|
_storage
|
|
@@ -33,9 +39,11 @@ export const NotificationWatcher = Object.freeze({
|
|
|
33
39
|
if (config.watchers.notification === false)
|
|
34
40
|
return () => undefined;
|
|
35
41
|
_storage = storage;
|
|
42
|
+
_redactionFields = [...config.redaction.keys, ...config.redaction.body];
|
|
36
43
|
_ignoreRoutes = config.ignoreRoutes;
|
|
37
44
|
return () => {
|
|
38
45
|
_storage = null;
|
|
46
|
+
_redactionFields = [];
|
|
39
47
|
_ignoreRoutes = [];
|
|
40
48
|
};
|
|
41
49
|
},
|
|
@@ -35,13 +35,17 @@ export const QueryWatcher = Object.freeze({
|
|
|
35
35
|
if (isTraceStorageQuery(query))
|
|
36
36
|
return;
|
|
37
37
|
const batchId = TraceContext.getBatchId();
|
|
38
|
-
const
|
|
38
|
+
const includeBindings = config.captureQueryBindings !== false;
|
|
39
|
+
const sql = includeBindings ? bindingsInterpolated(query, params) : query;
|
|
39
40
|
const roundedDuration = Math.round(duration * 100) / 100;
|
|
40
41
|
const hash = TraceStorage.familyHash(query);
|
|
41
42
|
const slow = roundedDuration >= config.slowQueryThreshold;
|
|
42
43
|
const content = {
|
|
43
44
|
connection: 'default',
|
|
44
45
|
sql,
|
|
46
|
+
statement: query,
|
|
47
|
+
...(includeBindings ? { bindings: [...params] } : {}),
|
|
48
|
+
bindingsIncluded: includeBindings,
|
|
45
49
|
time: roundedDuration,
|
|
46
50
|
duration: roundedDuration,
|
|
47
51
|
slow,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.82",
|
|
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.81"
|
|
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/config.ts
CHANGED
|
@@ -109,6 +109,8 @@ const DEFAULTS: ITraceConfig = Object.freeze({
|
|
|
109
109
|
pruneAfterHours: 24,
|
|
110
110
|
ignoreRoutes: ['/trace', '/health', '/ping'],
|
|
111
111
|
slowQueryThreshold: 100,
|
|
112
|
+
captureCachePayloads: false,
|
|
113
|
+
captureQueryBindings: true,
|
|
112
114
|
logMinLevel: 'info',
|
|
113
115
|
watchers: {},
|
|
114
116
|
redaction: {
|
package/src/dashboard/ui.ts
CHANGED
|
@@ -47,7 +47,7 @@ const encodeSvgDataUri = (svg: string): string => {
|
|
|
47
47
|
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(compactSvg)}`;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
50
|
+
const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
51
51
|
<html lang="en">
|
|
52
52
|
<head>
|
|
53
53
|
<meta charset="UTF-8">
|
|
@@ -80,7 +80,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
80
80
|
.tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(56,189,248,.12);color:#bae6fd;font-size:.78rem;font-weight:800;margin:0 6px 6px 0;border:1px solid rgba(56,189,248,.18);text-decoration:none}button.tag{cursor:pointer}html[data-theme='light'] .tag{color:#075985}.tag.failed{background:rgba(239,68,68,.14);color:#fecaca;border-color:rgba(239,68,68,.2)}html[data-theme='light'] .tag.failed{color:#b91c1c}.tag.slow{background:rgba(245,158,11,.12);color:#fde68a;border-color:rgba(245,158,11,.18)}html[data-theme='light'] .tag.slow{color:#92400e}.type-pill{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border-radius:999px;font-size:.74rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;border:1px solid transparent}.pill-request{background:rgba(56,189,248,.14);color:#93c5fd}.pill-request.method-get{background:rgba(34,197,94,.16);color:#bbf7d0}.pill-request.method-post{background:rgba(59,130,246,.16);color:#bfdbfe}.pill-request.method-other{background:rgba(245,158,11,.16);color:#fde68a}.pill-query{background:rgba(34,197,94,.12);color:#86efac}.pill-exception{background:rgba(239,68,68,.14);color:#fecaca}.pill-log{background:rgba(168,85,247,.14);color:#ddd6fe}.pill-job,.pill-batch{background:rgba(245,158,11,.14);color:#fde68a}.pill-cache{background:rgba(20,184,166,.12);color:#99f6e4}.pill-schedule,.pill-command{background:rgba(14,165,233,.14);color:#bae6fd}.pill-mail,.pill-notification{background:rgba(236,72,153,.14);color:#fbcfe8}.pill-auth{background:rgba(148,163,184,.16);color:#e2e8f0}.pill-event,.pill-model{background:rgba(74,222,128,.14);color:#bbf7d0}.pill-redis{background:rgba(239,68,68,.12);color:#fecaca}.pill-gate{background:rgba(99,102,241,.14);color:#c7d2fe}.pill-middleware{background:rgba(45,212,191,.12);color:#ccfbf1}.pill-dump,.pill-view{background:rgba(148,163,184,.14);color:#e2e8f0}.pill-client-request{background:rgba(59,130,246,.14);color:#bfdbfe}html[data-theme='light'] .pill-request{color:#1d4ed8}html[data-theme='light'] .pill-request.method-get{color:#166534}html[data-theme='light'] .pill-request.method-post{color:#1d4ed8}html[data-theme='light'] .pill-request.method-other{color:#92400e}html[data-theme='light'] .pill-query{color:#166534}html[data-theme='light'] .pill-exception{color:#b91c1c}html[data-theme='light'] .pill-log{color:#6d28d9}html[data-theme='light'] .pill-job,html[data-theme='light'] .pill-batch{color:#92400e}html[data-theme='light'] .pill-cache{color:#115e59}html[data-theme='light'] .pill-schedule,html[data-theme='light'] .pill-command{color:#0c4a6e}html[data-theme='light'] .pill-mail,html[data-theme='light'] .pill-notification{color:#9d174d}html[data-theme='light'] .pill-auth,html[data-theme='light'] .pill-dump,html[data-theme='light'] .pill-view{color:#334155}html[data-theme='light'] .pill-event,html[data-theme='light'] .pill-model{color:#166534}html[data-theme='light'] .pill-redis{color:#991b1b}html[data-theme='light'] .pill-gate{color:#3730a3}html[data-theme='light'] .pill-middleware{color:#155e75}html[data-theme='light'] .pill-client-request{color:#1d4ed8}
|
|
81
81
|
.monitoring-wrap{padding:0 24px 24px}.tag-list{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:18px}.tag-item{display:inline-flex;align-items:center;gap:10px;padding:10px 14px;border-radius:999px;border:1px solid var(--line);background:var(--surface-strong)}.tag-remove{border:none;background:rgba(239,68,68,.14);color:var(--danger);border-radius:999px;width:24px;height:24px;cursor:pointer;font-size:1rem;line-height:1}.helper-text{color:var(--muted);line-height:1.6}
|
|
82
82
|
.duration-chip{display:inline-flex;align-items:center;padding:5px 9px;border-radius:999px;border:1px solid transparent;font-size:.8rem;font-weight:700;color:var(--text);white-space:nowrap}.duration-chip.vfast{background:rgba(34,197,94,.14);border-color:rgba(34,197,94,.28);color:#bbf7d0}.duration-chip.fast{background:rgba(56,189,248,.12);border-color:rgba(56,189,248,.24);color:#bae6fd}.duration-chip.slow{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#fde68a}.duration-chip.vslow{background:rgba(239,68,68,.14);border-color:rgba(239,68,68,.24);color:#fecaca}html[data-theme='light'] .duration-chip.vfast{color:#166534}html[data-theme='light'] .duration-chip.fast{color:#1d4ed8}html[data-theme='light'] .duration-chip.slow{color:#92400e}html[data-theme='light'] .duration-chip.vslow{color:#b91c1c}
|
|
83
|
-
.code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block code{font-family:inherit}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
|
|
83
|
+
.code-card{border-radius:16px;border:1px solid var(--code-border);background:var(--surface-soft);overflow:hidden}.code-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line)}.code-label{font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:800}.copy-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;width:38px;height:38px;border-radius:12px;border:1px solid var(--line);background:var(--surface-strong);color:var(--text);cursor:pointer;transition:border-color .16s ease,color .16s ease}.copy-button:hover{border-color:rgba(56,189,248,.35);color:var(--accent)}.copy-button[data-copied='true']{color:var(--success);border-color:rgba(34,197,94,.28)}.copy-button svg{width:16px;height:16px;display:block}.code-block{margin:0;padding:18px 20px;background:var(--code-bg);color:#dbeafe;border:0;overflow:auto;white-space:pre;line-height:1.72;font-family:var(--mono);font-size:.92rem}.code-block code{font-family:inherit}.html-preview-wrap{padding:14px;background:var(--surface-strong);border-top:1px solid var(--line)}.html-preview{display:block;width:100%;min-height:320px;border:1px solid var(--line);border-radius:14px;background:#fff}.tok-key{color:#93c5fd}.tok-string{color:#86efac}.tok-number{color:#f9a8d4}.tok-boolean{color:#facc15}.tok-null{color:#fb7185}.tok-punctuation{color:#94a3b8}.tok-sql-keyword{color:#f472b6;font-weight:700}.tok-sql-identifier{color:#93c5fd}.tok-sql-string{color:#86efac}.tok-sql-number{color:#facc15}.tok-sql-comment{color:#64748b;font-style:italic}html[data-theme='light'] .code-block{color:#0f172a}html[data-theme='light'] .tok-key{color:#1d4ed8}html[data-theme='light'] .tok-string{color:#15803d}html[data-theme='light'] .tok-number{color:#c026d3}html[data-theme='light'] .tok-boolean{color:#b45309}html[data-theme='light'] .tok-null{color:#dc2626}html[data-theme='light'] .tok-punctuation{color:#64748b}html[data-theme='light'] .tok-sql-keyword{color:#db2777}html[data-theme='light'] .tok-sql-identifier{color:#2563eb}html[data-theme='light'] .tok-sql-string{color:#15803d}html[data-theme='light'] .tok-sql-number{color:#b45309}html[data-theme='light'] .tok-sql-comment{color:#6b7280}
|
|
84
84
|
@media (max-width:1120px){.content-grid{grid-template-columns:1fr}}@media (max-width:920px){.layout{grid-template-columns:1fr}.sidebar{position:static;height:auto;border-right:none;border-bottom:1px solid var(--line);padding:20px 16px 18px}.brand-row{padding:0 0 16px}.sidebar-status{margin:0 0 16px}.sidebar-group{padding:0}.main{padding:20px}}@media (max-width:640px){.stats-grid{grid-template-columns:1fr}.detail-card{padding:18px}.toolbar,.section-head,.pagination,.activity-list,.monitoring-wrap{padding-left:18px;padding-right:18px}.table-wrap{padding:0 8px 10px}.brand-row{align-items:stretch;gap:14px;padding:0 0 14px}.brand{width:100%;align-items:flex-start}.brand-copy{min-width:0}.brand-copy h1{font-size:1.18rem;line-height:1.12}.brand-copy p{font-size:.82rem;overflow-wrap:anywhere}.icon-button{align-self:flex-end}.sidebar-status{padding:12px}.nav-button{padding:11px 12px}.nav-title{font-size:.95rem}.nav-meta{font-size:.72rem}}@media (max-width:480px){.brand-row{flex-direction:column}.icon-button{align-self:flex-start}.nav-button{align-items:flex-start;flex-direction:column}.nav-meta{font-size:.7rem}}
|
|
85
85
|
</style>
|
|
86
86
|
</head>
|
|
@@ -202,6 +202,8 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
202
202
|
.replace(/"/g, '"')
|
|
203
203
|
.replace(/'/g, ''');
|
|
204
204
|
|
|
205
|
+
const looksLikeHtml = (value) => new RegExp('</?(?:html|body|div|table)\\b|<!doctype\\b', 'i').test(String(value || ''));
|
|
206
|
+
|
|
205
207
|
const api = async (path, opts) => {
|
|
206
208
|
const response = await fetch(API + path, opts);
|
|
207
209
|
if (!response.ok) {
|
|
@@ -341,6 +343,28 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
341
343
|
].join('');
|
|
342
344
|
};
|
|
343
345
|
|
|
346
|
+
const renderTextCard = (label, value) => {
|
|
347
|
+
const source = String(value ?? '');
|
|
348
|
+
return renderCodeCard(label, source, escapeHtml(source), 'language-text');
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const renderHtmlPreview = (label, html) => {
|
|
352
|
+
const source = String(html ?? '');
|
|
353
|
+
const copyId = registerCopyPayload(source);
|
|
354
|
+
return [
|
|
355
|
+
'<section class="code-card">',
|
|
356
|
+
'<div class="code-toolbar">',
|
|
357
|
+
'<span class="code-label">' + escapeHtml(label) + '</span>',
|
|
358
|
+
'<button type="button" class="copy-button" data-action="copy-code" data-copy-id="' + escapeHtml(copyId) + '" title="Copy ' + escapeHtml(label) + '">',
|
|
359
|
+
COPY_ICON,
|
|
360
|
+
'</button>',
|
|
361
|
+
'</div>',
|
|
362
|
+
'<pre class="code-block language-html"><code>' + escapeHtml(source) + '</code></pre>',
|
|
363
|
+
'<div class="html-preview-wrap"><iframe class="html-preview" sandbox="allow-same-origin" srcdoc="' + escapeHtml(source) + '"></iframe></div>',
|
|
364
|
+
'</section>'
|
|
365
|
+
].join('');
|
|
366
|
+
};
|
|
367
|
+
|
|
344
368
|
const highlightJson = (value, label = 'JSON') => {
|
|
345
369
|
const source = prettyJson(value);
|
|
346
370
|
let output = '';
|
|
@@ -392,6 +416,14 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
392
416
|
|
|
393
417
|
const detailJson = (value, label = 'JSON') => highlightJson(value ?? {}, label);
|
|
394
418
|
|
|
419
|
+
const renderPayload = (label, value) => {
|
|
420
|
+
if (value === undefined) return '<p class="trace-note">No ' + escapeHtml(label.toLowerCase()) + ' was captured.</p>';
|
|
421
|
+
if (typeof value === 'string') {
|
|
422
|
+
return looksLikeHtml(value) ? renderHtmlPreview(label, value) : renderTextCard(label, value);
|
|
423
|
+
}
|
|
424
|
+
return detailJson(value, label);
|
|
425
|
+
};
|
|
426
|
+
|
|
395
427
|
const entrySummaryText = (entry) => {
|
|
396
428
|
const content = entry && entry.content ? entry.content : {};
|
|
397
429
|
if (entry.type === 'request') return [content.responseStatus || '', content.method || '', content.uri || ''].filter(Boolean).join(' ');
|
|
@@ -399,20 +431,20 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
399
431
|
if (entry.type === 'exception') return [content.class || '', content.message || ''].filter(Boolean).join(': ');
|
|
400
432
|
if (entry.type === 'log') return '[' + String(content.level || 'log') + '] ' + String(content.message || '').slice(0, 160);
|
|
401
433
|
if (entry.type === 'job') return [content.name || '', content.status || 'queued'].filter(Boolean).join(' · ');
|
|
402
|
-
if (entry.type === 'cache') return [content.operation || '', content.key || ''].filter(Boolean).join(' ');
|
|
434
|
+
if (entry.type === 'cache') return [content.operation || '', content.key || '', content.payloadLogged ? '' : '(payload off)'].filter(Boolean).join(' ');
|
|
403
435
|
if (entry.type === 'schedule') return [content.name || '', content.status || 'ran'].filter(Boolean).join(' · ');
|
|
404
436
|
if (entry.type === 'mail') return ['To ' + (content.to || 'unknown'), content.subject || 'No subject'].join(' · ');
|
|
405
437
|
if (entry.type === 'auth') return [content.event || 'auth', content.userId ? '#' + content.userId : ''].filter(Boolean).join(' ');
|
|
406
438
|
if (entry.type === 'event') return String(content.name || 'event');
|
|
407
439
|
if (entry.type === 'model') return [content.action || '', content.model || ''].filter(Boolean).join(' ');
|
|
408
|
-
if (entry.type === 'notification') return [content.notification || '', (content.channels || []).join(', ')].filter(Boolean).join(' -> ');
|
|
440
|
+
if (entry.type === 'notification') return [content.notification || '', content.message || (content.channels || []).join(', ')].filter(Boolean).join(' -> ');
|
|
409
441
|
if (entry.type === 'redis') return String(content.command || 'redis');
|
|
410
442
|
if (entry.type === 'gate') return [content.ability || '', content.result || ''].filter(Boolean).join(' · ');
|
|
411
443
|
if (entry.type === 'middleware') return [content.name || '', content.event || ''].filter(Boolean).join(' · ');
|
|
412
444
|
if (entry.type === 'command') return [content.name || '', content.exitCode !== undefined ? 'exit=' + content.exitCode : ''].filter(Boolean).join(' ');
|
|
413
445
|
if (entry.type === 'batch') return [content.name || '', 'processed ' + (content.processed || 0) + '/' + (content.total || 0)].join(' · ');
|
|
414
446
|
if (entry.type === 'view') return String(content.template || 'view');
|
|
415
|
-
if (entry.type === 'client_request') return [content.method || '', content.url || ''].filter(Boolean).join(' ');
|
|
447
|
+
if (entry.type === 'client_request') return [content.method || '', content.url || '', content.responseStatus ? '[' + content.responseStatus + ']' : content.error ? '[failed]' : ''].filter(Boolean).join(' ');
|
|
416
448
|
return JSON.stringify(content).slice(0, 160);
|
|
417
449
|
};
|
|
418
450
|
|
|
@@ -448,6 +480,7 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
448
480
|
{ label: 'Connection', value: escapeHtml(content.connection || 'default') },
|
|
449
481
|
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) },
|
|
450
482
|
{ label: 'Slow', value: escapeHtml(content.slow ? 'Yes' : 'No') },
|
|
483
|
+
{ label: 'Bindings', value: escapeHtml(content.bindingsIncluded === false ? 'Hidden' : 'Included') },
|
|
451
484
|
{ label: 'Hash', value: '<span class="mono">' + escapeHtml(content.hash || '') + '</span>' }
|
|
452
485
|
]),
|
|
453
486
|
renderMetricBox('Runtime', [
|
|
@@ -455,7 +488,8 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
455
488
|
{ label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' }
|
|
456
489
|
]),
|
|
457
490
|
'</div>',
|
|
458
|
-
highlightSql(content.sql || '')
|
|
491
|
+
highlightSql(content.sql || ''),
|
|
492
|
+
content.bindingsIncluded === false ? '<p class="trace-note">SQL bindings were hidden for this entry.</p>' : (Array.isArray(content.bindings) ? detailJson(content.bindings, 'Bindings Json') : '')
|
|
459
493
|
].join('');
|
|
460
494
|
}
|
|
461
495
|
|
|
@@ -499,7 +533,34 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
499
533
|
renderMetricBox('Request', [
|
|
500
534
|
{ label: 'Method', value: escapeHtml(content.method || '') },
|
|
501
535
|
{ label: 'URL', value: '<span class="mono">' + escapeHtml(content.url || '') + '</span>' },
|
|
502
|
-
{ label: 'Status', value: escapeHtml(content.responseStatus || '') },
|
|
536
|
+
{ label: 'Status', value: escapeHtml(content.responseStatus || (content.error ? 'Failed' : 'Pending')) },
|
|
537
|
+
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
|
|
538
|
+
]),
|
|
539
|
+
renderMetricBox('Runtime', [
|
|
540
|
+
{ label: 'Hostname', value: escapeHtml(content.hostname || '') },
|
|
541
|
+
{ label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' },
|
|
542
|
+
{ label: 'Error', value: escapeHtml(content.error || '-') }
|
|
543
|
+
]),
|
|
544
|
+
'</div>',
|
|
545
|
+
'<div class="detail-stack">',
|
|
546
|
+
detailJson(content.requestHeaders || {}, 'Request Header Json'),
|
|
547
|
+
renderPayload('Request Body', content.requestBody),
|
|
548
|
+
detailJson(content.responseHeaders || {}, 'Response Header Json'),
|
|
549
|
+
renderPayload('Response Body', content.responseBody),
|
|
550
|
+
'</div>'
|
|
551
|
+
].join('');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (entry.type === 'cache') {
|
|
555
|
+
return [
|
|
556
|
+
'<div class="detail-grid">',
|
|
557
|
+
renderMetricBox('Cache', [
|
|
558
|
+
{ label: 'Operation', value: escapeHtml(content.operation || '') },
|
|
559
|
+
{ label: 'Key', value: '<span class="mono">' + escapeHtml(content.key || '') + '</span>' },
|
|
560
|
+
{ label: 'Store', value: escapeHtml(content.store || 'default') },
|
|
561
|
+
{ label: 'Hit', value: escapeHtml(content.hit === undefined ? '-' : (content.hit ? 'Yes' : 'No')) },
|
|
562
|
+
{ label: 'Payload', value: escapeHtml(content.payloadLogged ? 'Captured' : 'Disabled') },
|
|
563
|
+
{ label: 'TTL', value: escapeHtml(content.ttl === undefined ? '-' : String(content.ttl)) },
|
|
503
564
|
{ label: 'Duration', value: escapeHtml(formatDuration(getEntryDuration(entry))) }
|
|
504
565
|
]),
|
|
505
566
|
renderMetricBox('Runtime', [
|
|
@@ -507,7 +568,41 @@ const DASHBOARD_DOCUMENT = `<!DOCTYPE html>
|
|
|
507
568
|
{ label: 'Batch', value: '<span class="mono">' + escapeHtml(entry.batchId || '-') + '</span>' }
|
|
508
569
|
]),
|
|
509
570
|
'</div>',
|
|
510
|
-
|
|
571
|
+
content.payloadLogged ? renderPayload('Cache Payload', content.payload) : '<p class="trace-note">Cache payload logging is disabled. Set TRACE_CACHE_PAYLOADS=true to include values.</p>'
|
|
572
|
+
].join('');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (entry.type === 'mail') {
|
|
576
|
+
return [
|
|
577
|
+
'<div class="detail-grid">',
|
|
578
|
+
renderMetricBox('Mail', [
|
|
579
|
+
{ label: 'To', value: escapeHtml(content.to || '') },
|
|
580
|
+
{ label: 'Subject', value: escapeHtml(content.subject || '') },
|
|
581
|
+
{ label: 'Template', value: escapeHtml(content.template || '-') },
|
|
582
|
+
{ label: 'Hostname', value: escapeHtml(content.hostname || '') }
|
|
583
|
+
]),
|
|
584
|
+
'</div>',
|
|
585
|
+
'<div class="detail-stack">',
|
|
586
|
+
renderPayload('Mail Text', content.text),
|
|
587
|
+
renderPayload('Mail Html', content.html),
|
|
588
|
+
'</div>'
|
|
589
|
+
].join('');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (entry.type === 'notification') {
|
|
593
|
+
return [
|
|
594
|
+
'<div class="detail-grid">',
|
|
595
|
+
renderMetricBox('Notification', [
|
|
596
|
+
{ label: 'Notification', value: escapeHtml(content.notification || '') },
|
|
597
|
+
{ label: 'Channels', value: escapeHtml((content.channels || []).join(', ') || '-') },
|
|
598
|
+
{ label: 'Recipient', value: escapeHtml(content.notifiable || '-') },
|
|
599
|
+
{ label: 'Hostname', value: escapeHtml(content.hostname || '') }
|
|
600
|
+
]),
|
|
601
|
+
'</div>',
|
|
602
|
+
'<div class="detail-stack">',
|
|
603
|
+
renderPayload('Message', content.message),
|
|
604
|
+
content.payload === undefined ? '<p class="trace-note">No additional notification payload was captured.</p>' : detailJson(content.payload, 'Notification Payload Json'),
|
|
605
|
+
'</div>'
|
|
511
606
|
].join('');
|
|
512
607
|
}
|
|
513
608
|
|
package/src/register.ts
CHANGED
|
@@ -141,6 +141,14 @@ const parseEnvList = (rawValue: string): string[] | undefined => {
|
|
|
141
141
|
.filter((entry) => entry !== '');
|
|
142
142
|
};
|
|
143
143
|
|
|
144
|
+
const parseEnvBool = (rawValue: string): boolean | undefined => {
|
|
145
|
+
const value = rawValue.trim().toLowerCase();
|
|
146
|
+
if (value === '') return undefined;
|
|
147
|
+
if (['1', 'true', 'yes', 'on'].includes(value)) return true;
|
|
148
|
+
if (['0', 'false', 'no', 'off'].includes(value)) return false;
|
|
149
|
+
return undefined;
|
|
150
|
+
};
|
|
151
|
+
|
|
144
152
|
const resolveTraceStartupOverrides = (core: CoreApi): TraceConfigOverrides | undefined => {
|
|
145
153
|
const traceConfigFile = core.StartupConfigFile?.Trace;
|
|
146
154
|
if (typeof traceConfigFile !== 'string' || traceConfigFile.trim() === '') return undefined;
|
|
@@ -201,6 +209,8 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
201
209
|
const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
|
|
202
210
|
const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
|
|
203
211
|
const logMinLevelRaw = Env.get('TRACE_LOG_LEVEL', '').trim();
|
|
212
|
+
const captureCachePayloadsRaw = Env.get('TRACE_CACHE_PAYLOADS', '').trim();
|
|
213
|
+
const captureQueryBindingsRaw = Env.get('TRACE_QUERY_BINDINGS', '').trim();
|
|
204
214
|
const redactionKeys = parseEnvList(Env.get('TRACE_REDACT_KEYS', ''));
|
|
205
215
|
const redactionHeaders = parseEnvList(Env.get('TRACE_REDACT_HEADERS', ''));
|
|
206
216
|
const redactionBody = parseEnvList(Env.get('TRACE_REDACT_BODY', ''));
|
|
@@ -221,6 +231,10 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
221
231
|
| 'warn'
|
|
222
232
|
| 'error'
|
|
223
233
|
| 'fatal';
|
|
234
|
+
const captureCachePayloads =
|
|
235
|
+
parseEnvBool(captureCachePayloadsRaw) ?? startupOverrides?.captureCachePayloads;
|
|
236
|
+
const captureQueryBindings =
|
|
237
|
+
parseEnvBool(captureQueryBindingsRaw) ?? startupOverrides?.captureQueryBindings;
|
|
224
238
|
const redaction = buildTraceRedactionOverrides({
|
|
225
239
|
startupOverrides,
|
|
226
240
|
redactionBody,
|
|
@@ -239,6 +253,8 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
239
253
|
...(typeof slowQueryThreshold === 'number' && Number.isFinite(slowQueryThreshold)
|
|
240
254
|
? { slowQueryThreshold }
|
|
241
255
|
: {}),
|
|
256
|
+
...(typeof captureCachePayloads === 'boolean' ? { captureCachePayloads } : {}),
|
|
257
|
+
...(typeof captureQueryBindings === 'boolean' ? { captureQueryBindings } : {}),
|
|
242
258
|
logMinLevel,
|
|
243
259
|
...(redaction === undefined ? {} : { redaction }),
|
|
244
260
|
});
|
package/src/types.ts
CHANGED
|
@@ -55,6 +55,9 @@ export interface RequestContent {
|
|
|
55
55
|
export interface QueryContent {
|
|
56
56
|
connection: string;
|
|
57
57
|
sql: string;
|
|
58
|
+
statement?: string;
|
|
59
|
+
bindings?: unknown[];
|
|
60
|
+
bindingsIncluded?: boolean;
|
|
58
61
|
time: number;
|
|
59
62
|
duration: number;
|
|
60
63
|
slow: boolean;
|
|
@@ -97,6 +100,10 @@ export interface CacheContent {
|
|
|
97
100
|
operation: 'get' | 'set' | 'delete' | 'clear' | 'has';
|
|
98
101
|
key: string;
|
|
99
102
|
hit?: boolean;
|
|
103
|
+
store?: string;
|
|
104
|
+
payload?: unknown;
|
|
105
|
+
payloadLogged?: boolean;
|
|
106
|
+
ttl?: number;
|
|
100
107
|
duration: number;
|
|
101
108
|
hostname: string;
|
|
102
109
|
}
|
|
@@ -114,6 +121,8 @@ export interface MailContent {
|
|
|
114
121
|
to: string;
|
|
115
122
|
subject: string;
|
|
116
123
|
template?: string;
|
|
124
|
+
text?: string;
|
|
125
|
+
html?: string;
|
|
117
126
|
hostname: string;
|
|
118
127
|
}
|
|
119
128
|
|
|
@@ -142,6 +151,8 @@ export interface NotificationContent {
|
|
|
142
151
|
channels: string[];
|
|
143
152
|
notifiable?: string;
|
|
144
153
|
notification: string;
|
|
154
|
+
message?: string;
|
|
155
|
+
payload?: unknown;
|
|
145
156
|
hostname: string;
|
|
146
157
|
}
|
|
147
158
|
|
|
@@ -201,11 +212,27 @@ export interface ClientRequestContent {
|
|
|
201
212
|
method: string;
|
|
202
213
|
url: string;
|
|
203
214
|
requestHeaders: Record<string, string>;
|
|
204
|
-
|
|
215
|
+
requestBody?: unknown;
|
|
216
|
+
responseStatus?: number;
|
|
217
|
+
responseHeaders?: Record<string, string>;
|
|
218
|
+
responseBody?: unknown;
|
|
219
|
+
error?: string;
|
|
205
220
|
duration: number;
|
|
206
221
|
hostname: string;
|
|
207
222
|
}
|
|
208
223
|
|
|
224
|
+
export interface ClientRequestTraceInput {
|
|
225
|
+
method: string;
|
|
226
|
+
url: string;
|
|
227
|
+
requestHeaders: Record<string, string>;
|
|
228
|
+
responseStatus?: number;
|
|
229
|
+
duration: number;
|
|
230
|
+
requestBody?: unknown;
|
|
231
|
+
responseHeaders?: Record<string, string>;
|
|
232
|
+
responseBody?: unknown;
|
|
233
|
+
error?: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
209
236
|
// ---------------------------------------------------------------------------
|
|
210
237
|
// Core domain records
|
|
211
238
|
// ---------------------------------------------------------------------------
|
|
@@ -329,6 +356,8 @@ export interface ITraceConfig {
|
|
|
329
356
|
pruneAfterHours: number;
|
|
330
357
|
ignoreRoutes: string[];
|
|
331
358
|
slowQueryThreshold: number;
|
|
359
|
+
captureCachePayloads: boolean;
|
|
360
|
+
captureQueryBindings: boolean;
|
|
332
361
|
logMinLevel: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
333
362
|
watchers: WatcherToggles;
|
|
334
363
|
redaction: RedactionConfig;
|
|
@@ -6,10 +6,11 @@ import { TraceContext } from '../context';
|
|
|
6
6
|
import type { CacheContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
|
|
7
7
|
import { EntryType } from '../types';
|
|
8
8
|
import { AuthTag } from '../utils/authTag';
|
|
9
|
-
import { redactString } from '../utils/redact';
|
|
9
|
+
import { redactString, redactUnknown } from '../utils/redact';
|
|
10
10
|
import { RequestFilter } from '../utils/requestFilter';
|
|
11
11
|
|
|
12
12
|
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
13
|
+
let _config: ITraceWatcherConfig['config'] | null = null;
|
|
13
14
|
let _redactionFields: string[] = [];
|
|
14
15
|
let _ignoreRoutes: string[] = [];
|
|
15
16
|
|
|
@@ -17,15 +18,23 @@ const emit = (
|
|
|
17
18
|
operation: CacheContent['operation'],
|
|
18
19
|
key: string,
|
|
19
20
|
duration: number,
|
|
20
|
-
hit?: boolean
|
|
21
|
+
hit?: boolean,
|
|
22
|
+
payload?: unknown,
|
|
23
|
+
store?: string,
|
|
24
|
+
ttl?: number
|
|
21
25
|
): void => {
|
|
22
26
|
if (!_storage) return;
|
|
23
27
|
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
24
28
|
const safeKey = redactString(key, _redactionFields);
|
|
29
|
+
const shouldLogPayload = _config?.captureCachePayloads === true;
|
|
25
30
|
const content: CacheContent = {
|
|
26
31
|
operation,
|
|
27
32
|
key: safeKey,
|
|
28
33
|
hit,
|
|
34
|
+
...(typeof store === 'string' && store !== '' ? { store } : {}),
|
|
35
|
+
...(typeof ttl === 'number' ? { ttl } : {}),
|
|
36
|
+
payloadLogged: shouldLogPayload,
|
|
37
|
+
...(shouldLogPayload ? { payload: redactUnknown(payload, _redactionFields) } : {}),
|
|
29
38
|
duration,
|
|
30
39
|
hostname: TraceContext.getHostname(),
|
|
31
40
|
};
|
|
@@ -48,10 +57,12 @@ export const CacheWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze
|
|
|
48
57
|
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
49
58
|
if (config.watchers.cache === false) return () => undefined;
|
|
50
59
|
_storage = storage;
|
|
60
|
+
_config = config;
|
|
51
61
|
_redactionFields = config.redaction.query;
|
|
52
62
|
_ignoreRoutes = config.ignoreRoutes;
|
|
53
63
|
return () => {
|
|
54
64
|
_storage = null;
|
|
65
|
+
_config = null;
|
|
55
66
|
_ignoreRoutes = [];
|
|
56
67
|
};
|
|
57
68
|
},
|
|
@@ -1,30 +1,50 @@
|
|
|
1
1
|
import { TraceContext } from '../context';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ClientRequestContent,
|
|
4
|
+
ClientRequestTraceInput,
|
|
5
|
+
ITraceWatcher,
|
|
6
|
+
ITraceWatcherConfig,
|
|
7
|
+
} from '../types';
|
|
3
8
|
import { EntryType } from '../types';
|
|
4
9
|
import { AuthTag } from '../utils/authTag';
|
|
5
|
-
import { redactHeaders } from '../utils/redact';
|
|
10
|
+
import { redactHeaders, redactUnknown } from '../utils/redact';
|
|
6
11
|
import { RequestFilter } from '../utils/requestFilter';
|
|
7
12
|
|
|
8
13
|
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
9
14
|
let _redactHeaderNames: string[] = [];
|
|
15
|
+
let _redactBodyFields: string[] = [];
|
|
10
16
|
let _ignoreRoutes: string[] = [];
|
|
11
17
|
|
|
12
|
-
const emit = (
|
|
13
|
-
method
|
|
14
|
-
url
|
|
15
|
-
requestHeaders
|
|
16
|
-
responseStatus
|
|
17
|
-
duration
|
|
18
|
-
|
|
18
|
+
const emit = ({
|
|
19
|
+
method,
|
|
20
|
+
url,
|
|
21
|
+
requestHeaders,
|
|
22
|
+
responseStatus,
|
|
23
|
+
duration,
|
|
24
|
+
requestBody,
|
|
25
|
+
responseHeaders,
|
|
26
|
+
responseBody,
|
|
27
|
+
error,
|
|
28
|
+
}: ClientRequestTraceInput): void => {
|
|
19
29
|
if (!_storage) return;
|
|
20
30
|
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
21
31
|
const tags = AuthTag.append([method.toUpperCase()]);
|
|
22
|
-
if (responseStatus >= 400) tags.push('failed');
|
|
32
|
+
if ((responseStatus ?? 0) >= 400 || error) tags.push('failed');
|
|
23
33
|
const content: ClientRequestContent = {
|
|
24
34
|
method: method.toUpperCase(),
|
|
25
35
|
url,
|
|
26
36
|
requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames),
|
|
27
|
-
|
|
37
|
+
...(requestBody === undefined
|
|
38
|
+
? {}
|
|
39
|
+
: { requestBody: redactUnknown(requestBody, _redactBodyFields) }),
|
|
40
|
+
...(responseStatus === undefined ? {} : { responseStatus }),
|
|
41
|
+
...(responseHeaders === undefined
|
|
42
|
+
? {}
|
|
43
|
+
: { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) }),
|
|
44
|
+
...(responseBody === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { responseBody: redactUnknown(responseBody, _redactBodyFields) }),
|
|
47
|
+
...(typeof error === 'string' && error !== '' ? { error } : {}),
|
|
28
48
|
duration,
|
|
29
49
|
hostname: TraceContext.getHostname(),
|
|
30
50
|
};
|
|
@@ -47,9 +67,11 @@ export const HttpClientWatcher: ITraceWatcher & { emit: typeof emit } = Object.f
|
|
|
47
67
|
if (config.watchers.clientRequest === false) return () => undefined;
|
|
48
68
|
_storage = storage;
|
|
49
69
|
_redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
|
|
70
|
+
_redactBodyFields = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
|
|
50
71
|
_ignoreRoutes = config.ignoreRoutes;
|
|
51
72
|
return () => {
|
|
52
73
|
_storage = null;
|
|
74
|
+
_redactBodyFields = [];
|
|
53
75
|
_ignoreRoutes = [];
|
|
54
76
|
};
|
|
55
77
|
},
|
|
@@ -16,6 +16,15 @@ const LEVEL_PRIORITY: Record<string, number> = {
|
|
|
16
16
|
fatal: 4,
|
|
17
17
|
};
|
|
18
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
|
+
|
|
19
28
|
export const LogWatcher: ITraceWatcher = Object.freeze({
|
|
20
29
|
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
21
30
|
if (config.watchers.log === false) return () => undefined;
|
|
@@ -32,6 +41,7 @@ export const LogWatcher: ITraceWatcher = Object.freeze({
|
|
|
32
41
|
(level: string, message: string, context?: Record<string, unknown>) => {
|
|
33
42
|
if ((LEVEL_PRIORITY[level] ?? 0) < minPriority) return;
|
|
34
43
|
if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes)) return;
|
|
44
|
+
if (shouldSkipTraceInfrastructureLog(message)) return;
|
|
35
45
|
|
|
36
46
|
const content: LogContent = {
|
|
37
47
|
level,
|