@zintrust/trace 0.4.81 → 0.4.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/register.ts CHANGED
@@ -113,6 +113,26 @@ const resolveTraceConnectionName = (
113
113
  return resolveDefaultConnection();
114
114
  };
115
115
 
116
+ const resolveObservedConnectionName = (
117
+ env: Pick<NonNullable<CoreApi['Env']>, 'get'> | undefined,
118
+ configuredObservedConnection: string | undefined,
119
+ storageConnectionName: string
120
+ ): string => {
121
+ if (
122
+ typeof configuredObservedConnection === 'string' &&
123
+ configuredObservedConnection.trim() !== ''
124
+ ) {
125
+ return resolveTraceConnectionName(env, configuredObservedConnection);
126
+ }
127
+
128
+ const defaultConnectionName = resolveTraceConnectionName(env, undefined);
129
+ if (storageConnectionName !== defaultConnectionName) {
130
+ return defaultConnectionName;
131
+ }
132
+
133
+ return storageConnectionName;
134
+ };
135
+
116
136
  const isObjectValue = (value: unknown): value is Record<string, unknown> => {
117
137
  return typeof value === 'object' && value !== null && !Array.isArray(value);
118
138
  };
@@ -141,6 +161,14 @@ const parseEnvList = (rawValue: string): string[] | undefined => {
141
161
  .filter((entry) => entry !== '');
142
162
  };
143
163
 
