@zintrust/trace 0.4.81 → 0.4.83
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 +6 -0
- package/dist/build-manifest.json +45 -45
- package/dist/config.js +3 -0
- package/dist/dashboard/ui.js +103 -8
- package/dist/register.js +36 -4
- package/dist/types.d.ts +30 -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/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.d.ts +5 -1
- package/dist/watchers/QueryWatcher.js +49 -33
- package/package.json +3 -3
- package/src/config.ts +3 -0
- package/src/dashboard/ui.ts +103 -8
- package/src/register.ts +51 -5
- package/src/types.ts +31 -1
- package/src/watchers/CacheWatcher.ts +13 -2
- package/src/watchers/HttpClientWatcher.ts +33 -11
- package/src/watchers/MailWatcher.ts +18 -3
- package/src/watchers/NotificationWatcher.ts +15 -1
- package/src/watchers/QueryWatcher.ts +53 -35
|
@@ -5,21 +5,27 @@
|
|
|
5
5
|
import { TraceContext } from '../context.js';
|
|
6
6
|
import { EntryType } from '../types.js';
|
|
7
7
|
import { AuthTag } from '../utils/authTag.js';
|
|
8
|
-
import { redactString } from '../utils/redact.js';
|
|
8
|
+
import { redactString, redactUnknown } from '../utils/redact.js';
|
|
9
9
|
import { RequestFilter } from '../utils/requestFilter.js';
|
|
10
10
|
let _storage = null;
|
|
11
|
+
let _config = null;
|
|
11
12
|
let _redactionFields = [];
|
|
12
13
|
let _ignoreRoutes = [];
|
|
13
|
-
const emit = (operation, key, duration, hit) => {
|
|
14
|
+
const emit = (operation, key, duration, hit, payload, store, ttl) => {
|
|
14
15
|
if (!_storage)
|
|
15
16
|
return;
|
|
16
17
|
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes))
|
|
17
18
|
return;
|
|
18
19
|
const safeKey = redactString(key, _redactionFields);
|
|
20
|
+
const shouldLogPayload = _config?.captureCachePayloads === true;
|
|
19
21
|
const content = {
|
|
20
22
|
operation,
|
|
21
23
|
key: safeKey,
|
|
22
24
|
hit,
|
|
25
|
+
...(typeof store === 'string' && store !== '' ? { store } : {}),
|
|
26
|
+
...(typeof ttl === 'number' ? { ttl } : {}),
|
|
27
|
+
payloadLogged: shouldLogPayload,
|
|
28
|
+
...(shouldLogPayload ? { payload: redactUnknown(payload, _redactionFields) } : {}),
|
|
23
29
|
duration,
|
|
24
30
|
hostname: TraceContext.getHostname(),
|
|
25
31
|
};
|
|
@@ -41,10 +47,12 @@ export const CacheWatcher = Object.freeze({
|
|
|
41
47
|
if (config.watchers.cache === false)
|
|
42
48
|
return () => undefined;
|
|
43
49
|
_storage = storage;
|
|
50
|
+
_config = config;
|
|
44
51
|
_redactionFields = config.redaction.query;
|
|
45
52
|
_ignoreRoutes = config.ignoreRoutes;
|
|
46
53
|
return () => {
|
|
47
54
|
_storage = null;
|
|
55
|
+
_config = null;
|
|
48
56
|
_ignoreRoutes = [];
|
|
49
57
|
};
|
|
50
58
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ITraceWatcher } from '../types';
|
|
2
|
-
declare const emit: (method
|
|
1
|
+
import type { ClientRequestTraceInput, ITraceWatcher } from '../types';
|
|
2
|
+
declare const emit: ({ method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }: ClientRequestTraceInput) => void;
|
|
3
3
|
export declare const HttpClientWatcher: ITraceWatcher & {
|
|
4
4
|
emit: typeof emit;
|
|
5
5
|
};
|
|
@@ -1,24 +1,35 @@
|
|
|
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 } from '../utils/redact.js';
|
|
4
|
+
import { redactHeaders, redactUnknown } from '../utils/redact.js';
|
|
5
5
|
import { RequestFilter } from '../utils/requestFilter.js';
|
|
6
6
|
let _storage = null;
|
|
7
7
|
let _redactHeaderNames = [];
|
|
8
|
+
let _redactBodyFields = [];
|
|
8
9
|
let _ignoreRoutes = [];
|
|
9
|
-
const emit = (method, url, requestHeaders, responseStatus, duration) => {
|
|
10
|
+
const emit = ({ method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }) => {
|
|
10
11
|
if (!_storage)
|
|
11
12
|
return;
|
|
12
13
|
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes))
|
|
13
14
|
return;
|
|
14
15
|
const tags = AuthTag.append([method.toUpperCase()]);
|
|
15
|
-
if (responseStatus >= 400)
|
|
16
|
+
if ((responseStatus ?? 0) >= 400 || error)
|
|
16
17
|
tags.push('failed');
|
|
17
18
|
const content = {
|
|
18
19
|
method: method.toUpperCase(),
|
|
19
20
|
url,
|
|
20
21
|
requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames),
|
|
21
|
-
|
|
22
|
+
...(requestBody === undefined
|
|
23
|
+
? {}
|
|
24
|
+
: { requestBody: redactUnknown(requestBody, _redactBodyFields) }),
|
|
25
|
+
...(responseStatus === undefined ? {} : { responseStatus }),
|
|
26
|
+
...(responseHeaders === undefined
|
|
27
|
+
? {}
|
|
28
|
+
: { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) }),
|
|
29
|
+
...(responseBody === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: { responseBody: redactUnknown(responseBody, _redactBodyFields) }),
|
|
32
|
+
...(typeof error === 'string' && error !== '' ? { error } : {}),
|
|
22
33
|
duration,
|
|
23
34
|
hostname: TraceContext.getHostname(),
|
|
24
35
|
};
|
|
@@ -41,9 +52,11 @@ export const HttpClientWatcher = Object.freeze({
|
|
|
41
52
|
return () => undefined;
|
|
42
53
|
_storage = storage;
|
|
43
54
|
_redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
|
|
55
|
+
_redactBodyFields = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
|
|
44
56
|
_ignoreRoutes = config.ignoreRoutes;
|
|
45
57
|
return () => {
|
|
46
58
|
_storage = null;
|
|
59
|
+
_redactBodyFields = [];
|
|
47
60
|
_ignoreRoutes = [];
|
|
48
61
|
};
|
|
49
62
|
},
|
|
@@ -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
|
},
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { ITraceWatcher } from '../types';
|
|
2
|
-
|
|
2
|
+
declare const emit: (query: string, params: unknown[], duration: number, connection?: string) => void;
|
|
3
|
+
export declare const QueryWatcher: ITraceWatcher & {
|
|
4
|
+
emit: typeof emit;
|
|
5
|
+
};
|
|
6
|
+
export {};
|
|
@@ -6,6 +6,8 @@ import { TraceStorage } from '../storage/index.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
|
+
let _storage = null;
|
|
10
|
+
let _config = null;
|
|
9
11
|
const bindingsInterpolated = (sql, params) => {
|
|
10
12
|
// Inline params for display only — safe, not for re-execution.
|
|
11
13
|
let i = 0;
|
|
@@ -22,50 +24,64 @@ const isTraceStorageQuery = (sql) => {
|
|
|
22
24
|
const normalized = sql.toLowerCase();
|
|
23
25
|
return normalized.includes('zin_trace_entries') || normalized.includes('zin_trace_monitoring');
|
|
24
26
|
};
|
|
27
|
+
const emit = (query, params, duration, connection = 'default') => {
|
|
28
|
+
if (_storage === null || _config === null)
|
|
29
|
+
return;
|
|
30
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_config.ignoreRoutes))
|
|
31
|
+
return;
|
|
32
|
+
if (isTraceStorageQuery(query))
|
|
33
|
+
return;
|
|
34
|
+
const batchId = TraceContext.getBatchId();
|
|
35
|
+
const includeBindings = _config.captureQueryBindings !== false;
|
|
36
|
+
const sql = includeBindings ? bindingsInterpolated(query, params) : query;
|
|
37
|
+
const roundedDuration = Math.round(duration * 100) / 100;
|
|
38
|
+
const hash = TraceStorage.familyHash(query);
|
|
39
|
+
const slow = roundedDuration >= _config.slowQueryThreshold;
|
|
40
|
+
const content = {
|
|
41
|
+
connection,
|
|
42
|
+
sql,
|
|
43
|
+
statement: query,
|
|
44
|
+
...(includeBindings ? { bindings: [...params] } : {}),
|
|
45
|
+
bindingsIncluded: includeBindings,
|
|
46
|
+
time: roundedDuration,
|
|
47
|
+
duration: roundedDuration,
|
|
48
|
+
slow,
|
|
49
|
+
hash,
|
|
50
|
+
hostname: TraceContext.getHostname(),
|
|
51
|
+
};
|
|
52
|
+
const tags = AuthTag.append([]);
|
|
53
|
+
if (slow)
|
|
54
|
+
tags.push('slow');
|
|
55
|
+
_storage
|
|
56
|
+
.writeEntry({
|
|
57
|
+
uuid: crypto.randomUUID(),
|
|
58
|
+
batchId,
|
|
59
|
+
familyHash: hash,
|
|
60
|
+
type: EntryType.QUERY,
|
|
61
|
+
content,
|
|
62
|
+
tags,
|
|
63
|
+
isLatest: true,
|
|
64
|
+
createdAt: TraceContext.now(),
|
|
65
|
+
})
|
|
66
|
+
.catch(() => undefined);
|
|
67
|
+
};
|
|
25
68
|
export const QueryWatcher = Object.freeze({
|
|
69
|
+
emit,
|
|
26
70
|
register({ storage, config, db: injectedDb }) {
|
|
27
71
|
if (config.watchers.query === false)
|
|
28
72
|
return () => undefined;
|
|
29
73
|
if (!injectedDb)
|
|
30
74
|
return () => undefined; // no db available
|
|
75
|
+
_storage = storage;
|
|
76
|
+
_config = config;
|
|
31
77
|
const db = injectedDb;
|
|
32
78
|
const handler = (query, params, duration) => {
|
|
33
|
-
|
|
34
|
-
return;
|
|
35
|
-
if (isTraceStorageQuery(query))
|
|
36
|
-
return;
|
|
37
|
-
const batchId = TraceContext.getBatchId();
|
|
38
|
-
const sql = bindingsInterpolated(query, params);
|
|
39
|
-
const roundedDuration = Math.round(duration * 100) / 100;
|
|
40
|
-
const hash = TraceStorage.familyHash(query);
|
|
41
|
-
const slow = roundedDuration >= config.slowQueryThreshold;
|
|
42
|
-
const content = {
|
|
43
|
-
connection: 'default',
|
|
44
|
-
sql,
|
|
45
|
-
time: roundedDuration,
|
|
46
|
-
duration: roundedDuration,
|
|
47
|
-
slow,
|
|
48
|
-
hash,
|
|
49
|
-
hostname: TraceContext.getHostname(),
|
|
50
|
-
};
|
|
51
|
-
const tags = AuthTag.append([]);
|
|
52
|
-
if (slow)
|
|
53
|
-
tags.push('slow');
|
|
54
|
-
storage
|
|
55
|
-
.writeEntry({
|
|
56
|
-
uuid: crypto.randomUUID(),
|
|
57
|
-
batchId,
|
|
58
|
-
familyHash: hash,
|
|
59
|
-
type: EntryType.QUERY,
|
|
60
|
-
content,
|
|
61
|
-
tags,
|
|
62
|
-
isLatest: true,
|
|
63
|
-
createdAt: TraceContext.now(),
|
|
64
|
-
})
|
|
65
|
-
.catch(() => undefined);
|
|
79
|
+
emit(query, params, duration);
|
|
66
80
|
};
|
|
67
81
|
db.onAfterQuery?.(handler);
|
|
68
82
|
return () => {
|
|
83
|
+
_storage = null;
|
|
84
|
+
_config = null;
|
|
69
85
|
db.offAfterQuery?.(handler);
|
|
70
86
|
};
|
|
71
87
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.83",
|
|
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
|
@@ -106,9 +106,12 @@ const mergeWatchers = (
|
|
|
106
106
|
const DEFAULTS: ITraceConfig = Object.freeze({
|
|
107
107
|
enabled: false,
|
|
108
108
|
connection: undefined,
|
|
109
|
+
observeConnection: undefined,
|
|
109
110
|
pruneAfterHours: 24,
|
|
110
111
|
ignoreRoutes: ['/trace', '/health', '/ping'],
|
|
111
112
|
slowQueryThreshold: 100,
|
|
113
|
+
captureCachePayloads: false,
|
|
114
|
+
captureQueryBindings: true,
|
|
112
115
|
logMinLevel: 'info',
|
|
113
116
|
watchers: {},
|
|
114
117
|
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
|
|