@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.
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@zintrust/trace",
3
- "version": "0.4.86",
4
- "buildDate": "2026-04-09T08:08:55.865Z",
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": "a456e7ec",
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": 2470,
74
- "sha256": "99c28d43f79dbb2b372bf6a8b611841c131f59f5066702b499915b874e9fa2b8"
73
+ "size": 2537,
74
+ "sha256": "1707d26322dbad17f6bf85938ae6fe2477e84c7fed3333760ce8d6eadfaaffd2"
75
75
  },
76
76
  "index.js": {
77
- "size": 3255,
78
- "sha256": "faba7327d333f46340e9f6933510a88cb2439d14af9651cd8868a38782a44388"
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": 12522,
134
- "sha256": "66bccec7fac4d669702fa18e439788325d36ce17f228639005ffcde2cd968d70"
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": 2026,
334
- "sha256": "c5d2227cd76ce10162993ac31f474b2460cd41264c36f01b5130152f14a0ad21"
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": 2935,
374
- "sha256": "577c6fec0282d2290db5c4b6c606b9b6ecdd64209af2b09f3205a15bf656bbef"
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, undefined);
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
- if (storageDb && observedDb) {
209
- const storage = TraceWriteDiagnostics.wrapStorage(TraceContentRedaction.wrapStorage(TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config), config.redaction), {
210
- connectionName: resolvedConnectionName,
211
- logger: core.Logger,
212
- });
213
- if (core.RequestContext) {
214
- TraceContext.setRequestContextImpl(core.RequestContext);
215
- }
216
- const [{ HttpWatcher }, { QueryWatcher }, { LogWatcher }, { ExceptionWatcher }, { JobWatcher }, { CacheWatcher }, { ScheduleWatcher }, { MailWatcher }, { AuthWatcher }, { EventWatcher }, { ModelWatcher }, { NotificationWatcher }, { RedisWatcher }, { GateWatcher }, { MiddlewareWatcher }, { CommandWatcher }, { BatchWatcher }, { DumpWatcher }, { ViewWatcher }, { HttpClientWatcher },] = await Promise.all([
217
- import('./watchers/HttpWatcher.js'),
218
- import('./watchers/QueryWatcher.js'),
219
- import('./watchers/LogWatcher.js'),
220
- import('./watchers/ExceptionWatcher.js'),
221
- import('./watchers/JobWatcher.js'),
222
- import('./watchers/CacheWatcher.js'),
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,4 @@
1
+ import type { ITraceStorage } from '../types';
2
+ export declare const TraceContentBudget: Readonly<{
3
+ wrapStorage(storage: ITraceStorage): ITraceStorage;
4
+ }>;
@@ -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 shouldSkipTraceInfrastructureLog = (message) => {
21
- return TRACE_INFRASTRUCTURE_LOG_MESSAGES.has(message.trim());
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') || normalized.includes('zin_trace_monitoring');
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.86",
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.84"
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, undefined);
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
- if (storageDb && observedDb) {
296
- const storage = TraceWriteDiagnostics.wrapStorage(
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
- connectionName: resolvedConnectionName,
303
- logger: core.Logger,
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
- const [
317
- { HttpWatcher },
318
- { QueryWatcher },
319
- { LogWatcher },
320
- { ExceptionWatcher },
321
- { JobWatcher },
322
- { CacheWatcher },
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 shouldSkipTraceInfrastructureLog = (message: string): boolean => {
25
- return TRACE_INFRASTRUCTURE_LOG_MESSAGES.has(message.trim());
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 normalized.includes('zin_trace_entries') || normalized.includes('zin_trace_monitoring');
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 => {