@zintrust/trace 0.4.95 → 0.5.0
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 +23 -23
- package/dist/config.js +83 -6
- package/dist/dashboard/handlers.js +107 -4
- package/dist/dashboard/ui.js +4 -0
- package/dist/storage/TraceContentBudget.js +67 -15
- package/dist/types.d.ts +13 -1
- package/dist/utils/entryFilter.js +20 -0
- package/dist/watchers/HttpClientWatcher.d.ts +2 -2
- package/dist/watchers/HttpClientWatcher.js +118 -20
- package/package.json +2 -2
- package/src/config.ts +136 -6
- package/src/dashboard/handlers.ts +134 -5
- package/src/dashboard/ui.ts +4 -0
- package/src/storage/TraceContentBudget.ts +98 -17
- package/src/types.ts +15 -1
- package/src/utils/entryFilter.ts +23 -0
- package/src/watchers/HttpClientWatcher.ts +165 -26
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { generateUuid } from '@zintrust/core';
|
|
1
2
|
import { TraceContext } from '../context.js';
|
|
2
|
-
import { EntryType } from '../types.js';
|
|
3
|
+
import { EntryType, } from '../types.js';
|
|
3
4
|
import { AuthTag } from '../utils/authTag.js';
|
|
4
5
|
import { redactHeaders, redactUnknown } from '../utils/redact.js';
|
|
5
6
|
import { RequestFilter } from '../utils/requestFilter.js';
|
|
@@ -7,35 +8,127 @@ let _storage = null;
|
|
|
7
8
|
let _redactHeaderNames = [];
|
|
8
9
|
let _redactBodyFields = [];
|
|
9
10
|
let _ignoreRoutes = [];
|
|
10
|
-
|
|
11
|
+
let _clientRequestWatcher;
|
|
12
|
+
const isObjectValue = (value) => {
|
|
13
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
+
};
|
|
15
|
+
const resolveSource = (value) => {
|
|
16
|
+
if (typeof value !== 'string')
|
|
17
|
+
return undefined;
|
|
18
|
+
const normalized = value.trim().toLowerCase();
|
|
19
|
+
return normalized === '' ? undefined : normalized;
|
|
20
|
+
};
|
|
21
|
+
const resolveSourceRule = (source) => {
|
|
22
|
+
if (source === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
return _clientRequestWatcher?.sources?.[source];
|
|
25
|
+
};
|
|
26
|
+
const shouldCaptureField = (field, sourceRule) => {
|
|
27
|
+
const scoped = sourceRule?.[field];
|
|
28
|
+
if (typeof scoped === 'boolean')
|
|
29
|
+
return scoped;
|
|
30
|
+
const global = _clientRequestWatcher?.[field];
|
|
31
|
+
if (typeof global === 'boolean')
|
|
32
|
+
return global;
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
const buildRequestHeaders = (requestHeaders, sourceRule) => {
|
|
36
|
+
return shouldCaptureField('requestHeaders', sourceRule)
|
|
37
|
+
? { requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames) }
|
|
38
|
+
: { requestHeaders: {} };
|
|
39
|
+
};
|
|
40
|
+
const buildRequestBody = (requestBody, sourceRule) => {
|
|
41
|
+
if (requestBody === undefined)
|
|
42
|
+
return {};
|
|
43
|
+
if (!shouldCaptureField('requestBody', sourceRule))
|
|
44
|
+
return {};
|
|
45
|
+
return { requestBody: redactUnknown(requestBody, _redactBodyFields) };
|
|
46
|
+
};
|
|
47
|
+
const buildResponseHeaders = (responseHeaders, sourceRule) => {
|
|
48
|
+
if (responseHeaders === undefined)
|
|
49
|
+
return {};
|
|
50
|
+
if (!shouldCaptureField('responseHeaders', sourceRule))
|
|
51
|
+
return {};
|
|
52
|
+
return { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) };
|
|
53
|
+
};
|
|
54
|
+
const buildResponseBody = (responseBody, sourceRule) => {
|
|
55
|
+
if (responseBody === undefined)
|
|
56
|
+
return {};
|
|
57
|
+
if (!shouldCaptureField('responseBody', sourceRule))
|
|
58
|
+
return {};
|
|
59
|
+
return { responseBody: redactUnknown(responseBody, _redactBodyFields) };
|
|
60
|
+
};
|
|
61
|
+
const applySource = (content, normalizedSource) => {
|
|
62
|
+
if (normalizedSource !== undefined) {
|
|
63
|
+
content.source = normalizedSource;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const applyResponseStatus = (content, responseStatus) => {
|
|
67
|
+
if (responseStatus !== undefined) {
|
|
68
|
+
content.responseStatus = responseStatus;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const applyError = (content, error) => {
|
|
72
|
+
if (typeof error === 'string' && error !== '') {
|
|
73
|
+
content.error = error;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const mergePartialContent = (content, partial) => {
|
|
77
|
+
Object.assign(content, partial);
|
|
78
|
+
};
|
|
79
|
+
const buildClientRequestContent = (input, sourceRule, normalizedSource) => {
|
|
80
|
+
const content = {
|
|
81
|
+
method: input.method.toUpperCase(),
|
|
82
|
+
url: input.url,
|
|
83
|
+
requestHeaders: {},
|
|
84
|
+
duration: input.duration,
|
|
85
|
+
hostname: TraceContext.getHostname(),
|
|
86
|
+
};
|
|
87
|
+
applySource(content, normalizedSource);
|
|
88
|
+
mergePartialContent(content, buildRequestHeaders(input.requestHeaders, sourceRule));
|
|
89
|
+
mergePartialContent(content, buildRequestBody(input.requestBody, sourceRule));
|
|
90
|
+
applyResponseStatus(content, input.responseStatus);
|
|
91
|
+
mergePartialContent(content, buildResponseHeaders(input.responseHeaders, sourceRule));
|
|
92
|
+
mergePartialContent(content, buildResponseBody(input.responseBody, sourceRule));
|
|
93
|
+
applyError(content, input.error);
|
|
94
|
+
return content;
|
|
95
|
+
};
|
|
96
|
+
const isWatcherEnabled = (value) => {
|
|
97
|
+
if (value === false)
|
|
98
|
+
return false;
|
|
99
|
+
if (isObjectValue(value) && value.enabled === false)
|
|
100
|
+
return false;
|
|
101
|
+
return true;
|
|
102
|
+
};
|
|
103
|
+
const emit = ({ source, method, url, requestHeaders, responseStatus, duration, requestBody, responseHeaders, responseBody, error, }) => {
|
|
11
104
|
if (!_storage)
|
|
12
105
|
return;
|
|
13
106
|
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes))
|
|
14
107
|
return;
|
|
108
|
+
const normalizedSource = resolveSource(source);
|
|
109
|
+
const sourceRule = resolveSourceRule(normalizedSource);
|
|
110
|
+
if (sourceRule?.enabled === false)
|
|
111
|
+
return;
|
|
15
112
|
const tags = AuthTag.append([method.toUpperCase()]);
|
|
16
113
|
if ((responseStatus ?? 0) >= 400 || error)
|
|
17
114
|
tags.push('failed');
|
|
18
|
-
|
|
19
|
-
|
|
115
|
+
if (normalizedSource !== undefined)
|
|
116
|
+
tags.push(normalizedSource);
|
|
117
|
+
const content = buildClientRequestContent({
|
|
118
|
+
source,
|
|
119
|
+
method,
|
|
20
120
|
url,
|
|
21
|
-
requestHeaders
|
|
22
|
-
|
|
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 } : {}),
|
|
121
|
+
requestHeaders,
|
|
122
|
+
responseStatus,
|
|
33
123
|
duration,
|
|
34
|
-
|
|
35
|
-
|
|
124
|
+
requestBody,
|
|
125
|
+
responseHeaders,
|
|
126
|
+
responseBody,
|
|
127
|
+
error,
|
|
128
|
+
}, sourceRule, normalizedSource);
|
|
36
129
|
_storage
|
|
37
130
|
.writeEntry({
|
|
38
|
-
uuid:
|
|
131
|
+
uuid: generateUuid(),
|
|
39
132
|
batchId: TraceContext.getBatchId(),
|
|
40
133
|
type: EntryType.CLIENT_REQUEST,
|
|
41
134
|
content,
|
|
@@ -48,14 +141,19 @@ const emit = ({ method, url, requestHeaders, responseStatus, duration, requestBo
|
|
|
48
141
|
export const HttpClientWatcher = Object.freeze({
|
|
49
142
|
emit,
|
|
50
143
|
register({ storage, config }) {
|
|
51
|
-
if (config.watchers.clientRequest
|
|
144
|
+
if (!isWatcherEnabled(config.watchers.clientRequest))
|
|
52
145
|
return () => undefined;
|
|
53
146
|
_storage = storage;
|
|
147
|
+
_clientRequestWatcher =
|
|
148
|
+
typeof config.watchers.clientRequest === 'object' && config.watchers.clientRequest !== null
|
|
149
|
+
? config.watchers.clientRequest
|
|
150
|
+
: undefined;
|
|
54
151
|
_redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
|
|
55
152
|
_redactBodyFields = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
|
|
56
153
|
_ignoreRoutes = config.ignoreRoutes;
|
|
57
154
|
return () => {
|
|
58
155
|
_storage = null;
|
|
156
|
+
_clientRequestWatcher = undefined;
|
|
59
157
|
_redactBodyFields = [];
|
|
60
158
|
_ignoreRoutes = [];
|
|
61
159
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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.101"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
package/src/config.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type {
|
|
5
5
|
ITraceConfig,
|
|
6
|
+
TraceClientRequestCaptureRule,
|
|
7
|
+
TraceClientRequestWatcherToggle,
|
|
6
8
|
TraceConfigOverrides,
|
|
7
9
|
TraceFilterRule,
|
|
8
10
|
TraceRequestWatcherConfig,
|
|
@@ -25,19 +27,44 @@ const isObjectValue = (value: unknown): value is Record<string, unknown> => {
|
|
|
25
27
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
const resolveEnabled = (
|
|
31
|
+
base?: TraceFilterRule,
|
|
32
|
+
override?: TraceFilterRule
|
|
33
|
+
): boolean | undefined => {
|
|
34
|
+
return override?.enabled ?? base?.enabled;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const hasMergedRuleValues = (
|
|
38
|
+
include: string[],
|
|
39
|
+
exclude: string[],
|
|
40
|
+
enabled: boolean | undefined
|
|
41
|
+
): boolean => {
|
|
42
|
+
return include.length > 0 || exclude.length > 0 || enabled !== undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const buildFilterRule = (input: {
|
|
46
|
+
include: string[];
|
|
47
|
+
exclude: string[];
|
|
48
|
+
enabled: boolean | undefined;
|
|
49
|
+
}): TraceFilterRule => {
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
...(input.enabled === undefined ? {} : { enabled: input.enabled }),
|
|
52
|
+
...(input.include.length > 0 ? { include: input.include } : {}),
|
|
53
|
+
...(input.exclude.length > 0 ? { exclude: input.exclude } : {}),
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
28
57
|
const mergeFilterRule = (
|
|
29
58
|
base?: TraceFilterRule,
|
|
30
59
|
override?: TraceFilterRule
|
|
31
60
|
): TraceFilterRule | undefined => {
|
|
32
61
|
const include = mergeStringLists(base?.include ?? [], override?.include);
|
|
33
62
|
const exclude = mergeStringLists(base?.exclude ?? [], override?.exclude);
|
|
63
|
+
const enabled = resolveEnabled(base, override);
|
|
34
64
|
|
|
35
|
-
if (include
|
|
65
|
+
if (!hasMergedRuleValues(include, exclude, enabled)) return undefined;
|
|
36
66
|
|
|
37
|
-
return
|
|
38
|
-
...(include.length > 0 ? { include } : {}),
|
|
39
|
-
...(exclude.length > 0 ? { exclude } : {}),
|
|
40
|
-
});
|
|
67
|
+
return buildFilterRule({ include, exclude, enabled });
|
|
41
68
|
};
|
|
42
69
|
|
|
43
70
|
const mergeWatcherToggle = (
|
|
@@ -51,6 +78,109 @@ const mergeWatcherToggle = (
|
|
|
51
78
|
return mergeFilterRule(baseRule, override);
|
|
52
79
|
};
|
|
53
80
|
|
|
81
|
+
type ClientRequestCaptureFlags = Pick<
|
|
82
|
+
TraceClientRequestCaptureRule,
|
|
83
|
+
'requestHeaders' | 'requestBody' | 'responseHeaders' | 'responseBody'
|
|
84
|
+
>;
|
|
85
|
+
|
|
86
|
+
const resolveClientRequestCaptureFlags = (
|
|
87
|
+
base?: TraceClientRequestCaptureRule,
|
|
88
|
+
override?: TraceClientRequestCaptureRule
|
|
89
|
+
): ClientRequestCaptureFlags => {
|
|
90
|
+
return {
|
|
91
|
+
requestHeaders: override?.requestHeaders ?? base?.requestHeaders,
|
|
92
|
+
requestBody: override?.requestBody ?? base?.requestBody,
|
|
93
|
+
responseHeaders: override?.responseHeaders ?? base?.responseHeaders,
|
|
94
|
+
responseBody: override?.responseBody ?? base?.responseBody,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const hasClientRequestCaptureFlags = (flags: ClientRequestCaptureFlags): boolean => {
|
|
99
|
+
return Object.values(flags).some((value) => value !== undefined);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const buildClientRequestCaptureRule = (
|
|
103
|
+
mergedRule: TraceFilterRule | undefined,
|
|
104
|
+
flags: ClientRequestCaptureFlags
|
|
105
|
+
): TraceClientRequestCaptureRule => {
|
|
106
|
+
const baseRule = mergedRule ? { ...mergedRule } : {};
|
|
107
|
+
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
...baseRule,
|
|
110
|
+
...(flags.requestHeaders === undefined ? {} : { requestHeaders: flags.requestHeaders }),
|
|
111
|
+
...(flags.requestBody === undefined ? {} : { requestBody: flags.requestBody }),
|
|
112
|
+
...(flags.responseHeaders === undefined ? {} : { responseHeaders: flags.responseHeaders }),
|
|
113
|
+
...(flags.responseBody === undefined ? {} : { responseBody: flags.responseBody }),
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const mergeClientRequestCaptureRule = (
|
|
118
|
+
base?: TraceClientRequestCaptureRule,
|
|
119
|
+
override?: TraceClientRequestCaptureRule
|
|
120
|
+
): TraceClientRequestCaptureRule | undefined => {
|
|
121
|
+
const mergedRule = mergeFilterRule(base, override);
|
|
122
|
+
const flags = resolveClientRequestCaptureFlags(base, override);
|
|
123
|
+
|
|
124
|
+
if (mergedRule === undefined && !hasClientRequestCaptureFlags(flags)) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return buildClientRequestCaptureRule(mergedRule, flags);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const collectClientRequestSourceKeys = (
|
|
132
|
+
base?: TraceClientRequestWatcherToggle,
|
|
133
|
+
override?: Exclude<TraceClientRequestWatcherToggle, boolean>
|
|
134
|
+
): string[] => {
|
|
135
|
+
const overrideSources = override?.sources ?? {};
|
|
136
|
+
const sourceKeys = new Set<string>([
|
|
137
|
+
...Object.keys(isObjectValue(base) ? (base.sources ?? {}) : {}),
|
|
138
|
+
...Object.keys(overrideSources),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
return [...sourceKeys];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const mergeClientRequestSources = (
|
|
145
|
+
base?: TraceClientRequestWatcherToggle,
|
|
146
|
+
override?: Exclude<TraceClientRequestWatcherToggle, boolean>
|
|
147
|
+
): Record<string, TraceClientRequestCaptureRule> | undefined => {
|
|
148
|
+
if (override === undefined) return undefined;
|
|
149
|
+
|
|
150
|
+
const sources: Record<string, TraceClientRequestCaptureRule> = {};
|
|
151
|
+
|
|
152
|
+
for (const key of collectClientRequestSourceKeys(base, override)) {
|
|
153
|
+
const baseSources = isObjectValue(base) ? base.sources : undefined;
|
|
154
|
+
const sourceRule = mergeClientRequestCaptureRule(baseSources?.[key], override.sources?.[key]);
|
|
155
|
+
if (sourceRule !== undefined) {
|
|
156
|
+
sources[key] = sourceRule;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return Object.keys(sources).length === 0 ? undefined : sources;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const mergeClientRequestWatcherToggle = (
|
|
164
|
+
base?: TraceClientRequestWatcherToggle,
|
|
165
|
+
override?: TraceClientRequestWatcherToggle
|
|
166
|
+
): TraceClientRequestWatcherToggle | undefined => {
|
|
167
|
+
if (override === undefined) return base;
|
|
168
|
+
if (override === false || override === true) return override;
|
|
169
|
+
|
|
170
|
+
const baseConfig = isObjectValue(base) ? base : undefined;
|
|
171
|
+
const merged = mergeClientRequestCaptureRule(baseConfig, override) ?? {};
|
|
172
|
+
const sources = mergeClientRequestSources(base, override);
|
|
173
|
+
|
|
174
|
+
if (sources === undefined) {
|
|
175
|
+
return merged;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return Object.freeze({
|
|
179
|
+
...merged,
|
|
180
|
+
sources,
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
54
184
|
const REQUEST_METHOD_KEYS = ['all', 'get', 'post', 'put', 'patch', 'delete'] as const;
|
|
55
185
|
|
|
56
186
|
const mergeRequestWatcherToggle = (
|
|
@@ -99,7 +229,7 @@ const mergeWatchers = (
|
|
|
99
229
|
batch: mergeWatcherToggle(base.batch, override.batch),
|
|
100
230
|
dump: mergeWatcherToggle(base.dump, override.dump),
|
|
101
231
|
view: mergeWatcherToggle(base.view, override.view),
|
|
102
|
-
clientRequest:
|
|
232
|
+
clientRequest: mergeClientRequestWatcherToggle(base.clientRequest, override.clientRequest),
|
|
103
233
|
};
|
|
104
234
|
};
|
|
105
235
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* No auth in this layer — caller mounts middleware as needed.
|
|
4
4
|
*/
|
|
5
5
|
import type { IRequest, IResponse } from '@zintrust/core';
|
|
6
|
-
import type { EntryTypeValue, ITraceStorage } from '../types';
|
|
6
|
+
import type { EntryTypeValue, ITraceEntry, ITraceStorage } from '../types';
|
|
7
7
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
// Storage holder (set once from routes.ts)
|
|
@@ -57,6 +57,128 @@ const getNumericQueryParam = (req: IRequest, key: string): number | undefined =>
|
|
|
57
57
|
return undefined;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
const DEFAULT_PER_PAGE = 50;
|
|
61
|
+
const MAX_PER_PAGE = 100;
|
|
62
|
+
const DEFAULT_REQUEST_PER_PAGE = 25;
|
|
63
|
+
const MAX_REQUEST_PER_PAGE = 50;
|
|
64
|
+
const SUMMARY_TEXT_LIMIT = 280;
|
|
65
|
+
const SUMMARY_ARRAY_LIMIT = 10;
|
|
66
|
+
|
|
67
|
+
type CompactTraceEntry = ITraceEntry<Record<string, unknown>> & {
|
|
68
|
+
hasDetails: true;
|
|
69
|
+
contentBytes?: number;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const truncateText = (value: string, limit = SUMMARY_TEXT_LIMIT): string =>
|
|
73
|
+
value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 3))}...`;
|
|
74
|
+
|
|
75
|
+
const compactValue = (value: unknown): unknown => {
|
|
76
|
+
if (typeof value === 'string') {
|
|
77
|
+
return truncateText(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
typeof value === 'number' ||
|
|
82
|
+
typeof value === 'boolean' ||
|
|
83
|
+
value === null ||
|
|
84
|
+
value === undefined
|
|
85
|
+
) {
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
return value.slice(0, SUMMARY_ARRAY_LIMIT).map((item) => {
|
|
91
|
+
if (typeof item === 'string') {
|
|
92
|
+
return truncateText(item);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (typeof item === 'number' || typeof item === 'boolean' || item === null) {
|
|
96
|
+
return item;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return '[complex]';
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return undefined;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const pickCompactContent = (content: unknown, keys: readonly string[]): Record<string, unknown> => {
|
|
107
|
+
if (typeof content !== 'object' || content === null || Array.isArray(content)) {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const source = content as Record<string, unknown>;
|
|
112
|
+
const compact: Record<string, unknown> = {};
|
|
113
|
+
|
|
114
|
+
for (const key of keys) {
|
|
115
|
+
const value = compactValue(source[key]);
|
|
116
|
+
if (value !== undefined) {
|
|
117
|
+
compact[key] = value;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return compact;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const COMPACT_ENTRY_KEYS: Record<EntryTypeValue, readonly string[]> = {
|
|
125
|
+
request: [
|
|
126
|
+
'method',
|
|
127
|
+
'uri',
|
|
128
|
+
'responseStatus',
|
|
129
|
+
'duration',
|
|
130
|
+
'memory',
|
|
131
|
+
'middleware',
|
|
132
|
+
'hostname',
|
|
133
|
+
'userId',
|
|
134
|
+
],
|
|
135
|
+
query: ['connection', 'sql', 'time', 'duration', 'slow', 'hash', 'hostname'],
|
|
136
|
+
exception: ['class', 'file', 'line', 'message', 'occurrences', 'hostname', 'userId'],
|
|
137
|
+
log: ['level', 'message', 'hostname'],
|
|
138
|
+
job: ['status', 'connection', 'queue', 'name', 'tries', 'timeout', 'hostname'],
|
|
139
|
+
cache: ['operation', 'key', 'hit', 'store', 'payloadLogged', 'ttl', 'duration', 'hostname'],
|
|
140
|
+
schedule: ['name', 'expression', 'status', 'duration', 'hostname'],
|
|
141
|
+
mail: ['to', 'subject', 'template', 'hostname'],
|
|
142
|
+
auth: ['event', 'userId', 'hostname'],
|
|
143
|
+
event: ['name', 'listenerCount', 'hostname'],
|
|
144
|
+
model: ['action', 'model', 'id', 'hostname'],
|
|
145
|
+
notification: ['channels', 'notifiable', 'notification', 'message', 'hostname'],
|
|
146
|
+
redis: ['command', 'duration', 'hostname'],
|
|
147
|
+
gate: ['ability', 'result', 'userId', 'subject', 'hostname'],
|
|
148
|
+
middleware: ['name', 'event', 'duration', 'hostname'],
|
|
149
|
+
command: ['name', 'exitCode', 'duration', 'hostname'],
|
|
150
|
+
batch: ['name', 'total', 'processed', 'failed', 'status', 'hostname'],
|
|
151
|
+
dump: ['file', 'line', 'hostname'],
|
|
152
|
+
view: ['template', 'duration', 'hostname'],
|
|
153
|
+
client_request: ['source', 'method', 'url', 'responseStatus', 'error', 'duration', 'hostname'],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const compactEntryContent = (entry: ITraceEntry): Record<string, unknown> =>
|
|
157
|
+
pickCompactContent(entry.content, COMPACT_ENTRY_KEYS[entry.type]);
|
|
158
|
+
|
|
159
|
+
const estimateContentBytes = (content: unknown): number | undefined => {
|
|
160
|
+
try {
|
|
161
|
+
return new TextEncoder().encode(JSON.stringify(content)).length;
|
|
162
|
+
} catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const compactListEntry = (entry: ITraceEntry): CompactTraceEntry => ({
|
|
168
|
+
...entry,
|
|
169
|
+
content: compactEntryContent(entry),
|
|
170
|
+
hasDetails: true,
|
|
171
|
+
contentBytes: estimateContentBytes(entry.content),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const resolvePerPage = (req: IRequest, type?: EntryTypeValue): number => {
|
|
175
|
+
const isRequestList = type === 'request';
|
|
176
|
+
const fallback = isRequestList ? DEFAULT_REQUEST_PER_PAGE : DEFAULT_PER_PAGE;
|
|
177
|
+
const limit = isRequestList ? MAX_REQUEST_PER_PAGE : MAX_PER_PAGE;
|
|
178
|
+
|
|
179
|
+
return Math.max(1, Math.min(qpInt(req, 'perPage', fallback), limit));
|
|
180
|
+
};
|
|
181
|
+
|
|
60
182
|
// ---------------------------------------------------------------------------
|
|
61
183
|
// Entry handlers
|
|
62
184
|
// ---------------------------------------------------------------------------
|
|
@@ -64,18 +186,25 @@ const getNumericQueryParam = (req: IRequest, key: string): number | undefined =>
|
|
|
64
186
|
export async function listEntries(req: IRequest, res: IResponse): Promise<void> {
|
|
65
187
|
const storage = getStorage(res);
|
|
66
188
|
if (storage !== null) {
|
|
189
|
+
const type = qp(req, 'type') as EntryTypeValue | undefined;
|
|
67
190
|
const opts = {
|
|
68
|
-
type
|
|
191
|
+
type,
|
|
69
192
|
tag: qp(req, 'tag'),
|
|
70
193
|
batchId: qp(req, 'batchId'),
|
|
71
194
|
from: getNumericQueryParam(req, 'from'),
|
|
72
195
|
to: getNumericQueryParam(req, 'to'),
|
|
73
|
-
page: qpInt(req, 'page', 1),
|
|
74
|
-
perPage:
|
|
196
|
+
page: Math.max(1, qpInt(req, 'page', 1)),
|
|
197
|
+
perPage: resolvePerPage(req, type),
|
|
75
198
|
};
|
|
76
199
|
try {
|
|
77
200
|
const result = await storage.queryEntries(opts);
|
|
78
|
-
res.json({
|
|
201
|
+
res.json({
|
|
202
|
+
ok: true,
|
|
203
|
+
data: result.data.map(compactListEntry),
|
|
204
|
+
total: result.total,
|
|
205
|
+
page: opts.page,
|
|
206
|
+
perPage: opts.perPage,
|
|
207
|
+
});
|
|
79
208
|
} catch (err) {
|
|
80
209
|
res.setStatus(500).json({ error: (err as Error).message });
|
|
81
210
|
}
|
package/src/dashboard/ui.ts
CHANGED
|
@@ -38,6 +38,7 @@ const SUN_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke
|
|
|
38
38
|
const MOON_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"></path></svg>`;
|
|
39
39
|
|
|
40
40
|
const COPY_ICON = `<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`;
|
|
41
|
+
const DISCLOSURE_ICON = `<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 4l6 6-6 6"></path></svg>`;
|
|
41
42
|
|
|
42
43
|
const JSON_HIGHLIGHT_PATTERN = String.raw`("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")(?=\s*:)|(\s*:)|("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*")|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?`;
|
|
43
44
|
const SQL_HIGHLIGHT_PATTERN = String.raw`(\/\*[\s\S]*?\*\/|--.*$|'(?:''|[^'])*'|\x60[^\x60]+\x60|\b(?:select|from|where|insert|into|values|update|delete|join|left|right|inner|outer|on|and|or|limit|order|by|group|having|as|distinct|null|is|in|like|set|case|when|then|else|end|returning|union|all)\b|-?\d+(?:\.\d+)?)`;
|
|
@@ -137,6 +138,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
137
138
|
const SUN_ICON = __TRACE_SUN_ICON__;
|
|
138
139
|
const MOON_ICON = __TRACE_MOON_ICON__;
|
|
139
140
|
const COPY_ICON = __TRACE_COPY_ICON__;
|
|
141
|
+
const DISCLOSURE_ICON = __TRACE_DISCLOSURE_ICON__;
|
|
140
142
|
const JSON_HIGHLIGHT_PATTERN = new RegExp(__TRACE_JSON_REGEX__, 'g');
|
|
141
143
|
const SQL_HIGHLIGHT_PATTERN = new RegExp(__TRACE_SQL_REGEX__, 'gim');
|
|
142
144
|
const ENTRY_TYPES = ['request','query','exception','log','job','cache','schedule','mail','auth','event','model','notification','redis','gate','middleware','command','batch','dump','view','client_request'];
|
|
@@ -651,6 +653,7 @@ const DASHBOARD_DOCUMENT = String.raw`<!DOCTYPE html>
|
|
|
651
653
|
return [
|
|
652
654
|
'<details class="trace-item trace-disclosure"' + (isInitiallyOpen ? ' open' : '') + '>',
|
|
653
655
|
'<summary class="trace-item-head trace-summary">',
|
|
656
|
+
'<span class="trace-summary-icon">' + DISCLOSURE_ICON + '</span>',
|
|
654
657
|
'<span class="trace-summary-main">',
|
|
655
658
|
'<span><span class="' + typeClass(entry) + '">' + escapeHtml(entryTypeLabel(entry)) + '</span></span>',
|
|
656
659
|
'<span class="trace-summary-copy">' + entrySummaryInlineHtml(entry) + '</span>',
|
|
@@ -1083,6 +1086,7 @@ const buildDashboardHtml = (basePath: string, projectName?: string): string => {
|
|
|
1083
1086
|
.replace('__TRACE_SUN_ICON__', JSON.stringify(SUN_ICON))
|
|
1084
1087
|
.replace('__TRACE_MOON_ICON__', JSON.stringify(MOON_ICON))
|
|
1085
1088
|
.replace('__TRACE_COPY_ICON__', JSON.stringify(COPY_ICON))
|
|
1089
|
+
.replace('__TRACE_DISCLOSURE_ICON__', JSON.stringify(DISCLOSURE_ICON))
|
|
1086
1090
|
.replace('__TRACE_JSON_REGEX__', JSON.stringify(JSON_HIGHLIGHT_PATTERN))
|
|
1087
1091
|
.replace('__TRACE_SQL_REGEX__', JSON.stringify(SQL_HIGHLIGHT_PATTERN))
|
|
1088
1092
|
.replace('__TRACE_BASE_PATH_LABEL__', basePath)
|