@zintrust/trace 0.4.81 → 0.4.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/build-manifest.json +45 -45
- package/dist/config.js +3 -0
- package/dist/dashboard/ui.js +103 -8
- package/dist/register.js +36 -4
- package/dist/types.d.ts +30 -1
- package/dist/watchers/CacheWatcher.d.ts +1 -1
- package/dist/watchers/CacheWatcher.js +10 -2
- package/dist/watchers/HttpClientWatcher.d.ts +2 -2
- package/dist/watchers/HttpClientWatcher.js +17 -4
- package/dist/watchers/MailWatcher.d.ts +1 -1
- package/dist/watchers/MailWatcher.js +12 -3
- package/dist/watchers/NotificationWatcher.d.ts +1 -1
- package/dist/watchers/NotificationWatcher.js +9 -1
- package/dist/watchers/QueryWatcher.d.ts +5 -1
- package/dist/watchers/QueryWatcher.js +49 -33
- package/package.json +3 -3
- package/src/config.ts +3 -0
- package/src/dashboard/ui.ts +103 -8
- package/src/register.ts +51 -5
- package/src/types.ts +31 -1
- package/src/watchers/CacheWatcher.ts +13 -2
- package/src/watchers/HttpClientWatcher.ts +33 -11
- package/src/watchers/MailWatcher.ts +18 -3
- package/src/watchers/NotificationWatcher.ts +15 -1
- package/src/watchers/QueryWatcher.ts +53 -35
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
|
|
248
|
-
|
|
249
|
-
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
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
|
|
14
|
-
url
|
|
15
|
-
requestHeaders
|
|
16
|
-
responseStatus
|
|
17
|
-
duration
|
|
18
|
-
|
|
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
|
-
|
|
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 = (
|
|
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 = (
|
|
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
|
-
|
|
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
|
-
|
|
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;
|