164
+ const parseEnvBool = (rawValue: string): boolean | undefined => {
165
+ const value = rawValue.trim().toLowerCase();
166
+ if (value === '') return undefined;
167
+ if (['1', 'true', 'yes', 'on'].includes(value)) return true;
168
+ if (['0', 'false', 'no', 'off'].includes(value)) return false;
169
+ return undefined;
170
+ };
171
+
144
172
  const resolveTraceStartupOverrides = (core: CoreApi): TraceConfigOverrides | undefined => {
145
173
  const traceConfigFile = core.StartupConfigFile?.Trace;
146
174
  if (typeof traceConfigFile !== 'string' || traceConfigFile.trim() === '') return undefined;
@@ -198,15 +226,20 @@ if (!traceAlreadyInitialized && Env) {
198
226
 
199
227
  if (enabled) {
200
228
  const connectionRaw = Env.get('TRACE_DB_CONNECTION', '').trim();
229
+ const observeConnectionRaw = Env.get('TRACE_QUERY_CONNECTION', '').trim();
201
230
  const pruneAfterHoursRaw = Env.get('TRACE_PRUNE_HOURS', '').trim();
202
231
  const slowQueryThresholdRaw = Env.get('TRACE_SLOW_QUERY_MS', '').trim();
203
232
  const logMinLevelRaw = Env.get('TRACE_LOG_LEVEL', '').trim();
233
+ const captureCachePayloadsRaw = Env.get('TRACE_CACHE_PAYLOADS', '').trim();
234
+ const captureQueryBindingsRaw = Env.get('TRACE_QUERY_BINDINGS', '').trim();
204
235
  const redactionKeys = parseEnvList(Env.get('TRACE_REDACT_KEYS', ''));
205
236
  const redactionHeaders = parseEnvList(Env.get('TRACE_REDACT_HEADERS', ''));
206
237
  const redactionBody = parseEnvList(Env.get('TRACE_REDACT_BODY', ''));
207
238
  const redactionQuery = parseEnvList(Env.get('TRACE_REDACT_QUERY', ''));
208
239
 
209
240
  const connection = connectionRaw === '' ? startupOverrides?.connection : connectionRaw;
241
+ const observeConnection =
242
+ observeConnectionRaw === '' ? startupOverrides?.observeConnection : observeConnectionRaw;
210
243
  const pruneAfterHours =
211
244
  pruneAfterHoursRaw === ''
212
245
  ? startupOverrides?.pruneAfterHours
@@ -221,6 +254,10 @@ if (!traceAlreadyInitialized && Env) {
221
254
  | 'warn'
222
255
  | 'error'
223
256
  | 'fatal';
257
+ const captureCachePayloads =
258
+ parseEnvBool(captureCachePayloadsRaw) ?? startupOverrides?.captureCachePayloads;
259
+ const captureQueryBindings =
260
+ parseEnvBool(captureQueryBindingsRaw) ?? startupOverrides?.captureQueryBindings;
224
261
  const redaction = buildTraceRedactionOverrides({
225
262
  startupOverrides,
226
263
  redactionBody,
@@ -233,23 +270,32 @@ if (!traceAlreadyInitialized && Env) {
233
270
  ...startupOverrides,
234
271
  enabled,
235
272
  connection,
273
+ observeConnection,
236
274
  ...(typeof pruneAfterHours === 'number' && Number.isFinite(pruneAfterHours)
237
275
  ? { pruneAfterHours }
238
276
  : {}),
239
277
  ...(typeof slowQueryThreshold === 'number' && Number.isFinite(slowQueryThreshold)
240
278
  ? { slowQueryThreshold }
241
279
  : {}),
280
+ ...(typeof captureCachePayloads === 'boolean' ? { captureCachePayloads } : {}),
281
+ ...(typeof captureQueryBindings === 'boolean' ? { captureQueryBindings } : {}),
242
282
  logMinLevel,
243
283
  ...(redaction === undefined ? {} : { redaction }),
244
284
  });
245
285
 
246
286
  const resolvedConnectionName = resolveTraceConnectionName(Env, config.connection);
247
- const db = core.useDatabase?.(undefined, resolvedConnectionName);
248
-
249
- if (db) {
287
+ const resolvedObservedConnectionName = resolveObservedConnectionName(
288
+ Env,
289
+ config.observeConnection,
290
+ resolvedConnectionName
291
+ );
292
+ const storageDb = core.useDatabase?.(undefined, resolvedConnectionName);
293
+ const observedDb = core.useDatabase?.(undefined, resolvedObservedConnectionName);
294
+
295
+ if (storageDb && observedDb) {
250
296
  const storage = TraceWriteDiagnostics.wrapStorage(
251
297
  TraceContentRedaction.wrapStorage(
252
- TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(db), config),
298
+ TraceEntryFiltering.wrapStorage(TraceStorage.resolveStorage(storageDb), config),
253
299
  config.redaction
254
300
  ),
255
301
  {
@@ -311,7 +357,7 @@ if (!traceAlreadyInitialized && Env) {
311
357
  import('./watchers/HttpClientWatcher'),
312
358
  ]);
313
359
 
314
- const watcherArgs = { storage, config, db };
360
+ const watcherArgs = { storage, config, db: observedDb };
315
361
 
316
362
  HttpWatcher.register({ ...watcherArgs, registerMiddleware: resolveRegisterMiddleware() });
317
363
 
package/src/types.ts CHANGED
@@ -55,6 +55,9 @@ export interface RequestContent {
55
55
  export interface QueryContent {
56
56
  connection: string;
57
57
  sql: string;
58
+ statement?: string;
59
+ bindings?: unknown[];
60
+ bindingsIncluded?: boolean;
58
61
  time: number;
59
62
  duration: number;
60
63
  slow: boolean;
@@ -97,6 +100,10 @@ export interface CacheContent {
97
100
  operation: 'get' | 'set' | 'delete' | 'clear' | 'has';
98
101
  key: string;
99
102
  hit?: boolean;
103
+ store?: string;
104
+ payload?: unknown;
105
+ payloadLogged?: boolean;
106
+ ttl?: number;
100
107
  duration: number;
101
108
  hostname: string;
102
109
  }
@@ -114,6 +121,8 @@ export interface MailContent {
114
121
  to: string;
115
122
  subject: string;
116
123
  template?: string;
124
+ text?: string;
125
+ html?: string;
117
126
  hostname: string;
118
127
  }
119
128
 
@@ -142,6 +151,8 @@ export interface NotificationContent {
142
151
  channels: string[];
143
152
  notifiable?: string;
144
153
  notification: string;
154
+ message?: string;
155
+ payload?: unknown;
145
156
  hostname: string;
146
157
  }
147
158
 
@@ -201,11 +212,27 @@ export interface ClientRequestContent {
201
212
  method: string;
202
213
  url: string;
203
214
  requestHeaders: Record<string, string>;
204
- responseStatus: number;
215
+ requestBody?: unknown;
216
+ responseStatus?: number;
217
+ responseHeaders?: Record<string, string>;
218
+ responseBody?: unknown;
219
+ error?: string;
205
220
  duration: number;
206
221
  hostname: string;
207
222
  }
208
223
 
224
+ export interface ClientRequestTraceInput {
225
+ method: string;
226
+ url: string;
227
+ requestHeaders: Record<string, string>;
228
+ responseStatus?: number;
229
+ duration: number;
230
+ requestBody?: unknown;
231
+ responseHeaders?: Record<string, string>;
232
+ responseBody?: unknown;
233
+ error?: string;
234
+ }
235
+
209
236
  // ---------------------------------------------------------------------------
210
237
  // Core domain records
211
238
  // ---------------------------------------------------------------------------
@@ -326,9 +353,12 @@ export type WatcherToggles = {
326
353
  export interface ITraceConfig {
327
354
  enabled: boolean;
328
355
  connection?: string;
356
+ observeConnection?: string;
329
357
  pruneAfterHours: number;
330
358
  ignoreRoutes: string[];
331
359
  slowQueryThreshold: number;
360
+ captureCachePayloads: boolean;
361
+ captureQueryBindings: boolean;
332
362
  logMinLevel: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
333
363
  watchers: WatcherToggles;
334
364
  redaction: RedactionConfig;
@@ -6,10 +6,11 @@ import { TraceContext } from '../context';
6
6
  import type { CacheContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
7
7
  import { EntryType } from '../types';
8
8
  import { AuthTag } from '../utils/authTag';
9
- import { redactString } from '../utils/redact';
9
+ import { redactString, redactUnknown } from '../utils/redact';
10
10
  import { RequestFilter } from '../utils/requestFilter';
11
11
 
12
12
  let _storage: ITraceWatcherConfig['storage'] | null = null;
13
+ let _config: ITraceWatcherConfig['config'] | null = null;
13
14
  let _redactionFields: string[] = [];
14
15
  let _ignoreRoutes: string[] = [];
15
16
 
@@ -17,15 +18,23 @@ const emit = (
17
18
  operation: CacheContent['operation'],
18
19
  key: string,
19
20
  duration: number,
20
- hit?: boolean
21
+ hit?: boolean,
22
+ payload?: unknown,
23
+ store?: string,
24
+ ttl?: number
21
25
  ): void => {
22
26
  if (!_storage) return;
23
27
  if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
24
28
  const safeKey = redactString(key, _redactionFields);
29
+ const shouldLogPayload = _config?.captureCachePayloads === true;
25
30
  const content: CacheContent = {
26
31
  operation,
27
32
  key: safeKey,
28
33
  hit,
34
+ ...(typeof store === 'string' && store !== '' ? { store } : {}),
35
+ ...(typeof ttl === 'number' ? { ttl } : {}),
36
+ payloadLogged: shouldLogPayload,
37
+ ...(shouldLogPayload ? { payload: redactUnknown(payload, _redactionFields) } : {}),
29
38
  duration,
30
39
  hostname: TraceContext.getHostname(),
31
40
  };
@@ -48,10 +57,12 @@ export const CacheWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze
48
57
  register({ storage, config }: ITraceWatcherConfig): () => void {
49
58
  if (config.watchers.cache === false) return () => undefined;
50
59
  _storage = storage;
60
+ _config = config;
51
61
  _redactionFields = config.redaction.query;
52
62
  _ignoreRoutes = config.ignoreRoutes;
53
63
  return () => {
54
64
  _storage = null;
65
+ _config = null;
55
66
  _ignoreRoutes = [];
56
67
  };
57
68
  },
@@ -1,30 +1,50 @@
1
1
  import { TraceContext } from '../context';
2
- import type { ClientRequestContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
2
+ import type {
3
+ ClientRequestContent,
4
+ ClientRequestTraceInput,
5
+ ITraceWatcher,
6
+ ITraceWatcherConfig,
7
+ } from '../types';
3
8
  import { EntryType } from '../types';
4
9
  import { AuthTag } from '../utils/authTag';
5
- import { redactHeaders } from '../utils/redact';
10
+ import { redactHeaders, redactUnknown } from '../utils/redact';
6
11
  import { RequestFilter } from '../utils/requestFilter';
7
12
 
8
13
  let _storage: ITraceWatcherConfig['storage'] | null = null;
9
14
  let _redactHeaderNames: string[] = [];
15
+ let _redactBodyFields: string[] = [];
10
16
  let _ignoreRoutes: string[] = [];
11
17
 
12
- const emit = (
13
- method: string,
14
- url: string,
15
- requestHeaders: Record<string, string>,
16
- responseStatus: number,
17
- duration: number
18
- ): void => {
18
+ const emit = ({
19
+ method,
20
+ url,
21
+ requestHeaders,
22
+ responseStatus,
23
+ duration,
24
+ requestBody,
25
+ responseHeaders,
26
+ responseBody,
27
+ error,
28
+ }: ClientRequestTraceInput): void => {
19
29
  if (!_storage) return;
20
30
  if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
21
31
  const tags = AuthTag.append([method.toUpperCase()]);
22
- if (responseStatus >= 400) tags.push('failed');
32
+ if ((responseStatus ?? 0) >= 400 || error) tags.push('failed');
23
33
  const content: ClientRequestContent = {
24
34
  method: method.toUpperCase(),
25
35
  url,
26
36
  requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames),
27
- responseStatus,
37
+ ...(requestBody === undefined
38
+ ? {}
39
+ : { requestBody: redactUnknown(requestBody, _redactBodyFields) }),
40
+ ...(responseStatus === undefined ? {} : { responseStatus }),
41
+ ...(responseHeaders === undefined
42
+ ? {}
43
+ : { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) }),
44
+ ...(responseBody === undefined
45
+ ? {}
46
+ : { responseBody: redactUnknown(responseBody, _redactBodyFields) }),
47
+ ...(typeof error === 'string' && error !== '' ? { error } : {}),
28
48
  duration,
29
49
  hostname: TraceContext.getHostname(),
30
50
  };
@@ -47,9 +67,11 @@ export const HttpClientWatcher: ITraceWatcher & { emit: typeof emit } = Object.f
47
67
  if (config.watchers.clientRequest === false) return () => undefined;
48
68
  _storage = storage;
49
69
  _redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
70
+ _redactBodyFields = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
50
71
  _ignoreRoutes = config.ignoreRoutes;
51
72
  return () => {
52
73
  _storage = null;
74
+ _redactBodyFields = [];
53
75
  _ignoreRoutes = [];
54
76
  };
55
77
  },
@@ -1,22 +1,35 @@
1
1
  /**
2
- * MailWatcher — records mail dispatch intent.
3
- * Body is never captured; only to/subject/template.
2
+ * MailWatcher — records mail dispatch intent and rendered content.
4
3
  */
5
4
  import { TraceContext } from '../context';
6
5
  import type { ITraceWatcher, ITraceWatcherConfig, MailContent } from '../types';
7
6
  import { EntryType } from '../types';
7
+ import { redactUnknown } from '../utils/redact';
8
8
  import { RequestFilter } from '../utils/requestFilter';
9
9
 
10
10
  let _storage: ITraceWatcherConfig['storage'] | null = null;
11
+ let _redactionFields: string[] = [];
11
12
  let _ignoreRoutes: string[] = [];
12
13
 
13
- const emit = (to: string, subject: string, template?: string): void => {
14
+ const emit = (
15
+ to: string,
16
+ subject: string,
17
+ template?: string,
18
+ text?: string,
19
+ html?: string
20
+ ): void => {
14
21
  if (!_storage) return;
15
22
  if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
16
23
  const content: MailContent = {
17
24
  to,
18
25
  subject,
19
26
  template,
27
+ ...(typeof text === 'string' && text !== ''
28
+ ? { text: redactUnknown(text, _redactionFields) as string }
29
+ : {}),
30
+ ...(typeof html === 'string' && html !== ''
31
+ ? { html: redactUnknown(html, _redactionFields) as string }
32
+ : {}),
20
33
  hostname: TraceContext.getHostname(),
21
34
  };
22
35
  _storage
@@ -38,9 +51,11 @@ export const MailWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze(
38
51
  register({ storage, config }: ITraceWatcherConfig): () => void {
39
52
  if (config.watchers.mail === false) return () => undefined;
40
53
  _storage = storage;
54
+ _redactionFields = [...config.redaction.keys, ...config.redaction.body];
41
55
  _ignoreRoutes = config.ignoreRoutes;
42
56
  return () => {
43
57
  _storage = null;
58
+ _redactionFields = [];
44
59
  _ignoreRoutes = [];
45
60
  };
46
61
  },
@@ -2,18 +2,30 @@ import { TraceContext } from '../context';
2
2
  import type { ITraceWatcher, ITraceWatcherConfig, NotificationContent } from '../types';
3
3
  import { EntryType } from '../types';
4
4
  import { AuthTag } from '../utils/authTag';
5
+ import { redactUnknown } from '../utils/redact';
5
6
  import { RequestFilter } from '../utils/requestFilter';
6
7
 
7
8
  let _storage: ITraceWatcherConfig['storage'] | null = null;
9
+ let _redactionFields: string[] = [];
8
10
  let _ignoreRoutes: string[] = [];
9
11
 
10
- const emit = (notification: string, channels: string[], notifiable?: string): void => {
12
+ const emit = (
13
+ notification: string,
14
+ channels: string[],
15
+ notifiable?: string,
16
+ message?: string,
17
+ payload?: unknown
18
+ ): void => {
11
19
  if (!_storage) return;
12
20
  if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
13
21
  const content: NotificationContent = {
14
22
  notification,
15
23
  channels,
16
24
  notifiable,
25
+ ...(typeof message === 'string' && message !== ''
26
+ ? { message: redactUnknown(message, _redactionFields) as string }
27
+ : {}),
28
+ ...(payload === undefined ? {} : { payload: redactUnknown(payload, _redactionFields) }),
17
29
  hostname: TraceContext.getHostname(),
18
30
  };
19
31
  _storage
@@ -34,9 +46,11 @@ export const NotificationWatcher: ITraceWatcher & { emit: typeof emit } = Object
34
46
  register({ storage, config }: ITraceWatcherConfig): () => void {
35
47
  if (config.watchers.notification === false) return () => undefined;
36
48
  _storage = storage;
49
+ _redactionFields = [...config.redaction.keys, ...config.redaction.body];
37
50
  _ignoreRoutes = config.ignoreRoutes;
38
51
  return () => {
39
52
  _storage = null;
53
+ _redactionFields = [];
40
54
  _ignoreRoutes = [];
41
55
  };
42
56
  },
@@ -8,6 +8,9 @@ import { EntryType } from '../types';
8
8
  import { AuthTag } from '../utils/authTag';
9
9
  import { RequestFilter } from '../utils/requestFilter';
10
10
 
11
+ let _storage: ITraceWatcherConfig['storage'] | null = null;
12
+ let _config: ITraceWatcherConfig['config'] | null = null;
13
+
11
14
  const bindingsInterpolated = (sql: string, params: unknown[]): string => {
12
15
  // Inline params for display only — safe, not for re-execution.
13
16
  let i = 0;
@@ -24,48 +27,61 @@ const isTraceStorageQuery = (sql: string): boolean => {
24
27
  return normalized.includes('zin_trace_entries') || normalized.includes('zin_trace_monitoring');
25
28
  };
26
29
 
27
- export const QueryWatcher: ITraceWatcher = Object.freeze({
30
+ const emit = (query: string, params: unknown[], duration: number, connection = 'default'): void => {
31
+ if (_storage === null || _config === null) return;
32
+ if (RequestFilter.shouldIgnoreCurrentRequest(_config.ignoreRoutes)) return;
33
+ if (isTraceStorageQuery(query)) return;
34
+
35
+ const batchId = TraceContext.getBatchId();
36
+ const includeBindings = _config.captureQueryBindings !== false;
37
+ const sql = includeBindings ? bindingsInterpolated(query, params) : query;
38
+ const roundedDuration = Math.round(duration * 100) / 100;
39
+ const hash = TraceStorage.familyHash(query);
40
+ const slow = roundedDuration >= _config.slowQueryThreshold;
41
+
42
+ const content: QueryContent = {
43
+ connection,
44
+ sql,
45
+ statement: query,
46
+ ...(includeBindings ? { bindings: [...params] } : {}),
47
+ bindingsIncluded: includeBindings,
48
+ time: roundedDuration,
49
+ duration: roundedDuration,
50
+ slow,
51
+ hash,
52
+ hostname: TraceContext.getHostname(),
53
+ };
54
+
55
+ const tags = AuthTag.append([]);
56
+ if (slow) tags.push('slow');
57
+
58
+ _storage
59
+ .writeEntry({
60
+ uuid: crypto.randomUUID(),
61
+ batchId,
62
+ familyHash: hash,
63
+ type: EntryType.QUERY,
64
+ content,
65
+ tags,
66
+ isLatest: true,
67
+ createdAt: TraceContext.now(),
68
+ })
69
+ .catch(() => undefined);
70
+ };
71
+
72
+ export const QueryWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
73
+ emit,
74
+
28
75
  register({ storage, config, db: injectedDb }: ITraceWatcherConfig): () => void {
29
76
  if (config.watchers.query === false) return () => undefined;
30
77
  if (!injectedDb) return () => undefined; // no db available
31
78
 
79
+ _storage = storage;
80
+ _config = config;
32
81
  const db = injectedDb;
33
82
 
34
83
  const handler = (query: string, params: unknown[], duration: number): void => {
35
- if (RequestFilter.shouldIgnoreCurrentRequest(config.ignoreRoutes)) return;
36
- if (isTraceStorageQuery(query)) return;
37
-
38
- const batchId = TraceContext.getBatchId();
39
- const sql = bindingsInterpolated(query, params);
40
- const roundedDuration = Math.round(duration * 100) / 100;
41
- const hash = TraceStorage.familyHash(query);
42
- const slow = roundedDuration >= config.slowQueryThreshold;
43
-
44
- const content: QueryContent = {
45
- connection: 'default',
46
- sql,
47
- time: roundedDuration,
48
- duration: roundedDuration,
49
- slow,
50
- hash,
51
- hostname: TraceContext.getHostname(),
52
- };
53
-
54
- const tags = AuthTag.append([]);
55
- if (slow) tags.push('slow');
56
-
57
- storage
58
- .writeEntry({
59
- uuid: crypto.randomUUID(),
60
- batchId,
61
- familyHash: hash,
62
- type: EntryType.QUERY,
63
- content,
64
- tags,
65
- isLatest: true,
66
- createdAt: TraceContext.now(),
67
- })
68
- .catch(() => undefined);
84
+ emit(query, params, duration);
69
85
  };
70
86
 
71
87
  (
@@ -75,6 +91,8 @@ export const QueryWatcher: ITraceWatcher = Object.freeze({
75
91
  ).onAfterQuery?.(handler);
76
92
 
77
93
  return () => {
94
+ _storage = null;
95
+ _config = null;
78
96
  (
79
97
  db as {
80
98
  offAfterQuery?: (h: (sql: string, params: unknown[], duration: number) => void) => void;