@zintrust/trace 0.4.86 → 0.4.94
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 +21 -13
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/register.js +102 -56
- package/dist/storage/TraceContentBudget.d.ts +4 -0
- package/dist/storage/TraceContentBudget.js +114 -0
- package/dist/watchers/LogWatcher.js +32 -3
- package/dist/watchers/QueryWatcher.js +3 -1
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/register.ts +171 -87
- package/src/storage/TraceContentBudget.ts +145 -0
- package/src/watchers/LogWatcher.ts +45 -3
- package/src/watchers/QueryWatcher.ts +5 -1
package/dist/build-manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.4.94",
|
|
4
|
+
"buildDate": "2026-04-11T09:24:47.024Z",
|
|
5
5
|
"buildEnvironment": {
|
|
6
6
|
"node": "v20.20.2",
|
|
7
7
|
"platform": "linux",
|
|
8
8
|
"arch": "x64"
|
|
9
9
|
},
|
|
10
10
|
"git": {
|
|
11
|
-
"commit": "
|
|
11
|
+
"commit": "785d0351",
|
|
12
12
|
"branch": "master"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
@@ -70,12 +70,12 @@
|
|
|
70
70
|
"sha256": "2903901d8c0c5076118aa691727daa79be7abe87fdb393c5b389a2b1a8fce170"
|
|
71
71
|
},
|
|
72
72
|
"index.d.ts": {
|
|
73
|
-
"size":
|
|
74
|
-
"sha256": "
|
|
73
|
+
"size": 2537,
|
|
74
|
+
"sha256": "1707d26322dbad17f6bf85938ae6fe2477e84c7fed3333760ce8d6eadfaaffd2"
|
|
75
75
|
},
|
|
76
76
|
"index.js": {
|
|
77
|
-
"size":
|
|
78
|
-
"sha256": "
|
|
77
|
+
"size": 3325,
|
|
78
|
+
"sha256": "0926788b00fc68f513ae20a508f2ef3c2f4e7e907fa32d70f146ce6e8b0ec812"
|
|
79
79
|
},
|
|
80
80
|
"migrations/20260331000001_create_zin_trace_entries_table.d.ts": {
|
|
81
81
|
"size": 304,
|
|
@@ -130,8 +130,16 @@
|
|
|
130
130
|
"sha256": "71d366165dd36f1675aa253a76262b226fb6c62e5ab632746b8aea61c0c625fc"
|
|
131
131
|
},
|
|
132
132
|
"register.js": {
|
|
133
|
-
"size":
|
|
134
|
-
"sha256": "
|
|
133
|
+
"size": 14327,
|
|
134
|
+
"sha256": "efc9bb131b9eef7e81a6f85ea3338fd8ac74257f794aa0fc7c76efed636f99d8"
|
|
135
|
+
},
|
|
136
|
+
"storage/TraceContentBudget.d.ts": {
|
|
137
|
+
"size": 159,
|
|
138
|
+
"sha256": "d899a615e6cf2a5eea51f6200347cb81fbaae11979b801859f57135083aaf85b"
|
|
139
|
+
},
|
|
140
|
+
"storage/TraceContentBudget.js": {
|
|
141
|
+
"size": 4022,
|
|
142
|
+
"sha256": "4b1d4f0ad7da15caeaa1f9fe8b4edcd3000b0000c5bdc270601cd6db2eedce2e"
|
|
135
143
|
},
|
|
136
144
|
"storage/TraceContentRedaction.d.ts": {
|
|
137
145
|
"size": 207,
|
|
@@ -330,8 +338,8 @@
|
|
|
330
338
|
"sha256": "f3ddc5f8b58c6c86ac6b464dd48e5a55e79ab2bf2e735feacffc7480e4ccc0c4"
|
|
331
339
|
},
|
|
332
340
|
"watchers/LogWatcher.js": {
|
|
333
|
-
"size":
|
|
334
|
-
"sha256": "
|
|
341
|
+
"size": 3126,
|
|
342
|
+
"sha256": "e0944661b48b682520d60ee9e98b3fa9f8ba4694743f134f98b04b8b2dd479e6"
|
|
335
343
|
},
|
|
336
344
|
"watchers/MailWatcher.d.ts": {
|
|
337
345
|
"size": 244,
|
|
@@ -370,8 +378,8 @@
|
|
|
370
378
|
"sha256": "5d5046c65e5b683369c7709f1acd09b60aec3e7f44748fd1baeb35498836465b"
|
|
371
379
|
},
|
|
372
380
|
"watchers/QueryWatcher.js": {
|
|
373
|
-
"size":
|
|
374
|
-
"sha256": "
|
|
381
|
+
"size": 3002,
|
|
382
|
+
"sha256": "c7131284e75ab2f0193597cdf3ef0aa7eab1a3872fe9193579a140a41fadb57e"
|
|
375
383
|
},
|
|
376
384
|
"watchers/RedisWatcher.d.ts": {
|
|
377
385
|
"size": 294,
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
export { TraceConfig } from './config';
|
|
9
9
|
export { TraceStorage } from './storage';
|
|
10
10
|
export type { ITraceStorage } from './storage';
|
|
11
|
+
export { TraceContentBudget } from './storage/TraceContentBudget';
|
|
11
12
|
export { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
12
13
|
export { TraceContext } from './context';
|
|
13
14
|
export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export { TraceConfig } from './config.js';
|
|
|
14
14
|
// Storage
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
export { TraceStorage } from './storage/index.js';
|
|
17
|
+
export { TraceContentBudget } from './storage/TraceContentBudget.js';
|
|
17
18
|
export { TraceContentRedaction } from './storage/TraceContentRedaction.js';
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Context
|
package/dist/register.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { TraceConfig } from './config.js';
|
|
23
23
|
import { TraceContext } from './context.js';
|
|
24
24
|
import { TraceStorage } from './storage/index.js';
|
|
25
|
+
import { TraceContentBudget } from './storage/TraceContentBudget.js';
|
|
25
26
|
import { TraceContentRedaction } from './storage/TraceContentRedaction.js';
|
|
26
27
|
import { TraceEntryFiltering } from './storage/TraceEntryFiltering.js';
|
|
27
28
|
import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics.js';
|
|
@@ -39,6 +40,11 @@ const importCore = async () => {
|
|
|
39
40
|
return {};
|
|
40
41
|
}
|
|
41
42
|
};
|
|
43
|
+
const TRACE_REQUIRED_TABLES = [
|
|
44
|
+
'zin_trace_entries',
|
|
45
|
+
'zin_trace_entries_tags',
|
|
46
|
+
'zin_trace_monitoring',
|
|
47
|
+
];
|
|
42
48
|
const resolveRegisterMiddleware = () => {
|
|
43
49
|
const globalMiddlewareRegistrarState = globalThis;
|
|
44
50
|
return (middleware) => {
|
|
@@ -69,7 +75,7 @@ const resolveObservedConnectionName = (env, configuredObservedConnection, storag
|
|
|
69
75
|
configuredObservedConnection.trim() !== '') {
|
|
70
76
|
return resolveTraceConnectionName(env, configuredObservedConnection);
|
|
71
77
|
}
|
|
72
|
-
const defaultConnectionName = resolveTraceConnectionName(env
|
|
78
|
+
const defaultConnectionName = resolveTraceConnectionName(env);
|
|
73
79
|
if (storageConnectionName !== defaultConnectionName) {
|
|
74
80
|
return defaultConnectionName;
|
|
75
81
|
}
|
|
@@ -150,6 +156,43 @@ const buildTraceRedactionOverrides = (input) => {
|
|
|
150
156
|
? redaction
|
|
151
157
|
: undefined;
|
|
152
158
|
};
|
|
159
|
+
const createTraceConfigError = (coreApi, message, details) => {
|
|
160
|
+
if (coreApi.ErrorFactory?.createConfigError !== undefined) {
|
|
161
|
+
return coreApi.ErrorFactory.createConfigError(message, details);
|
|
162
|
+
}
|
|
163
|
+
const error = new globalThis.Error(message);
|
|
164
|
+
error.name = 'ConfigError';
|
|
165
|
+
error.code = 'CONFIG_ERROR';
|
|
166
|
+
error.statusCode = 500;
|
|
167
|
+
error.details = details;
|
|
168
|
+
return error;
|
|
169
|
+
};
|
|
170
|
+
function assertTraceConnectionResolved(coreApi, db, params) {
|
|
171
|
+
if (db !== undefined) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
throw createTraceConfigError(coreApi, `Trace connection "${params.connectionName}" could not be resolved.`, {
|
|
175
|
+
connectionName: params.connectionName,
|
|
176
|
+
envKey: params.envKey,
|
|
177
|
+
hint: params.envKey === 'TRACE_DB_CONNECTION'
|
|
178
|
+
? 'Configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.'
|
|
179
|
+
: 'Configure TRACE_QUERY_CONNECTION, or ensure DB_CONNECTION resolves to an existing database connection.',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const assertTraceStorageReady = async (coreApi, db, connectionName) => {
|
|
183
|
+
try {
|
|
184
|
+
await Promise.all(TRACE_REQUIRED_TABLES.map(async (table) => {
|
|
185
|
+
await db.queryOne(`SELECT 1 AS ok FROM ${table} LIMIT 1`, []);
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw createTraceConfigError(coreApi, `Trace storage connection "${connectionName}" is not ready. Create the database if needed and run \`zin migrate:trace\` before enabling TRACE_ENABLED.`, {
|
|
190
|
+
connectionName,
|
|
191
|
+
error,
|
|
192
|
+
requiredTables: [...TRACE_REQUIRED_TABLES],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
};
|
|
153
196
|
const core = (await importCore());
|
|
154
197
|
const Env = core.Env;
|
|
155
198
|
const startupOverrides = resolveTraceStartupOverrides(core);
|
|
@@ -205,62 +248,65 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
205
248
|
const resolvedObservedConnectionName = resolveObservedConnectionName(Env, config.observeConnection, resolvedConnectionName);
|
|
206
249
|
const storageDb = core.useDatabase?.(undefined, resolvedConnectionName);
|
|
207
250
|
const observedDb = core.useDatabase?.(undefined, resolvedObservedConnectionName);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
import('./watchers/ScheduleWatcher.js'),
|
|
224
|
-
import('./watchers/MailWatcher.js'),
|
|
225
|
-
import('./watchers/AuthWatcher.js'),
|
|
226
|
-
import('./watchers/EventWatcher.js'),
|
|
227
|
-
import('./watchers/ModelWatcher.js'),
|
|
228
|
-
import('./watchers/NotificationWatcher.js'),
|
|
229
|
-
import('./watchers/RedisWatcher.js'),
|
|
230
|
-
import('./watchers/GateWatcher.js'),
|
|
231
|
-
import('./watchers/MiddlewareWatcher.js'),
|
|
232
|
-
import('./watchers/CommandWatcher.js'),
|
|
233
|
-
import('./watchers/BatchWatcher.js'),
|
|
234
|
-
import('./watchers/DumpWatcher.js'),
|
|
235
|
-
import('./watchers/ViewWatcher.js'),
|
|
236
|
-
import('./watchers/HttpClientWatcher.js'),
|
|
237
|
-
]);
|
|
238
|
-
const watcherArgs = { storage, config, db: observedDb };
|
|
239
|
-
HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
|
|
240
|
-
QueryWatcher.register(watcherArgs);
|
|
241
|
-
LogWatcher.register(watcherArgs);
|
|
242
|
-
ExceptionWatcher.register(watcherArgs);
|
|
243
|
-
JobWatcher.register(watcherArgs);
|
|
244
|
-
CacheWatcher.register(watcherArgs);
|
|
245
|
-
ScheduleWatcher.register(watcherArgs);
|
|
246
|
-
MailWatcher.register(watcherArgs);
|
|
247
|
-
AuthWatcher.register(watcherArgs);
|
|
248
|
-
EventWatcher.register(watcherArgs);
|
|
249
|
-
ModelWatcher.register(watcherArgs);
|
|
250
|
-
NotificationWatcher.register(watcherArgs);
|
|
251
|
-
RedisWatcher.register(watcherArgs);
|
|
252
|
-
GateWatcher.register(watcherArgs);
|
|
253
|
-
MiddlewareWatcher.register(watcherArgs);
|
|
254
|
-
CommandWatcher.register(watcherArgs);
|
|
255
|
-
BatchWatcher.register(watcherArgs);
|
|
256
|
-
DumpWatcher.register(watcherArgs);
|
|
257
|
-
ViewWatcher.register(watcherArgs);
|
|
258
|
-
HttpClientWatcher.register(watcherArgs);
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
// eslint-disable-next-line no-console
|
|
262
|
-
console.warn('[trace] Could not resolve database connection - skipping init.');
|
|
251
|
+
assertTraceConnectionResolved(core, storageDb, {
|
|
252
|
+
connectionName: resolvedConnectionName,
|
|
253
|
+
envKey: 'TRACE_DB_CONNECTION',
|
|
254
|
+
});
|
|
255
|
+
assertTraceConnectionResolved(core, observedDb, {
|
|
256
|
+
connectionName: resolvedObservedConnectionName,
|
|
257
|
+
envKey: 'TRACE_QUERY_CONNECTION',
|
|
258
|
+
});
|
|
259
|
+
await assertTraceStorageReady(core, storageDb, resolvedConnectionName);
|
|
260
|
+
const storage = TraceWriteDiagnostics.wrapStorage(TraceContentBudget.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config), config.redaction)), {
|
|
261
|
+
connectionName: resolvedConnectionName,
|
|
262
|
+
logger: core.Logger,
|
|
263
|
+
});
|
|
264
|
+
if (core.RequestContext) {
|
|
265
|
+
TraceContext.setRequestContextImpl(core.RequestContext);
|
|
263
266
|
}
|
|
267
|
+
const [{ HttpWatcher }, { QueryWatcher }, { LogWatcher }, { ExceptionWatcher }, { JobWatcher }, { CacheWatcher }, { ScheduleWatcher }, { MailWatcher }, { AuthWatcher }, { EventWatcher }, { ModelWatcher }, { NotificationWatcher }, { RedisWatcher }, { GateWatcher }, { MiddlewareWatcher }, { CommandWatcher }, { BatchWatcher }, { DumpWatcher }, { ViewWatcher }, { HttpClientWatcher },] = await Promise.all([
|
|
268
|
+
import('./watchers/HttpWatcher.js'),
|
|
269
|
+
import('./watchers/QueryWatcher.js'),
|
|
270
|
+
import('./watchers/LogWatcher.js'),
|
|
271
|
+
import('./watchers/ExceptionWatcher.js'),
|
|
272
|
+
import('./watchers/JobWatcher.js'),
|
|
273
|
+
import('./watchers/CacheWatcher.js'),
|
|
274
|
+
import('./watchers/ScheduleWatcher.js'),
|
|
275
|
+
import('./watchers/MailWatcher.js'),
|
|
276
|
+
import('./watchers/AuthWatcher.js'),
|
|
277
|
+
import('./watchers/EventWatcher.js'),
|
|
278
|
+
import('./watchers/ModelWatcher.js'),
|
|
279
|
+
import('./watchers/NotificationWatcher.js'),
|
|
280
|
+
import('./watchers/RedisWatcher.js'),
|
|
281
|
+
import('./watchers/GateWatcher.js'),
|
|
282
|
+
import('./watchers/MiddlewareWatcher.js'),
|
|
283
|
+
import('./watchers/CommandWatcher.js'),
|
|
284
|
+
import('./watchers/BatchWatcher.js'),
|
|
285
|
+
import('./watchers/DumpWatcher.js'),
|
|
286
|
+
import('./watchers/ViewWatcher.js'),
|
|
287
|
+
import('./watchers/HttpClientWatcher.js'),
|
|
288
|
+
]);
|
|
289
|
+
const watcherArgs = { storage, config, db: observedDb };
|
|
290
|
+
HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
|
|
291
|
+
QueryWatcher.register(watcherArgs);
|
|
292
|
+
LogWatcher.register(watcherArgs);
|
|
293
|
+
ExceptionWatcher.register(watcherArgs);
|
|
294
|
+
JobWatcher.register(watcherArgs);
|
|
295
|
+
CacheWatcher.register(watcherArgs);
|
|
296
|
+
ScheduleWatcher.register(watcherArgs);
|
|
297
|
+
MailWatcher.register(watcherArgs);
|
|
298
|
+
AuthWatcher.register(watcherArgs);
|
|
299
|
+
EventWatcher.register(watcherArgs);
|
|
300
|
+
ModelWatcher.register(watcherArgs);
|
|
301
|
+
NotificationWatcher.register(watcherArgs);
|
|
302
|
+
RedisWatcher.register(watcherArgs);
|
|
303
|
+
GateWatcher.register(watcherArgs);
|
|
304
|
+
MiddlewareWatcher.register(watcherArgs);
|
|
305
|
+
CommandWatcher.register(watcherArgs);
|
|
306
|
+
BatchWatcher.register(watcherArgs);
|
|
307
|
+
DumpWatcher.register(watcherArgs);
|
|
308
|
+
ViewWatcher.register(watcherArgs);
|
|
309
|
+
HttpClientWatcher.register(watcherArgs);
|
|
264
310
|
}
|
|
265
311
|
}
|
|
266
312
|
else if (!traceAlreadyInitialized) {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const DEFAULT_MAX_ENTRY_BYTES = 64 * 1024;
|
|
2
|
+
const DEFAULT_MAX_STRING_BYTES = 16 * 1024;
|
|
3
|
+
const DEFAULT_MAX_ARRAY_ITEMS = 25;
|
|
4
|
+
const DEFAULT_MAX_OBJECT_ENTRIES = 40;
|
|
5
|
+
const DEFAULT_MAX_DEPTH = 6;
|
|
6
|
+
const DROPPED_FIELD_MESSAGE = '[trace] Value dropped because the field exceeded the trace storage size limit.';
|
|
7
|
+
const COMPACTED_CONTENT_MESSAGE = '[trace] Trace content was compacted because it exceeded the trace storage size limit.';
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const serializedSize = (value) => {
|
|
10
|
+
try {
|
|
11
|
+
return encoder.encode(JSON.stringify(value)).length;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return Number.MAX_SAFE_INTEGER;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const describeValueType = (value) => {
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return 'array';
|
|
20
|
+
if (value === null)
|
|
21
|
+
return 'null';
|
|
22
|
+
return typeof value;
|
|
23
|
+
};
|
|
24
|
+
const compactValue = (value, depth) => {
|
|
25
|
+
if (depth >= DEFAULT_MAX_DEPTH) {
|
|
26
|
+
return DROPPED_FIELD_MESSAGE;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
return serializedSize(value) > DEFAULT_MAX_STRING_BYTES ? DROPPED_FIELD_MESSAGE : value;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
const next = value
|
|
33
|
+
.slice(0, DEFAULT_MAX_ARRAY_ITEMS)
|
|
34
|
+
.map((item) => compactValue(item, depth + 1));
|
|
35
|
+
if (value.length > DEFAULT_MAX_ARRAY_ITEMS) {
|
|
36
|
+
next.push(`[trace] ${String(value.length - DEFAULT_MAX_ARRAY_ITEMS)} additional items were dropped.`);
|
|
37
|
+
}
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value !== 'object' || value === null) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
const entries = Object.entries(value);
|
|
44
|
+
const compactedEntries = entries
|
|
45
|
+
.slice(0, DEFAULT_MAX_OBJECT_ENTRIES)
|
|
46
|
+
.map(([key, entryValue]) => [key, compactValue(entryValue, depth + 1)]);
|
|
47
|
+
if (entries.length > DEFAULT_MAX_OBJECT_ENTRIES) {
|
|
48
|
+
compactedEntries.push([
|
|
49
|
+
'__traceNotice',
|
|
50
|
+
`[trace] ${String(entries.length - DEFAULT_MAX_OBJECT_ENTRIES)} additional fields were dropped.`,
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
return Object.fromEntries(compactedEntries);
|
|
54
|
+
};
|
|
55
|
+
const compactTopLevelObjectToBudget = (value) => {
|
|
56
|
+
const compacted = {
|
|
57
|
+
...value,
|
|
58
|
+
__traceNotice: COMPACTED_CONTENT_MESSAGE,
|
|
59
|
+
};
|
|
60
|
+
const keysByDescendingSize = Object.keys(compacted)
|
|
61
|
+
.filter((key) => key !== '__traceNotice')
|
|
62
|
+
.sort((left, right) => serializedSize(compacted[right]) - serializedSize(compacted[left]));
|
|
63
|
+
for (const key of keysByDescendingSize) {
|
|
64
|
+
if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES)
|
|
65
|
+
break;
|
|
66
|
+
compacted[key] = DROPPED_FIELD_MESSAGE;
|
|
67
|
+
}
|
|
68
|
+
return compacted;
|
|
69
|
+
};
|
|
70
|
+
const fitContentToBudget = (content) => {
|
|
71
|
+
if (serializedSize(content) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
72
|
+
return content;
|
|
73
|
+
}
|
|
74
|
+
const compacted = compactValue(content, 0);
|
|
75
|
+
if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
76
|
+
return compacted;
|
|
77
|
+
}
|
|
78
|
+
if (typeof compacted === 'object' && compacted !== null && !Array.isArray(compacted)) {
|
|
79
|
+
const topLevelCompacted = compactTopLevelObjectToBudget(compacted);
|
|
80
|
+
if (serializedSize(topLevelCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
81
|
+
return topLevelCompacted;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
__traceNotice: COMPACTED_CONTENT_MESSAGE,
|
|
86
|
+
dropped: true,
|
|
87
|
+
valueType: describeValueType(content),
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const fitEntryToBudget = (entry) => ({
|
|
91
|
+
...entry,
|
|
92
|
+
content: fitContentToBudget(entry.content),
|
|
93
|
+
});
|
|
94
|
+
const fitPatchToBudget = (patch) => {
|
|
95
|
+
if (patch.content === undefined)
|
|
96
|
+
return patch;
|
|
97
|
+
return {
|
|
98
|
+
...patch,
|
|
99
|
+
content: fitContentToBudget(patch.content),
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
export const TraceContentBudget = Object.freeze({
|
|
103
|
+
wrapStorage(storage) {
|
|
104
|
+
return Object.freeze({
|
|
105
|
+
...storage,
|
|
106
|
+
writeEntry: async (entry) => {
|
|
107
|
+
await storage.writeEntry(fitEntryToBudget(entry));
|
|
108
|
+
},
|
|
109
|
+
updateEntry: async (uuid, patch) => {
|
|
110
|
+
await storage.updateEntry(uuid, fitPatchToBudget(patch));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -17,8 +17,37 @@ const TRACE_INFRASTRUCTURE_LOG_MESSAGES = new Set([
|
|
|
17
17
|
'[MySQLProxyAdapter] Proxy request failed',
|
|
18
18
|
'[trace] Trace storage write degraded',
|
|
19
19
|
]);
|
|
20
|
-
const
|
|
21
|
-
|
|
20
|
+
const TRACE_STORAGE_TABLE_NAMES = [
|
|
21
|
+
'zin_trace_entries',
|
|
22
|
+
'zin_trace_entries_tags',
|
|
23
|
+
'zin_trace_monitoring',
|
|
24
|
+
];
|
|
25
|
+
const isTraceStorageQuery = (sql) => {
|
|
26
|
+
const normalized = sql.toLowerCase();
|
|
27
|
+
return TRACE_STORAGE_TABLE_NAMES.some((tableName) => normalized.includes(tableName));
|
|
28
|
+
};
|
|
29
|
+
const extractSqlFromLog = (message, context) => {
|
|
30
|
+
const contextSql = context?.['sql'];
|
|
31
|
+
if (typeof contextSql === 'string')
|
|
32
|
+
return contextSql;
|
|
33
|
+
const trimmed = message.trim();
|
|
34
|
+
const rawPrefix = 'Raw SQL Query executed:';
|
|
35
|
+
if (trimmed.startsWith(rawPrefix)) {
|
|
36
|
+
const sql = trimmed.slice(rawPrefix.length).trim();
|
|
37
|
+
return sql === '' ? undefined : sql;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
};
|
|
41
|
+
const isTraceStorageQueryLog = (message, context) => {
|
|
42
|
+
const normalizedMessage = message.trim().toLowerCase();
|
|
43
|
+
if (!normalizedMessage.includes('query executed'))
|
|
44
|
+
return false;
|
|
45
|
+
const sql = extractSqlFromLog(message, context);
|
|
46
|
+
return typeof sql === 'string' && isTraceStorageQuery(sql);
|
|
47
|
+
};
|
|
48
|
+
const shouldSkipTraceInfrastructureLog = (message, context) => {
|
|
49
|
+
return (TRACE_INFRASTRUCTURE_LOG_MESSAGES.has(message.trim()) ||
|
|
50
|
+
isTraceStorageQueryLog(message, context));
|
|
22
51
|
};
|
|
23
52
|
export const LogWatcher = Object.freeze({
|
|
24
53
|
register({ storage, config }) {
|
|
@@ -34,7 +63,7 @@ export const LogWatcher = Object.freeze({
|
|
|
34
63
|
return;
|
|
35
64
|
if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes))
|
|
36
65
|
return;
|
|
37
|
-
if (shouldSkipTraceInfrastructureLog(message))
|
|
66
|
+
if (shouldSkipTraceInfrastructureLog(message, context))
|
|
38
67
|
return;
|
|
39
68
|
const content = {
|
|
40
69
|
level,
|
|
@@ -22,7 +22,9 @@ const bindingsInterpolated = (sql, params) => {
|
|
|
22
22
|
};
|
|
23
23
|
const isTraceStorageQuery = (sql) => {
|
|
24
24
|
const normalized = sql.toLowerCase();
|
|
25
|
-
return normalized.includes('zin_trace_entries') ||
|
|
25
|
+
return (normalized.includes('zin_trace_entries') ||
|
|
26
|
+
normalized.includes('zin_trace_entries_tags') ||
|
|
27
|
+
normalized.includes('zin_trace_monitoring'));
|
|
26
28
|
};
|
|
27
29
|
const emit = (query, params, duration, connection = 'default') => {
|
|
28
30
|
if (_storage === null || _config === null)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/trace",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.94",
|
|
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.93"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { TraceConfig } from './config';
|
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
export { TraceStorage } from './storage';
|
|
20
20
|
export type { ITraceStorage } from './storage';
|
|
21
|
+
export { TraceContentBudget } from './storage/TraceContentBudget';
|
|
21
22
|
export { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
package/src/register.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { TraceConfig } from './config';
|
|
23
23
|
import { TraceContext } from './context';
|
|
24
24
|
import { TraceStorage } from './storage';
|
|
25
|
+
import { TraceContentBudget } from './storage/TraceContentBudget';
|
|
25
26
|
import { TraceContentRedaction } from './storage/TraceContentRedaction';
|
|
26
27
|
import { TraceEntryFiltering } from './storage/TraceEntryFiltering';
|
|
27
28
|
import { TraceWriteDiagnostics } from './storage/TraceWriteDiagnostics';
|
|
@@ -64,6 +65,9 @@ type CoreApi = {
|
|
|
64
65
|
Logger?: {
|
|
65
66
|
warn(message: string, context?: Record<string, unknown>): void;
|
|
66
67
|
};
|
|
68
|
+
ErrorFactory?: {
|
|
69
|
+
createConfigError(message: string, details?: unknown): Error;
|
|
70
|
+
};
|
|
67
71
|
StartupConfigFile?: {
|
|
68
72
|
Trace?: string;
|
|
69
73
|
};
|
|
@@ -72,6 +76,14 @@ type CoreApi = {
|
|
|
72
76
|
};
|
|
73
77
|
};
|
|
74
78
|
|
|
79
|
+
type CoreDatabase = import('@zintrust/core').IDatabase;
|
|
80
|
+
|
|
81
|
+
const TRACE_REQUIRED_TABLES = [
|
|
82
|
+
'zin_trace_entries',
|
|
83
|
+
'zin_trace_entries_tags',
|
|
84
|
+
'zin_trace_monitoring',
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
75
87
|
type GlobalMiddlewareRegistrarState = {
|
|
76
88
|
__zintrust_register_global_middleware__?: ITraceWatcherConfig['registerMiddleware'];
|
|
77
89
|
__zintrust_pending_global_middlewares__?: Array<
|
|
@@ -125,7 +137,7 @@ const resolveObservedConnectionName = (
|
|
|
125
137
|
return resolveTraceConnectionName(env, configuredObservedConnection);
|
|
126
138
|
}
|
|
127
139
|
|
|
128
|
-
const defaultConnectionName = resolveTraceConnectionName(env
|
|
140
|
+
const defaultConnectionName = resolveTraceConnectionName(env);
|
|
129
141
|
if (storageConnectionName !== defaultConnectionName) {
|
|
130
142
|
return defaultConnectionName;
|
|
131
143
|
}
|
|
@@ -217,6 +229,71 @@ const buildTraceRedactionOverrides = (input: {
|
|
|
217
229
|
: undefined;
|
|
218
230
|
};
|
|
219
231
|
|
|
232
|
+
const createTraceConfigError = (coreApi: CoreApi, message: string, details?: unknown): Error => {
|
|
233
|
+
if (coreApi.ErrorFactory?.createConfigError !== undefined) {
|
|
234
|
+
return coreApi.ErrorFactory.createConfigError(message, details);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const error = new globalThis.Error(message) as Error & {
|
|
238
|
+
code?: string;
|
|
239
|
+
details?: unknown;
|
|
240
|
+
name?: string;
|
|
241
|
+
statusCode?: number;
|
|
242
|
+
};
|
|
243
|
+
error.name = 'ConfigError';
|
|
244
|
+
error.code = 'CONFIG_ERROR';
|
|
245
|
+
error.statusCode = 500;
|
|
246
|
+
error.details = details;
|
|
247
|
+
return error;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
function assertTraceConnectionResolved(
|
|
251
|
+
coreApi: CoreApi,
|
|
252
|
+
db: CoreDatabase | undefined,
|
|
253
|
+
params: { connectionName: string; envKey: 'TRACE_DB_CONNECTION' | 'TRACE_QUERY_CONNECTION' }
|
|
254
|
+
): asserts db is CoreDatabase {
|
|
255
|
+
if (db !== undefined) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
throw createTraceConfigError(
|
|
260
|
+
coreApi,
|
|
261
|
+
`Trace connection "${params.connectionName}" could not be resolved.`,
|
|
262
|
+
{
|
|
263
|
+
connectionName: params.connectionName,
|
|
264
|
+
envKey: params.envKey,
|
|
265
|
+
hint:
|
|
266
|
+
params.envKey === 'TRACE_DB_CONNECTION'
|
|
267
|
+
? 'Configure TRACE_DB_CONNECTION to an existing database connection before enabling TRACE_ENABLED.'
|
|
268
|
+
: 'Configure TRACE_QUERY_CONNECTION, or ensure DB_CONNECTION resolves to an existing database connection.',
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const assertTraceStorageReady = async (
|
|
274
|
+
coreApi: CoreApi,
|
|
275
|
+
db: CoreDatabase,
|
|
276
|
+
connectionName: string
|
|
277
|
+
): Promise<void> => {
|
|
278
|
+
try {
|
|
279
|
+
await Promise.all(
|
|
280
|
+
TRACE_REQUIRED_TABLES.map(async (table) => {
|
|
281
|
+
await db.queryOne(`SELECT 1 AS ok FROM ${table} LIMIT 1`, []);
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw createTraceConfigError(
|
|
286
|
+
coreApi,
|
|
287
|
+
`Trace storage connection "${connectionName}" is not ready. Create the database if needed and run \`zin migrate:trace\` before enabling TRACE_ENABLED.`,
|
|
288
|
+
{
|
|
289
|
+
connectionName,
|
|
290
|
+
error,
|
|
291
|
+
requiredTables: [...TRACE_REQUIRED_TABLES],
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
220
297
|
const core = (await importCore()) as CoreApi;
|
|
221
298
|
const Env = core.Env;
|
|
222
299
|
const startupOverrides = resolveTraceStartupOverrides(core);
|
|
@@ -292,98 +369,105 @@ if (!traceAlreadyInitialized && Env) {
|
|
|
292
369
|
const storageDb = core.useDatabase?.(undefined, resolvedConnectionName);
|
|
293
370
|
const observedDb = core.useDatabase?.(undefined, resolvedObservedConnectionName);
|
|
294
371
|
|
|
295
|
-
|
|
296
|
-
|
|
372
|
+
assertTraceConnectionResolved(core, storageDb, {
|
|
373
|
+
connectionName: resolvedConnectionName,
|
|
374
|
+
envKey: 'TRACE_DB_CONNECTION',
|
|
375
|
+
});
|
|
376
|
+
assertTraceConnectionResolved(core, observedDb, {
|
|
377
|
+
connectionName: resolvedObservedConnectionName,
|
|
378
|
+
envKey: 'TRACE_QUERY_CONNECTION',
|
|
379
|
+
});
|
|
380
|
+
await assertTraceStorageReady(core, storageDb, resolvedConnectionName);
|
|
381
|
+
|
|
382
|
+
const storage = TraceWriteDiagnostics.wrapStorage(
|
|
383
|
+
TraceContentBudget.wrapStorage(
|
|
297
384
|
TraceContentRedaction.wrapStorage(
|
|
298
385
|
TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config),
|
|
299
386
|
config.redaction
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
if (core.RequestContext) {
|
|
308
|
-
TraceContext.setRequestContextImpl(
|
|
309
|
-
core.RequestContext as {
|
|
310
|
-
current?: () => unknown;
|
|
311
|
-
peek?: () => unknown;
|
|
312
|
-
}
|
|
313
|
-
);
|
|
387
|
+
)
|
|
388
|
+
),
|
|
389
|
+
{
|
|
390
|
+
connectionName: resolvedConnectionName,
|
|
391
|
+
logger: core.Logger,
|
|
314
392
|
}
|
|
393
|
+
);
|
|
315
394
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
{ ScheduleWatcher },
|
|
324
|
-
{ MailWatcher },
|
|
325
|
-
{ AuthWatcher },
|
|
326
|
-
{ EventWatcher },
|
|
327
|
-
{ ModelWatcher },
|
|
328
|
-
{ NotificationWatcher },
|
|
329
|
-
{ RedisWatcher },
|
|
330
|
-
{ GateWatcher },
|
|
331
|
-
{ MiddlewareWatcher },
|
|
332
|
-
{ CommandWatcher },
|
|
333
|
-
{ BatchWatcher },
|
|
334
|
-
{ DumpWatcher },
|
|
335
|
-
{ ViewWatcher },
|
|
336
|
-
{ HttpClientWatcher },
|
|
337
|
-
] = await Promise.all([
|
|
338
|
-
import('./watchers/HttpWatcher'),
|
|
339
|
-
import('./watchers/QueryWatcher'),
|
|
340
|
-
import('./watchers/LogWatcher'),
|
|
341
|
-
import('./watchers/ExceptionWatcher'),
|
|
342
|
-
import('./watchers/JobWatcher'),
|
|
343
|
-
import('./watchers/CacheWatcher'),
|
|
344
|
-
import('./watchers/ScheduleWatcher'),
|
|
345
|
-
import('./watchers/MailWatcher'),
|
|
346
|
-
import('./watchers/AuthWatcher'),
|
|
347
|
-
import('./watchers/EventWatcher'),
|
|
348
|
-
import('./watchers/ModelWatcher'),
|
|
349
|
-
import('./watchers/NotificationWatcher'),
|
|
350
|
-
import('./watchers/RedisWatcher'),
|
|
351
|
-
import('./watchers/GateWatcher'),
|
|
352
|
-
import('./watchers/MiddlewareWatcher'),
|
|
353
|
-
import('./watchers/CommandWatcher'),
|
|
354
|
-
import('./watchers/BatchWatcher'),
|
|
355
|
-
import('./watchers/DumpWatcher'),
|
|
356
|
-
import('./watchers/ViewWatcher'),
|
|
357
|
-
import('./watchers/HttpClientWatcher'),
|
|
358
|
-
]);
|
|
359
|
-
|
|
360
|
-
const watcherArgs = { storage, config, db: observedDb };
|
|
361
|
-
|
|
362
|
-
HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
|
|
363
|
-
|
|
364
|
-
QueryWatcher.register(watcherArgs);
|
|
365
|
-
LogWatcher.register(watcherArgs);
|
|
366
|
-
ExceptionWatcher.register(watcherArgs);
|
|
367
|
-
JobWatcher.register(watcherArgs);
|
|
368
|
-
CacheWatcher.register(watcherArgs);
|
|
369
|
-
ScheduleWatcher.register(watcherArgs);
|
|
370
|
-
MailWatcher.register(watcherArgs);
|
|
371
|
-
AuthWatcher.register(watcherArgs);
|
|
372
|
-
EventWatcher.register(watcherArgs);
|
|
373
|
-
ModelWatcher.register(watcherArgs);
|
|
374
|
-
NotificationWatcher.register(watcherArgs);
|
|
375
|
-
RedisWatcher.register(watcherArgs);
|
|
376
|
-
GateWatcher.register(watcherArgs);
|
|
377
|
-
MiddlewareWatcher.register(watcherArgs);
|
|
378
|
-
CommandWatcher.register(watcherArgs);
|
|
379
|
-
BatchWatcher.register(watcherArgs);
|
|
380
|
-
DumpWatcher.register(watcherArgs);
|
|
381
|
-
ViewWatcher.register(watcherArgs);
|
|
382
|
-
HttpClientWatcher.register(watcherArgs);
|
|
383
|
-
} else {
|
|
384
|
-
// eslint-disable-next-line no-console
|
|
385
|
-
console.warn('[trace] Could not resolve database connection - skipping init.');
|
|
395
|
+
if (core.RequestContext) {
|
|
396
|
+
TraceContext.setRequestContextImpl(
|
|
397
|
+
core.RequestContext as {
|
|
398
|
+
current?: () => unknown;
|
|
399
|
+
peek?: () => unknown;
|
|
400
|
+
}
|
|
401
|
+
);
|
|
386
402
|
}
|
|
403
|
+
|
|
404
|
+
const [
|
|
405
|
+
{ HttpWatcher },
|
|
406
|
+
{ QueryWatcher },
|
|
407
|
+
{ LogWatcher },
|
|
408
|
+
{ ExceptionWatcher },
|
|
409
|
+
{ JobWatcher },
|
|
410
|
+
{ CacheWatcher },
|
|
411
|
+
{ ScheduleWatcher },
|
|
412
|
+
{ MailWatcher },
|
|
413
|
+
{ AuthWatcher },
|
|
414
|
+
{ EventWatcher },
|
|
415
|
+
{ ModelWatcher },
|
|
416
|
+
{ NotificationWatcher },
|
|
417
|
+
{ RedisWatcher },
|
|
418
|
+
{ GateWatcher },
|
|
419
|
+
{ MiddlewareWatcher },
|
|
420
|
+
{ CommandWatcher },
|
|
421
|
+
{ BatchWatcher },
|
|
422
|
+
{ DumpWatcher },
|
|
423
|
+
{ ViewWatcher },
|
|
424
|
+
{ HttpClientWatcher },
|
|
425
|
+
] = await Promise.all([
|
|
426
|
+
import('./watchers/HttpWatcher'),
|
|
427
|
+
import('./watchers/QueryWatcher'),
|
|
428
|
+
import('./watchers/LogWatcher'),
|
|
429
|
+
import('./watchers/ExceptionWatcher'),
|
|
430
|
+
import('./watchers/JobWatcher'),
|
|
431
|
+
import('./watchers/CacheWatcher'),
|
|
432
|
+
import('./watchers/ScheduleWatcher'),
|
|
433
|
+
import('./watchers/MailWatcher'),
|
|
434
|
+
import('./watchers/AuthWatcher'),
|
|
435
|
+
import('./watchers/EventWatcher'),
|
|
436
|
+
import('./watchers/ModelWatcher'),
|
|
437
|
+
import('./watchers/NotificationWatcher'),
|
|
438
|
+
import('./watchers/RedisWatcher'),
|
|
439
|
+
import('./watchers/GateWatcher'),
|
|
440
|
+
import('./watchers/MiddlewareWatcher'),
|
|
441
|
+
import('./watchers/CommandWatcher'),
|
|
442
|
+
import('./watchers/BatchWatcher'),
|
|
443
|
+
import('./watchers/DumpWatcher'),
|
|
444
|
+
import('./watchers/ViewWatcher'),
|
|
445
|
+
import('./watchers/HttpClientWatcher'),
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
const watcherArgs = { storage, config, db: observedDb };
|
|
449
|
+
|
|
450
|
+
HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
|
|
451
|
+
|
|
452
|
+
QueryWatcher.register(watcherArgs);
|
|
453
|
+
LogWatcher.register(watcherArgs);
|
|
454
|
+
ExceptionWatcher.register(watcherArgs);
|
|
455
|
+
JobWatcher.register(watcherArgs);
|
|
456
|
+
CacheWatcher.register(watcherArgs);
|
|
457
|
+
ScheduleWatcher.register(watcherArgs);
|
|
458
|
+
MailWatcher.register(watcherArgs);
|
|
459
|
+
AuthWatcher.register(watcherArgs);
|
|
460
|
+
EventWatcher.register(watcherArgs);
|
|
461
|
+
ModelWatcher.register(watcherArgs);
|
|
462
|
+
NotificationWatcher.register(watcherArgs);
|
|
463
|
+
RedisWatcher.register(watcherArgs);
|
|
464
|
+
GateWatcher.register(watcherArgs);
|
|
465
|
+
MiddlewareWatcher.register(watcherArgs);
|
|
466
|
+
CommandWatcher.register(watcherArgs);
|
|
467
|
+
BatchWatcher.register(watcherArgs);
|
|
468
|
+
DumpWatcher.register(watcherArgs);
|
|
469
|
+
ViewWatcher.register(watcherArgs);
|
|
470
|
+
HttpClientWatcher.register(watcherArgs);
|
|
387
471
|
}
|
|
388
472
|
} else if (!traceAlreadyInitialized) {
|
|
389
473
|
// Running outside a ZinTrust project - skip init silently.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ITraceEntry, ITraceStorage } from '../types';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_ENTRY_BYTES = 64 * 1024;
|
|
4
|
+
const DEFAULT_MAX_STRING_BYTES = 16 * 1024;
|
|
5
|
+
const DEFAULT_MAX_ARRAY_ITEMS = 25;
|
|
6
|
+
const DEFAULT_MAX_OBJECT_ENTRIES = 40;
|
|
7
|
+
const DEFAULT_MAX_DEPTH = 6;
|
|
8
|
+
|
|
9
|
+
const DROPPED_FIELD_MESSAGE =
|
|
10
|
+
'[trace] Value dropped because the field exceeded the trace storage size limit.';
|
|
11
|
+
const COMPACTED_CONTENT_MESSAGE =
|
|
12
|
+
'[trace] Trace content was compacted because it exceeded the trace storage size limit.';
|
|
13
|
+
|
|
14
|
+
const encoder = new TextEncoder();
|
|
15
|
+
|
|
16
|
+
const serializedSize = (value: unknown): number => {
|
|
17
|
+
try {
|
|
18
|
+
return encoder.encode(JSON.stringify(value)).length;
|
|
19
|
+
} catch {
|
|
20
|
+
return Number.MAX_SAFE_INTEGER;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const describeValueType = (value: unknown): string => {
|
|
25
|
+
if (Array.isArray(value)) return 'array';
|
|
26
|
+
if (value === null) return 'null';
|
|
27
|
+
return typeof value;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const compactValue = (value: unknown, depth: number): unknown => {
|
|
31
|
+
if (depth >= DEFAULT_MAX_DEPTH) {
|
|
32
|
+
return DROPPED_FIELD_MESSAGE;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof value === 'string') {
|
|
36
|
+
return serializedSize(value) > DEFAULT_MAX_STRING_BYTES ? DROPPED_FIELD_MESSAGE : value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
const next = value
|
|
41
|
+
.slice(0, DEFAULT_MAX_ARRAY_ITEMS)
|
|
42
|
+
.map((item) => compactValue(item, depth + 1));
|
|
43
|
+
|
|
44
|
+
if (value.length > DEFAULT_MAX_ARRAY_ITEMS) {
|
|
45
|
+
next.push(
|
|
46
|
+
`[trace] ${String(value.length - DEFAULT_MAX_ARRAY_ITEMS)} additional items were dropped.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof value !== 'object' || value === null) {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const entries = Object.entries(value);
|
|
58
|
+
const compactedEntries = entries
|
|
59
|
+
.slice(0, DEFAULT_MAX_OBJECT_ENTRIES)
|
|
60
|
+
.map(([key, entryValue]) => [key, compactValue(entryValue, depth + 1)]);
|
|
61
|
+
|
|
62
|
+
if (entries.length > DEFAULT_MAX_OBJECT_ENTRIES) {
|
|
63
|
+
compactedEntries.push([
|
|
64
|
+
'__traceNotice',
|
|
65
|
+
`[trace] ${String(entries.length - DEFAULT_MAX_OBJECT_ENTRIES)} additional fields were dropped.`,
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Object.fromEntries(compactedEntries);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const compactTopLevelObjectToBudget = (value: Record<string, unknown>): Record<string, unknown> => {
|
|
73
|
+
const compacted: Record<string, unknown> = {
|
|
74
|
+
...value,
|
|
75
|
+
__traceNotice: COMPACTED_CONTENT_MESSAGE,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const keysByDescendingSize = Object.keys(compacted)
|
|
79
|
+
.filter((key) => key !== '__traceNotice')
|
|
80
|
+
.sort((left, right) => serializedSize(compacted[right]) - serializedSize(compacted[left]));
|
|
81
|
+
|
|
82
|
+
for (const key of keysByDescendingSize) {
|
|
83
|
+
if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) break;
|
|
84
|
+
compacted[key] = DROPPED_FIELD_MESSAGE;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return compacted;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const fitContentToBudget = (content: unknown): unknown => {
|
|
91
|
+
if (serializedSize(content) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
92
|
+
return content;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const compacted = compactValue(content, 0);
|
|
96
|
+
if (serializedSize(compacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
97
|
+
return compacted;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof compacted === 'object' && compacted !== null && !Array.isArray(compacted)) {
|
|
101
|
+
const topLevelCompacted = compactTopLevelObjectToBudget(compacted as Record<string, unknown>);
|
|
102
|
+
if (serializedSize(topLevelCompacted) <= DEFAULT_MAX_ENTRY_BYTES) {
|
|
103
|
+
return topLevelCompacted;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
__traceNotice: COMPACTED_CONTENT_MESSAGE,
|
|
109
|
+
dropped: true,
|
|
110
|
+
valueType: describeValueType(content),
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const fitEntryToBudget = (entry: ITraceEntry): ITraceEntry => ({
|
|
115
|
+
...entry,
|
|
116
|
+
content: fitContentToBudget(entry.content),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const fitPatchToBudget = (
|
|
120
|
+
patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
|
|
121
|
+
): Partial<Pick<ITraceEntry, 'content' | 'isLatest'>> => {
|
|
122
|
+
if (patch.content === undefined) return patch;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
...patch,
|
|
126
|
+
content: fitContentToBudget(patch.content),
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const TraceContentBudget = Object.freeze({
|
|
131
|
+
wrapStorage(storage: ITraceStorage): ITraceStorage {
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
...storage,
|
|
134
|
+
writeEntry: async (entry: ITraceEntry): Promise<void> => {
|
|
135
|
+
await storage.writeEntry(fitEntryToBudget(entry));
|
|
136
|
+
},
|
|
137
|
+
updateEntry: async (
|
|
138
|
+
uuid: string,
|
|
139
|
+
patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
|
|
140
|
+
): Promise<void> => {
|
|
141
|
+
await storage.updateEntry(uuid, fitPatchToBudget(patch));
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
});
|
|
@@ -21,8 +21,50 @@ const TRACE_INFRASTRUCTURE_LOG_MESSAGES = new Set<string>([
|
|
|
21
21
|
'[trace] Trace storage write degraded',
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const TRACE_STORAGE_TABLE_NAMES = [
|
|
25
|
+
'zin_trace_entries',
|
|
26
|
+
'zin_trace_entries_tags',
|
|
27
|
+
'zin_trace_monitoring',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const isTraceStorageQuery = (sql: string): boolean => {
|
|
31
|
+
const normalized = sql.toLowerCase();
|
|
32
|
+
return TRACE_STORAGE_TABLE_NAMES.some((tableName) => normalized.includes(tableName));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const extractSqlFromLog = (
|
|
36
|
+
message: string,
|
|
37
|
+
context?: Record<string, unknown>
|
|
38
|
+
): string | undefined => {
|
|
39
|
+
const contextSql = context?.['sql'];
|
|
40
|
+
if (typeof contextSql === 'string') return contextSql;
|
|
41
|
+
|
|
42
|
+
const trimmed = message.trim();
|
|
43
|
+
const rawPrefix = 'Raw SQL Query executed:';
|
|
44
|
+
if (trimmed.startsWith(rawPrefix)) {
|
|
45
|
+
const sql = trimmed.slice(rawPrefix.length).trim();
|
|
46
|
+
return sql === '' ? undefined : sql;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return undefined;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const isTraceStorageQueryLog = (message: string, context?: Record<string, unknown>): boolean => {
|
|
53
|
+
const normalizedMessage = message.trim().toLowerCase();
|
|
54
|
+
if (!normalizedMessage.includes('query executed')) return false;
|
|
55
|
+
|
|
56
|
+
const sql = extractSqlFromLog(message, context);
|
|
57
|
+
return typeof sql === 'string' && isTraceStorageQuery(sql);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const shouldSkipTraceInfrastructureLog = (
|
|
61
|
+
message: string,
|
|
62
|
+
context?: Record<string, unknown>
|
|
63
|
+
): boolean => {
|
|
64
|
+
return (
|
|
65
|
+
TRACE_INFRASTRUCTURE_LOG_MESSAGES.has(message.trim()) ||
|
|
66
|
+
isTraceStorageQueryLog(message, context)
|
|
67
|
+
);
|
|
26
68
|
};
|
|
27
69
|
|
|
28
70
|
export const LogWatcher: ITraceWatcher = Object.freeze({
|
|
@@ -41,7 +83,7 @@ export const LogWatcher: ITraceWatcher = Object.freeze({
|
|
|
41
83
|
(level: string, message: string, context?: Record<string, unknown>) => {
|
|
42
84
|
if ((LEVEL_PRIORITY[level] ?? 0) < minPriority) return;
|
|
43
85
|
if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes)) return;
|
|
44
|
-
if (shouldSkipTraceInfrastructureLog(message)) return;
|
|
86
|
+
if (shouldSkipTraceInfrastructureLog(message, context)) return;
|
|
45
87
|
|
|
46
88
|
const content: LogContent = {
|
|
47
89
|
level,
|
|
@@ -24,7 +24,11 @@ const bindingsInterpolated = (sql: string, params: unknown[]): string => {
|
|
|
24
24
|
|
|
25
25
|
const isTraceStorageQuery = (sql: string): boolean => {
|
|
26
26
|
const normalized = sql.toLowerCase();
|
|
27
|
-
return
|
|
27
|
+
return (
|
|
28
|
+
normalized.includes('zin_trace_entries') ||
|
|
29
|
+
normalized.includes('zin_trace_entries_tags') ||
|
|
30
|
+
normalized.includes('zin_trace_monitoring')
|
|
31
|
+
);
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
const emit = (query: string, params: unknown[], duration: number, connection = 'default'): void => {
|