@zintrust/trace 0.4.75
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 +288 -0
- package/dist/build-manifest.json +365 -0
- package/dist/cli-register.d.ts +9 -0
- package/dist/cli-register.js +32 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +38 -0
- package/dist/context.d.ts +18 -0
- package/dist/context.js +86 -0
- package/dist/dashboard/handlers.d.ts +15 -0
- package/dist/dashboard/handlers.js +179 -0
- package/dist/dashboard/routes.d.ts +19 -0
- package/dist/dashboard/routes.js +50 -0
- package/dist/dashboard/ui.d.ts +2 -0
- package/dist/dashboard/ui.js +870 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +50 -0
- package/dist/migrations/20260331000001_create_zin_debugger_entries_table.d.ts +10 -0
- package/dist/migrations/20260331000001_create_zin_debugger_entries_table.js +28 -0
- package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.d.ts +10 -0
- package/dist/migrations/20260331000002_create_zin_debugger_entries_tags_table.js +21 -0
- package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.d.ts +10 -0
- package/dist/migrations/20260331000003_create_zin_debugger_monitoring_table.js +17 -0
- package/dist/migrations/index.d.ts +6 -0
- package/dist/migrations/index.js +4 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.js +3 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +140 -0
- package/dist/storage/DebuggerStorage.d.ts +13 -0
- package/dist/storage/DebuggerStorage.js +195 -0
- package/dist/storage/TraceStorage.d.ts +13 -0
- package/dist/storage/TraceStorage.js +195 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.js +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.js +25 -0
- package/dist/ui.d.ts +8 -0
- package/dist/ui.js +7 -0
- package/dist/utils/authTag.d.ts +5 -0
- package/dist/utils/authTag.js +18 -0
- package/dist/utils/familyHash.d.ts +1 -0
- package/dist/utils/familyHash.js +8 -0
- package/dist/utils/redact.d.ts +6 -0
- package/dist/utils/redact.js +49 -0
- package/dist/utils/requestFilter.d.ts +4 -0
- package/dist/utils/requestFilter.js +26 -0
- package/dist/utils/stackFrame.d.ts +6 -0
- package/dist/utils/stackFrame.js +38 -0
- package/dist/watchers/AuthWatcher.d.ts +6 -0
- package/dist/watchers/AuthWatcher.js +49 -0
- package/dist/watchers/BatchWatcher.d.ts +6 -0
- package/dist/watchers/BatchWatcher.js +46 -0
- package/dist/watchers/CacheWatcher.d.ts +6 -0
- package/dist/watchers/CacheWatcher.js +51 -0
- package/dist/watchers/CommandWatcher.d.ts +6 -0
- package/dist/watchers/CommandWatcher.js +49 -0
- package/dist/watchers/DumpWatcher.d.ts +7 -0
- package/dist/watchers/DumpWatcher.js +41 -0
- package/dist/watchers/EventWatcher.d.ts +6 -0
- package/dist/watchers/EventWatcher.js +42 -0
- package/dist/watchers/ExceptionWatcher.d.ts +4 -0
- package/dist/watchers/ExceptionWatcher.js +103 -0
- package/dist/watchers/GateWatcher.d.ts +6 -0
- package/dist/watchers/GateWatcher.js +45 -0
- package/dist/watchers/HttpClientWatcher.d.ts +6 -0
- package/dist/watchers/HttpClientWatcher.js +50 -0
- package/dist/watchers/HttpWatcher.d.ts +2 -0
- package/dist/watchers/HttpWatcher.js +71 -0
- package/dist/watchers/JobWatcher.d.ts +10 -0
- package/dist/watchers/JobWatcher.js +108 -0
- package/dist/watchers/LogWatcher.d.ts +2 -0
- package/dist/watchers/LogWatcher.js +50 -0
- package/dist/watchers/MailWatcher.d.ts +6 -0
- package/dist/watchers/MailWatcher.js +45 -0
- package/dist/watchers/MiddlewareWatcher.d.ts +6 -0
- package/dist/watchers/MiddlewareWatcher.js +41 -0
- package/dist/watchers/ModelWatcher.d.ts +6 -0
- package/dist/watchers/ModelWatcher.js +42 -0
- package/dist/watchers/NotificationWatcher.d.ts +6 -0
- package/dist/watchers/NotificationWatcher.js +42 -0
- package/dist/watchers/QueryWatcher.d.ts +2 -0
- package/dist/watchers/QueryWatcher.js +72 -0
- package/dist/watchers/RedisWatcher.d.ts +7 -0
- package/dist/watchers/RedisWatcher.js +38 -0
- package/dist/watchers/ScheduleWatcher.d.ts +6 -0
- package/dist/watchers/ScheduleWatcher.js +46 -0
- package/dist/watchers/ViewWatcher.d.ts +6 -0
- package/dist/watchers/ViewWatcher.js +36 -0
- package/package.json +59 -0
- package/src/cli-register.ts +63 -0
- package/src/config.ts +46 -0
- package/src/context.ts +101 -0
- package/src/dashboard/handlers.ts +197 -0
- package/src/dashboard/routes.ts +101 -0
- package/src/dashboard/ui.ts +879 -0
- package/src/dashboard/zintrust-debuger.svg +30 -0
- package/src/index.ts +88 -0
- package/src/plugin.ts +9 -0
- package/src/register.ts +219 -0
- package/src/storage/TraceStorage.ts +306 -0
- package/src/storage/index.ts +2 -0
- package/src/types.ts +317 -0
- package/src/ui.ts +9 -0
- package/src/utils/authTag.ts +20 -0
- package/src/utils/familyHash.ts +8 -0
- package/src/utils/redact.ts +64 -0
- package/src/utils/requestFilter.ts +33 -0
- package/src/utils/stackFrame.ts +44 -0
- package/src/watchers/AuthWatcher.ts +50 -0
- package/src/watchers/BatchWatcher.ts +52 -0
- package/src/watchers/CacheWatcher.ts +58 -0
- package/src/watchers/CommandWatcher.ts +55 -0
- package/src/watchers/DumpWatcher.ts +42 -0
- package/src/watchers/EventWatcher.ts +43 -0
- package/src/watchers/ExceptionWatcher.ts +114 -0
- package/src/watchers/GateWatcher.ts +50 -0
- package/src/watchers/HttpClientWatcher.ts +56 -0
- package/src/watchers/HttpWatcher.ts +94 -0
- package/src/watchers/JobWatcher.ts +121 -0
- package/src/watchers/LogWatcher.ts +61 -0
- package/src/watchers/MailWatcher.ts +47 -0
- package/src/watchers/MiddlewareWatcher.ts +42 -0
- package/src/watchers/ModelWatcher.ts +48 -0
- package/src/watchers/NotificationWatcher.ts +43 -0
- package/src/watchers/QueryWatcher.ts +85 -0
- package/src/watchers/RedisWatcher.ts +39 -0
- package/src/watchers/ScheduleWatcher.ts +54 -0
- package/src/watchers/ViewWatcher.ts +37 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for @zintrust/trace
|
|
3
|
+
* Sealed type definitions — no side effects.
|
|
4
|
+
*/
|
|
5
|
+
import type { IDatabase } from '@zintrust/core';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Entry types used by the trace event stream.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
export const EntryType = Object.freeze({
|
|
12
|
+
REQUEST: 'request',
|
|
13
|
+
QUERY: 'query',
|
|
14
|
+
EXCEPTION: 'exception',
|
|
15
|
+
LOG: 'log',
|
|
16
|
+
JOB: 'job',
|
|
17
|
+
CACHE: 'cache',
|
|
18
|
+
SCHEDULE: 'schedule',
|
|
19
|
+
MAIL: 'mail',
|
|
20
|
+
AUTH: 'auth',
|
|
21
|
+
EVENT: 'event',
|
|
22
|
+
MODEL: 'model',
|
|
23
|
+
NOTIFICATION: 'notification',
|
|
24
|
+
REDIS: 'redis',
|
|
25
|
+
GATE: 'gate',
|
|
26
|
+
MIDDLEWARE: 'middleware',
|
|
27
|
+
COMMAND: 'command',
|
|
28
|
+
BATCH: 'batch',
|
|
29
|
+
DUMP: 'dump',
|
|
30
|
+
VIEW: 'view',
|
|
31
|
+
CLIENT_REQUEST: 'client_request',
|
|
32
|
+
} as const);
|
|
33
|
+
|
|
34
|
+
export type EntryTypeValue = (typeof EntryType)[keyof typeof EntryType];
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Per-type content shapes
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
export interface RequestContent {
|
|
41
|
+
method: string;
|
|
42
|
+
uri: string;
|
|
43
|
+
headers: Record<string, string>;
|
|
44
|
+
payload: Record<string, unknown>;
|
|
45
|
+
responseStatus: number;
|
|
46
|
+
responseHeaders: Record<string, string>;
|
|
47
|
+
duration: number;
|
|
48
|
+
memory: number | null;
|
|
49
|
+
middleware: string[];
|
|
50
|
+
hostname: string;
|
|
51
|
+
userId?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface QueryContent {
|
|
55
|
+
connection: string;
|
|
56
|
+
sql: string;
|
|
57
|
+
time: number;
|
|
58
|
+
duration: number;
|
|
59
|
+
slow: boolean;
|
|
60
|
+
hash: string;
|
|
61
|
+
hostname: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ExceptionContent {
|
|
65
|
+
class: string;
|
|
66
|
+
file: string;
|
|
67
|
+
line: number;
|
|
68
|
+
message: string;
|
|
69
|
+
trace: Array<{ file: string; line: number; function?: string }>;
|
|
70
|
+
linePreview: Record<string, string>;
|
|
71
|
+
occurrences: number;
|
|
72
|
+
hostname: string;
|
|
73
|
+
userId?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface LogContent {
|
|
77
|
+
level: string;
|
|
78
|
+
message: string;
|
|
79
|
+
context?: Record<string, unknown>;
|
|
80
|
+
hostname: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface JobContent {
|
|
84
|
+
status: 'pending' | 'processed' | 'failed';
|
|
85
|
+
connection: string;
|
|
86
|
+
queue: string;
|
|
87
|
+
name: string;
|
|
88
|
+
tries?: number;
|
|
89
|
+
timeout?: number;
|
|
90
|
+
data?: unknown;
|
|
91
|
+
exception?: { message: string; trace: Array<{ file: string; line: number }> };
|
|
92
|
+
hostname: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface CacheContent {
|
|
96
|
+
operation: 'get' | 'set' | 'delete' | 'clear' | 'has';
|
|
97
|
+
key: string;
|
|
98
|
+
hit?: boolean;
|
|
99
|
+
duration: number;
|
|
100
|
+
hostname: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ScheduleContent {
|
|
104
|
+
name: string;
|
|
105
|
+
expression: string;
|
|
106
|
+
status: 'ran' | 'failed' | 'skipped';
|
|
107
|
+
duration: number;
|
|
108
|
+
output?: string;
|
|
109
|
+
hostname: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface MailContent {
|
|
113
|
+
to: string;
|
|
114
|
+
subject: string;
|
|
115
|
+
template?: string;
|
|
116
|
+
hostname: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface AuthContent {
|
|
120
|
+
event: 'login' | 'logout' | 'failed';
|
|
121
|
+
userId?: string;
|
|
122
|
+
hostname: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface EventContent {
|
|
126
|
+
name: string;
|
|
127
|
+
payload?: unknown;
|
|
128
|
+
listenerCount: number;
|
|
129
|
+
hostname: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface ModelContent {
|
|
133
|
+
action: 'create' | 'update' | 'delete';
|
|
134
|
+
model: string;
|
|
135
|
+
id?: string | number;
|
|
136
|
+
changes?: Record<string, unknown>;
|
|
137
|
+
hostname: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface NotificationContent {
|
|
141
|
+
channels: string[];
|
|
142
|
+
notifiable?: string;
|
|
143
|
+
notification: string;
|
|
144
|
+
hostname: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface RedisContent {
|
|
148
|
+
command: string;
|
|
149
|
+
duration: number;
|
|
150
|
+
hostname: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface GateContent {
|
|
154
|
+
ability: string;
|
|
155
|
+
result: 'allowed' | 'denied';
|
|
156
|
+
userId?: string;
|
|
157
|
+
subject?: string;
|
|
158
|
+
hostname: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface MiddlewareContent {
|
|
162
|
+
name: string;
|
|
163
|
+
event: 'before' | 'after';
|
|
164
|
+
duration?: number;
|
|
165
|
+
hostname: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface CommandContent {
|
|
169
|
+
name: string;
|
|
170
|
+
arguments: Record<string, unknown>;
|
|
171
|
+
exitCode: number;
|
|
172
|
+
duration: number;
|
|
173
|
+
output?: string;
|
|
174
|
+
hostname: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface BatchContent {
|
|
178
|
+
name: string;
|
|
179
|
+
total: number;
|
|
180
|
+
processed: number;
|
|
181
|
+
failed: number;
|
|
182
|
+
status: 'pending' | 'processing' | 'finished' | 'failed';
|
|
183
|
+
hostname: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface DumpContent {
|
|
187
|
+
value: unknown;
|
|
188
|
+
file?: string;
|
|
189
|
+
line?: number;
|
|
190
|
+
hostname: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface ViewContent {
|
|
194
|
+
template: string;
|
|
195
|
+
duration: number;
|
|
196
|
+
hostname: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface ClientRequestContent {
|
|
200
|
+
method: string;
|
|
201
|
+
url: string;
|
|
202
|
+
requestHeaders: Record<string, string>;
|
|
203
|
+
responseStatus: number;
|
|
204
|
+
duration: number;
|
|
205
|
+
hostname: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Core domain records
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
export interface ITraceEntry<T = unknown> {
|
|
213
|
+
uuid: string;
|
|
214
|
+
batchId: string;
|
|
215
|
+
familyHash?: string;
|
|
216
|
+
type: EntryTypeValue;
|
|
217
|
+
content: T;
|
|
218
|
+
tags: string[];
|
|
219
|
+
isLatest: boolean;
|
|
220
|
+
createdAt: number;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Storage interface
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
export interface QueryEntriesOptions {
|
|
228
|
+
type?: EntryTypeValue;
|
|
229
|
+
tag?: string;
|
|
230
|
+
batchId?: string;
|
|
231
|
+
from?: number;
|
|
232
|
+
to?: number;
|
|
233
|
+
page?: number;
|
|
234
|
+
perPage?: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface ITraceStorage {
|
|
238
|
+
writeEntry(entry: ITraceEntry): Promise<void>;
|
|
239
|
+
updateEntry(
|
|
240
|
+
uuid: string,
|
|
241
|
+
patch: Partial<Pick<ITraceEntry, 'content' | 'isLatest'>>
|
|
242
|
+
): Promise<void>;
|
|
243
|
+
markFamilyStale(familyHash: string, exceptUuid: string): Promise<void>;
|
|
244
|
+
queryEntries(opts: QueryEntriesOptions): Promise<{ data: ITraceEntry[]; total: number }>;
|
|
245
|
+
getEntry(uuid: string): Promise<ITraceEntry | null>;
|
|
246
|
+
getBatch(batchId: string): Promise<ITraceEntry[]>;
|
|
247
|
+
prune(olderThanMs: number, keepExceptions?: boolean): Promise<number>;
|
|
248
|
+
clear(): Promise<void>;
|
|
249
|
+
getMonitoring(): Promise<string[]>;
|
|
250
|
+
addMonitoring(tag: string): Promise<void>;
|
|
251
|
+
removeMonitoring(tag: string): Promise<void>;
|
|
252
|
+
stats(): Promise<Record<EntryTypeValue, number>>;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Watcher interface
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
export interface ITraceWatcherConfig {
|
|
260
|
+
storage: ITraceStorage;
|
|
261
|
+
config: ITraceConfig;
|
|
262
|
+
db?: IDatabase;
|
|
263
|
+
/** Optional: provide to allow HttpWatcher to register as global middleware. */
|
|
264
|
+
registerMiddleware?: (
|
|
265
|
+
fn: (req: unknown, res: unknown, next: () => Promise<void>) => Promise<void>
|
|
266
|
+
) => void;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface ITraceWatcher {
|
|
270
|
+
register(opts: ITraceWatcherConfig): () => void;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Config interface
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
export type RedactionConfig = {
|
|
278
|
+
headers: string[];
|
|
279
|
+
body: string[];
|
|
280
|
+
query: string[];
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export type WatcherToggles = {
|
|
284
|
+
request?: boolean;
|
|
285
|
+
query?: boolean;
|
|
286
|
+
exception?: boolean;
|
|
287
|
+
log?: boolean;
|
|
288
|
+
job?: boolean;
|
|
289
|
+
cache?: boolean;
|
|
290
|
+
schedule?: boolean;
|
|
291
|
+
mail?: boolean;
|
|
292
|
+
auth?: boolean;
|
|
293
|
+
event?: boolean;
|
|
294
|
+
model?: boolean;
|
|
295
|
+
notification?: boolean;
|
|
296
|
+
redis?: boolean;
|
|
297
|
+
gate?: boolean;
|
|
298
|
+
middleware?: boolean;
|
|
299
|
+
command?: boolean;
|
|
300
|
+
batch?: boolean;
|
|
301
|
+
dump?: boolean;
|
|
302
|
+
view?: boolean;
|
|
303
|
+
clientRequest?: boolean;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export interface ITraceConfig {
|
|
307
|
+
enabled: boolean;
|
|
308
|
+
connection?: string;
|
|
309
|
+
pruneAfterHours: number;
|
|
310
|
+
ignoreRoutes: string[];
|
|
311
|
+
slowQueryThreshold: number;
|
|
312
|
+
logMinLevel: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
313
|
+
watchers: WatcherToggles;
|
|
314
|
+
redaction: RedactionConfig;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export type TraceConfigOverrides = Partial<ITraceConfig>;
|
package/src/ui.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight UI/dashboard entrypoint for @zintrust/trace.
|
|
3
|
+
*
|
|
4
|
+
* Import this subpath when you only need trace dashboard registration
|
|
5
|
+
* without pulling in the package root re-export surface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { registerTraceDashboard, registerTraceRoutes } from './dashboard/routes';
|
|
9
|
+
export type { TraceDashboardOptions, TraceDashboardRegistrationOptions } from './dashboard/routes';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TraceContext } from '../context';
|
|
2
|
+
|
|
3
|
+
const resolveAuthTag = (): string | undefined => {
|
|
4
|
+
const userId = TraceContext.getUserId();
|
|
5
|
+
if (userId === undefined || userId === '') return undefined;
|
|
6
|
+
return `Auth:${userId}`;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const appendAuthTag = (tags: string[]): string[] => {
|
|
10
|
+
const authTag = resolveAuthTag();
|
|
11
|
+
if (authTag === undefined || tags.includes(authTag)) return tags;
|
|
12
|
+
return [...tags, authTag];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const AuthTag = Object.freeze({
|
|
16
|
+
append: appendAuthTag,
|
|
17
|
+
resolve: resolveAuthTag,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default AuthTag;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction helpers for @zintrust/trace watchers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const REDACTED = '[REDACTED]';
|
|
6
|
+
|
|
7
|
+
const redactQuerySegment = (segment: string, fields: Set<string>): string => {
|
|
8
|
+
const separatorIndex = segment.indexOf('=');
|
|
9
|
+
if (separatorIndex <= 0) return segment;
|
|
10
|
+
|
|
11
|
+
const key = segment.slice(0, separatorIndex);
|
|
12
|
+
const value = segment.slice(separatorIndex + 1);
|
|
13
|
+
if (!fields.has(key.toLowerCase())) return `${key}=${value}`;
|
|
14
|
+
|
|
15
|
+
return `${key}=${REDACTED}`;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const redactHeaders = (
|
|
19
|
+
headers: Record<string, string>,
|
|
20
|
+
fields: string[]
|
|
21
|
+
): Record<string, string> => {
|
|
22
|
+
const lower = new Set(fields.map((f) => f.toLowerCase()));
|
|
23
|
+
const out: Record<string, string> = {};
|
|
24
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
25
|
+
out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const redactObject = (
|
|
31
|
+
obj: Record<string, unknown>,
|
|
32
|
+
fields: string[]
|
|
33
|
+
): Record<string, unknown> => {
|
|
34
|
+
const lower = new Set(fields.map((f) => f.toLowerCase()));
|
|
35
|
+
const out: Record<string, unknown> = {};
|
|
36
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
37
|
+
out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const redactString = (value: string, fields: string[]): string => {
|
|
43
|
+
const lower = new Set(fields.map((f) => f.toLowerCase()));
|
|
44
|
+
if (value === '') return value;
|
|
45
|
+
|
|
46
|
+
let output = '';
|
|
47
|
+
let segmentStart = 0;
|
|
48
|
+
|
|
49
|
+
for (let index = 0; index <= value.length; index += 1) {
|
|
50
|
+
const isBoundary = index === value.length || value[index] === '&';
|
|
51
|
+
if (!isBoundary) continue;
|
|
52
|
+
|
|
53
|
+
const segment = value.slice(segmentStart, index);
|
|
54
|
+
output += redactQuerySegment(segment, lower);
|
|
55
|
+
|
|
56
|
+
if (index < value.length) {
|
|
57
|
+
output += '&';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
segmentStart = index + 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return output;
|
|
64
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TraceContext } from '../context';
|
|
2
|
+
|
|
3
|
+
const normalizePath = (input: string): string => {
|
|
4
|
+
const trimmed = input.trim();
|
|
5
|
+
const [pathOnly] = trimmed.split('?');
|
|
6
|
+
if (!pathOnly || pathOnly === '') return '/';
|
|
7
|
+
return pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const matchesIgnoredPath = (path: string, ignoreRoutes: string[]): boolean => {
|
|
11
|
+
const normalizedPath = normalizePath(path);
|
|
12
|
+
|
|
13
|
+
return ignoreRoutes.some((route) => {
|
|
14
|
+
const normalizedRoute = normalizePath(route);
|
|
15
|
+
return (
|
|
16
|
+
normalizedPath === normalizedRoute ||
|
|
17
|
+
normalizedPath.startsWith(
|
|
18
|
+
normalizedRoute.endsWith('/') ? normalizedRoute : `${normalizedRoute}/`
|
|
19
|
+
)
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const shouldIgnoreCurrentRequest = (ignoreRoutes: string[]): boolean => {
|
|
25
|
+
const currentPath = TraceContext.getRequestPath();
|
|
26
|
+
if (typeof currentPath !== 'string' || currentPath === '') return false;
|
|
27
|
+
return matchesIgnoredPath(currentPath, ignoreRoutes);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const RequestFilter = Object.freeze({
|
|
31
|
+
matchesIgnoredPath,
|
|
32
|
+
shouldIgnoreCurrentRequest,
|
|
33
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type StackFrame = { file: string; line: number };
|
|
2
|
+
|
|
3
|
+
const FRAME_PREFIX = 'at ';
|
|
4
|
+
|
|
5
|
+
const parsePositiveInt = (value: string): number | null => {
|
|
6
|
+
if (value === '') return null;
|
|
7
|
+
|
|
8
|
+
for (const char of value) {
|
|
9
|
+
if (char < '0' || char > '9') return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const parsed = Number.parseInt(value, 10);
|
|
13
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const parseFrameLocation = (value: string): StackFrame | null => {
|
|
17
|
+
const columnSeparatorIndex = value.lastIndexOf(':');
|
|
18
|
+
if (columnSeparatorIndex <= 0) return null;
|
|
19
|
+
|
|
20
|
+
const lineSeparatorIndex = value.lastIndexOf(':', columnSeparatorIndex - 1);
|
|
21
|
+
if (lineSeparatorIndex <= 0) return null;
|
|
22
|
+
|
|
23
|
+
const file = value.slice(0, lineSeparatorIndex).trim();
|
|
24
|
+
const line = parsePositiveInt(value.slice(lineSeparatorIndex + 1, columnSeparatorIndex));
|
|
25
|
+
const column = parsePositiveInt(value.slice(columnSeparatorIndex + 1));
|
|
26
|
+
|
|
27
|
+
if (file === '' || line === null || column === null) return null;
|
|
28
|
+
return { file, line };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const parseStackFrameLine = (line: string): StackFrame | null => {
|
|
32
|
+
const trimmed = line.trim();
|
|
33
|
+
if (!trimmed.startsWith(FRAME_PREFIX)) return null;
|
|
34
|
+
|
|
35
|
+
const body = trimmed.slice(FRAME_PREFIX.length).trim();
|
|
36
|
+
if (body === '') return null;
|
|
37
|
+
|
|
38
|
+
const wrappedStartIndex = body.lastIndexOf(' (');
|
|
39
|
+
if (wrappedStartIndex !== -1 && body.endsWith(')')) {
|
|
40
|
+
return parseFrameLocation(body.slice(wrappedStartIndex + 2, -1));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return parseFrameLocation(body);
|
|
44
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthWatcher — records login/logout/failed auth events.
|
|
3
|
+
* Credentials are never stored; only the outcome.
|
|
4
|
+
*/
|
|
5
|
+
import { TraceContext } from '../context';
|
|
6
|
+
import type { AuthContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
|
|
7
|
+
import { EntryType } from '../types';
|
|
8
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
9
|
+
|
|
10
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
11
|
+
let _ignoreRoutes: string[] = [];
|
|
12
|
+
|
|
13
|
+
const emit = (event: AuthContent['event'], userId?: string): void => {
|
|
14
|
+
if (!_storage) return;
|
|
15
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
16
|
+
const content: AuthContent = {
|
|
17
|
+
event,
|
|
18
|
+
userId,
|
|
19
|
+
hostname: TraceContext.getHostname(),
|
|
20
|
+
};
|
|
21
|
+
const tags: string[] = [];
|
|
22
|
+
if (userId) tags.push(`Auth:${userId}`);
|
|
23
|
+
if (event === 'failed') tags.push('failed');
|
|
24
|
+
|
|
25
|
+
_storage
|
|
26
|
+
.writeEntry({
|
|
27
|
+
uuid: crypto.randomUUID(),
|
|
28
|
+
batchId: TraceContext.getBatchId(),
|
|
29
|
+
type: EntryType.AUTH,
|
|
30
|
+
content,
|
|
31
|
+
tags,
|
|
32
|
+
isLatest: true,
|
|
33
|
+
createdAt: TraceContext.now(),
|
|
34
|
+
})
|
|
35
|
+
.catch(() => undefined);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const AuthWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
|
|
39
|
+
emit,
|
|
40
|
+
|
|
41
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
42
|
+
if (config.watchers.auth === false) return () => undefined;
|
|
43
|
+
_storage = storage;
|
|
44
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
45
|
+
return () => {
|
|
46
|
+
_storage = null;
|
|
47
|
+
_ignoreRoutes = [];
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { TraceContext } from '../context';
|
|
2
|
+
import type { BatchContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
|
|
3
|
+
import { EntryType } from '../types';
|
|
4
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
5
|
+
|
|
6
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
7
|
+
let _ignoreRoutes: string[] = [];
|
|
8
|
+
|
|
9
|
+
const emit = (
|
|
10
|
+
name: string,
|
|
11
|
+
total: number,
|
|
12
|
+
processed: number,
|
|
13
|
+
failed: number,
|
|
14
|
+
status: BatchContent['status']
|
|
15
|
+
): void => {
|
|
16
|
+
if (!_storage) return;
|
|
17
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
18
|
+
const tags = [name];
|
|
19
|
+
if (failed > 0) tags.push('failed');
|
|
20
|
+
const content: BatchContent = {
|
|
21
|
+
name,
|
|
22
|
+
total,
|
|
23
|
+
processed,
|
|
24
|
+
failed,
|
|
25
|
+
status,
|
|
26
|
+
hostname: TraceContext.getHostname(),
|
|
27
|
+
};
|
|
28
|
+
_storage
|
|
29
|
+
.writeEntry({
|
|
30
|
+
uuid: crypto.randomUUID(),
|
|
31
|
+
batchId: TraceContext.getBatchId(),
|
|
32
|
+
type: EntryType.BATCH,
|
|
33
|
+
content,
|
|
34
|
+
tags,
|
|
35
|
+
isLatest: true,
|
|
36
|
+
createdAt: TraceContext.now(),
|
|
37
|
+
})
|
|
38
|
+
.catch(() => undefined);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const BatchWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
|
|
42
|
+
emit,
|
|
43
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
44
|
+
if (config.watchers.batch === false) return () => undefined;
|
|
45
|
+
_storage = storage;
|
|
46
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
47
|
+
return () => {
|
|
48
|
+
_storage = null;
|
|
49
|
+
_ignoreRoutes = [];
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CacheWatcher — records cache operations.
|
|
3
|
+
* Call CacheWatcher.emit() from within your cache driver instrumentation.
|
|
4
|
+
*/
|
|
5
|
+
import { TraceContext } from '../context';
|
|
6
|
+
import type { CacheContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
|
|
7
|
+
import { EntryType } from '../types';
|
|
8
|
+
import { AuthTag } from '../utils/authTag';
|
|
9
|
+
import { redactString } from '../utils/redact';
|
|
10
|
+
import { RequestFilter } from '../utils/requestFilter';
|
|
11
|
+
|
|
12
|
+
let _storage: ITraceWatcherConfig['storage'] | null = null;
|
|
13
|
+
let _redactionFields: string[] = [];
|
|
14
|
+
let _ignoreRoutes: string[] = [];
|
|
15
|
+
|
|
16
|
+
const emit = (
|
|
17
|
+
operation: CacheContent['operation'],
|
|
18
|
+
key: string,
|
|
19
|
+
duration: number,
|
|
20
|
+
hit?: boolean
|
|
21
|
+
): void => {
|
|
22
|
+
if (!_storage) return;
|
|
23
|
+
if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes)) return;
|
|
24
|
+
const safeKey = redactString(key, _redactionFields);
|
|
25
|
+
const content: CacheContent = {
|
|
26
|
+
operation,
|
|
27
|
+
key: safeKey,
|
|
28
|
+
hit,
|
|
29
|
+
duration,
|
|
30
|
+
hostname: TraceContext.getHostname(),
|
|
31
|
+
};
|
|
32
|
+
_storage
|
|
33
|
+
.writeEntry({
|
|
34
|
+
uuid: crypto.randomUUID(),
|
|
35
|
+
batchId: TraceContext.getBatchId(),
|
|
36
|
+
type: EntryType.CACHE,
|
|
37
|
+
content,
|
|
38
|
+
tags: AuthTag.append([]),
|
|
39
|
+
isLatest: true,
|
|
40
|
+
createdAt: TraceContext.now(),
|
|
41
|
+
})
|
|
42
|
+
.catch(() => undefined);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const CacheWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
|
|
46
|
+
emit,
|
|
47
|
+
|
|
48
|
+
register({ storage, config }: ITraceWatcherConfig): () => void {
|
|
49
|
+
if (config.watchers.cache === false) return () => undefined;
|
|
50
|
+
_storage = storage;
|
|
51
|
+
_redactionFields = config.redaction.query;
|
|
52
|
+
_ignoreRoutes = config.ignoreRoutes;
|
|
53
|
+
return () => {
|
|
54
|
+
_storage = null;
|
|
55
|
+
_ignoreRoutes = [];
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
});
|