@zintrust/trace 1.6.1 → 1.6.3

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.
Files changed (50) hide show
  1. package/dist/register.js +19 -15
  2. package/package.json +2 -3
  3. package/src/TraceConnection.ts +0 -182
  4. package/src/cli-register.ts +0 -63
  5. package/src/config.ts +0 -383
  6. package/src/context.ts +0 -101
  7. package/src/dashboard/handlers.ts +0 -353
  8. package/src/dashboard/routes.ts +0 -114
  9. package/src/dashboard/ui.ts +0 -1262
  10. package/src/dashboard/zintrust-debuger.svg +0 -30
  11. package/src/index.ts +0 -102
  12. package/src/ingest/TraceIngestGateway.ts +0 -414
  13. package/src/plugin.ts +0 -9
  14. package/src/register.ts +0 -659
  15. package/src/storage/ProxyTraceStorage.ts +0 -190
  16. package/src/storage/TraceContentBudget.ts +0 -491
  17. package/src/storage/TraceContentRedaction.ts +0 -44
  18. package/src/storage/TraceEntryFiltering.ts +0 -92
  19. package/src/storage/TraceServiceTag.ts +0 -56
  20. package/src/storage/TraceStorage.ts +0 -543
  21. package/src/storage/TraceWriteDiagnostics.ts +0 -289
  22. package/src/storage/index.ts +0 -4
  23. package/src/types.ts +0 -430
  24. package/src/ui.ts +0 -9
  25. package/src/utils/authTag.ts +0 -20
  26. package/src/utils/entryFilter.ts +0 -131
  27. package/src/utils/familyHash.ts +0 -8
  28. package/src/utils/redact.ts +0 -112
  29. package/src/utils/requestFilter.ts +0 -79
  30. package/src/utils/stackFrame.ts +0 -44
  31. package/src/watchers/AuthWatcher.ts +0 -53
  32. package/src/watchers/BatchWatcher.ts +0 -55
  33. package/src/watchers/CacheWatcher.ts +0 -72
  34. package/src/watchers/CommandWatcher.ts +0 -58
  35. package/src/watchers/DumpWatcher.ts +0 -45
  36. package/src/watchers/EventWatcher.ts +0 -46
  37. package/src/watchers/ExceptionWatcher.ts +0 -130
  38. package/src/watchers/GateWatcher.ts +0 -53
  39. package/src/watchers/HttpClientWatcher.ts +0 -219
  40. package/src/watchers/HttpWatcher.ts +0 -220
  41. package/src/watchers/JobWatcher.ts +0 -124
  42. package/src/watchers/LogWatcher.ts +0 -120
  43. package/src/watchers/MailWatcher.ts +0 -65
  44. package/src/watchers/MiddlewareWatcher.ts +0 -54
  45. package/src/watchers/ModelWatcher.ts +0 -60
  46. package/src/watchers/NotificationWatcher.ts +0 -60
  47. package/src/watchers/QueryWatcher.ts +0 -107
  48. package/src/watchers/RedisWatcher.ts +0 -42
  49. package/src/watchers/ScheduleWatcher.ts +0 -57
  50. package/src/watchers/ViewWatcher.ts +0 -40
package/src/context.ts DELETED
@@ -1,101 +0,0 @@
1
- /**
2
- * TraceContext — sealed namespace for batch_id, userId, hostname, and memory.
3
- * Piggybacks on RequestContext (already available in core) — no new ALS store.
4
- */
5
- type RequestContextProvider = {
6
- current?: () => unknown;
7
- peek?: () => unknown;
8
- };
9
-
10
- // Lazy reference to ZinTrust RequestContext — typed as unknown to stay runtime-agnostic.
11
- let _reqCtx: RequestContextProvider | undefined;
12
-
13
- const getRequestContext = (): RequestContextProvider | undefined => {
14
- return _reqCtx;
15
- };
16
-
17
- const setRequestContextImpl = (impl: RequestContextProvider): void => {
18
- _reqCtx = impl;
19
- };
20
-
21
- const isPromiseLike = (value: unknown): value is PromiseLike<unknown> => {
22
- return typeof value === 'object' && value !== null && 'then' in value;
23
- };
24
-
25
- const getCurrentContext = (): Record<string, unknown> | undefined => {
26
- const provider = getRequestContext();
27
- if (!provider) return undefined;
28
-
29
- let currentValue: unknown;
30
-
31
- if (typeof provider.peek === 'function') {
32
- currentValue = provider.peek();
33
- } else if (typeof provider.current === 'function') {
34
- currentValue = provider.current();
35
- } else {
36
- currentValue = undefined;
37
- }
38
-
39
- if (isPromiseLike(currentValue)) return undefined;
40
- if (typeof currentValue !== 'object' || currentValue === null) return undefined;
41
-
42
- return currentValue as Record<string, unknown>;
43
- };
44
-
45
- const getContextString = (key: 'traceId' | 'userId' | 'path'): string | undefined => {
46
- const value = getCurrentContext()?.[key];
47
- if (typeof value === 'string' && value.trim() !== '') return value;
48
- if (typeof value === 'number') return String(value);
49
- return undefined;
50
- };
51
-
52
- const getBatchId = (): string => {
53
- return getContextString('traceId') ?? crypto.randomUUID();
54
- };
55
-
56
- const getUserId = (): string | undefined => {
57
- return getContextString('userId');
58
- };
59
-
60
- const getRequestPath = (): string | undefined => {
61
- return getContextString('path');
62
- };
63
-
64
- const getHostname = (): string => {
65
- // Workers do not expose `os` or `process` — return 'worker' as fallback.
66
- if (typeof process !== 'undefined' && typeof process.env === 'object') {
67
- try {
68
- // Dynamic import avoids the need for a node-singletons wrapper at the type level.
69
- // Hostname is non-critical; we fall back gracefully.
70
- const hostname = (process.env as Record<string, string | undefined>)['HOSTNAME'];
71
- if (typeof hostname === 'string' && hostname.length > 0) return hostname;
72
- } catch {
73
- // fall through
74
- }
75
- return 'node';
76
- }
77
- return 'worker';
78
- };
79
-
80
- const getMemory = (): number | null => {
81
- if (typeof process !== 'undefined' && typeof process.memoryUsage === 'function') {
82
- try {
83
- return Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
84
- } catch {
85
- return null;
86
- }
87
- }
88
- return null;
89
- };
90
-
91
- const now = (): number => Date.now();
92
-
93
- export const TraceContext = Object.freeze({
94
- getBatchId,
95
- getUserId,
96
- getRequestPath,
97
- getHostname,
98
- getMemory,
99
- now,
100
- setRequestContextImpl,
101
- });
@@ -1,353 +0,0 @@
1
- /**
2
- * TraceDashboard handlers — pure handler functions wired to ITraceStorage.
3
- * No auth in this layer — caller mounts middleware as needed.
4
- */
5
- import type { IRequest, IResponse } from '@zintrust/core';
6
- import type { EntryTypeValue, ITraceEntry, ITraceStorage } from '../types';
7
-
8
- // ---------------------------------------------------------------------------
9
- // Storage holder (set once from routes.ts)
10
- // ---------------------------------------------------------------------------
11
-
12
- let _storage: ITraceStorage | null = null;
13
-
14
- export const setHandlerStorage = (s: ITraceStorage): void => {
15
- _storage = s;
16
- };
17
-
18
- // ---------------------------------------------------------------------------
19
- // Helpers
20
- // ---------------------------------------------------------------------------
21
-
22
- const requireStorage = (res: IResponse): boolean => {
23
- if (_storage) {
24
- return true;
25
- }
26
-
27
- res.setStatus(503).json({ error: 'Trace not initialised' });
28
- return false;
29
- };
30
-
31
- const getStorage = (res: IResponse): ITraceStorage | null => {
32
- if (requireStorage(res)) {
33
- return _storage;
34
- }
35
-
36
- return null;
37
- };
38
-
39
- const qp = (req: IRequest, key: string): string | undefined => {
40
- const v = req.getQueryParam(key);
41
- return Array.isArray(v) ? v[0] : v;
42
- };
43
-
44
- const qpInt = (req: IRequest, key: string, fallback: number): number => {
45
- const raw = qp(req, key);
46
- const n = typeof raw === 'string' ? Number.parseInt(raw, 10) : Number.NaN;
47
- return Number.isNaN(n) ? fallback : n;
48
- };
49
-
50
- const getNumericQueryParam = (req: IRequest, key: string): number | undefined => {
51
- const raw = qp(req, key);
52
- if (typeof raw === 'string') {
53
- const parsed = Number(raw);
54
- return Number.isFinite(parsed) ? parsed : undefined;
55
- }
56
-
57
- return undefined;
58
- };
59
-
60
- const DEFAULT_PER_PAGE = 50;
61
- const MAX_PER_PAGE = 100;
62
- const DEFAULT_REQUEST_PER_PAGE = 25;
63
- const MAX_REQUEST_PER_PAGE = 50;
64
- const DEFAULT_BATCH_PER_PAGE = 10;
65
- const MAX_BATCH_PER_PAGE = 25;
66
- const SUMMARY_TEXT_LIMIT = 280;
67
- const SUMMARY_ARRAY_LIMIT = 10;
68
- const REQUEST_BATCH_DEFAULT_EXCLUDED_TYPES: EntryTypeValue[] = [
69
- 'request',
70
- 'query',
71
- 'middleware',
72
- 'model',
73
- 'log',
74
- 'exception',
75
- 'client_request',
76
- 'cache',
77
- ];
78
-
79
- type CompactTraceEntry = ITraceEntry<Record<string, unknown>> & {
80
- hasDetails: true;
81
- contentBytes?: number;
82
- };
83
-
84
- const truncateText = (value: string, limit = SUMMARY_TEXT_LIMIT): string =>
85
- value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 3))}...`;
86
-
87
- const compactValue = (value: unknown): unknown => {
88
- if (typeof value === 'string') {
89
- return truncateText(value);
90
- }
91
-
92
- if (
93
- typeof value === 'number' ||
94
- typeof value === 'boolean' ||
95
- value === null ||
96
- value === undefined
97
- ) {
98
- return value;
99
- }
100
-
101
- if (Array.isArray(value)) {
102
- return value.slice(0, SUMMARY_ARRAY_LIMIT).map((item) => {
103
- if (typeof item === 'string') {
104
- return truncateText(item);
105
- }
106
-
107
- if (typeof item === 'number' || typeof item === 'boolean' || item === null) {
108
- return item;
109
- }
110
-
111
- return '[complex]';
112
- });
113
- }
114
-
115
- return undefined;
116
- };
117
-
118
- const pickCompactContent = (content: unknown, keys: readonly string[]): Record<string, unknown> => {
119
- if (typeof content !== 'object' || content === null || Array.isArray(content)) {
120
- return {};
121
- }
122
-
123
- const source = content as Record<string, unknown>;
124
- const compact: Record<string, unknown> = {};
125
-
126
- for (const key of keys) {
127
- const value = compactValue(source[key]);
128
- if (value !== undefined) {
129
- compact[key] = value;
130
- }
131
- }
132
-
133
- return compact;
134
- };
135
-
136
- const COMPACT_ENTRY_KEYS: Record<EntryTypeValue, readonly string[]> = {
137
- request: [
138
- 'method',
139
- 'uri',
140
- 'responseStatus',
141
- 'duration',
142
- 'memory',
143
- 'middleware',
144
- 'hostname',
145
- 'userId',
146
- ],
147
- query: ['connection', 'sql', 'time', 'duration', 'slow', 'hash', 'hostname'],
148
- exception: ['class', 'file', 'line', 'message', 'occurrences', 'hostname', 'userId'],
149
- log: ['level', 'message', 'hostname'],
150
- job: ['status', 'connection', 'queue', 'name', 'tries', 'timeout', 'hostname'],
151
- cache: ['operation', 'key', 'hit', 'store', 'payloadLogged', 'ttl', 'duration', 'hostname'],
152
- schedule: ['name', 'expression', 'status', 'duration', 'hostname'],
153
- mail: ['to', 'subject', 'template', 'hostname'],
154
- auth: ['event', 'userId', 'hostname'],
155
- event: ['name', 'listenerCount', 'hostname'],
156
- model: ['action', 'model', 'id', 'hostname'],
157
- notification: ['channels', 'notifiable', 'notification', 'message', 'hostname'],
158
- redis: ['command', 'duration', 'hostname'],
159
- gate: ['ability', 'result', 'userId', 'subject', 'hostname'],
160
- middleware: ['name', 'event', 'duration', 'hostname'],
161
- command: ['name', 'exitCode', 'duration', 'hostname'],
162
- batch: ['name', 'total', 'processed', 'failed', 'status', 'hostname'],
163
- dump: ['file', 'line', 'hostname'],
164
- view: ['template', 'duration', 'hostname'],
165
- client_request: ['source', 'method', 'url', 'responseStatus', 'error', 'duration', 'hostname'],
166
- };
167
-
168
- const compactEntryContent = (entry: ITraceEntry): Record<string, unknown> =>
169
- pickCompactContent(entry.content, COMPACT_ENTRY_KEYS[entry.type]);
170
-
171
- const estimateContentBytes = (content: unknown): number | undefined => {
172
- try {
173
- return new TextEncoder().encode(JSON.stringify(content)).length;
174
- } catch {
175
- return undefined;
176
- }
177
- };
178
-
179
- const compactListEntry = (entry: ITraceEntry): CompactTraceEntry => ({
180
- ...entry,
181
- content: compactEntryContent(entry),
182
- hasDetails: true,
183
- contentBytes: estimateContentBytes(entry.content),
184
- });
185
-
186
- const resolvePerPage = (req: IRequest, type?: EntryTypeValue): number => {
187
- const isRequestList = type === 'request';
188
- const fallback = isRequestList ? DEFAULT_REQUEST_PER_PAGE : DEFAULT_PER_PAGE;
189
- const limit = isRequestList ? MAX_REQUEST_PER_PAGE : MAX_PER_PAGE;
190
-
191
- return Math.max(1, Math.min(qpInt(req, 'perPage', fallback), limit));
192
- };
193
-
194
- // ---------------------------------------------------------------------------
195
- // Entry handlers
196
- // ---------------------------------------------------------------------------
197
-
198
- export async function listEntries(req: IRequest, res: IResponse): Promise<void> {
199
- const storage = getStorage(res);
200
- if (storage !== null) {
201
- const type = qp(req, 'type') as EntryTypeValue | undefined;
202
- const opts = {
203
- type,
204
- tag: qp(req, 'tag'),
205
- batchId: qp(req, 'batchId'),
206
- from: getNumericQueryParam(req, 'from'),
207
- to: getNumericQueryParam(req, 'to'),
208
- page: Math.max(1, qpInt(req, 'page', 1)),
209
- perPage: resolvePerPage(req, type),
210
- summary: true,
211
- };
212
- try {
213
- const result = await storage.queryEntries(opts);
214
- res.json({
215
- ok: true,
216
- data: result.data.map(compactListEntry),
217
- total: result.total,
218
- page: opts.page,
219
- perPage: opts.perPage,
220
- });
221
- } catch (err) {
222
- res.setStatus(500).json({ error: (err as Error).message });
223
- }
224
- }
225
- }
226
-
227
- export async function getEntry(req: IRequest, res: IResponse): Promise<void> {
228
- const storage = getStorage(res);
229
- if (storage === null) return;
230
- const uuid = req.getParam('uuid');
231
- if (uuid) {
232
- try {
233
- const entry = await storage.getEntry(uuid);
234
- if (entry) {
235
- res.json({ ok: true, entry });
236
- return;
237
- }
238
-
239
- res.setStatus(404).json({ error: 'Not found' });
240
- return;
241
- } catch (err) {
242
- res.setStatus(500).json({ error: (err as Error).message });
243
- return;
244
- }
245
- }
246
-
247
- res.setStatus(400).json({ error: 'uuid required' });
248
- }
249
-
250
- export async function getBatch(req: IRequest, res: IResponse): Promise<void> {
251
- const storage = getStorage(res);
252
- if (storage === null) return;
253
- const batchId = req.getParam('batchId');
254
- if (batchId) {
255
- try {
256
- const scope = qp(req, 'scope');
257
- const type = qp(req, 'type') as EntryTypeValue | undefined;
258
- const countsOnly = qp(req, 'countsOnly') === 'true';
259
- const page = Math.max(1, qpInt(req, 'page', 1));
260
- const perPage = Math.max(
261
- 1,
262
- Math.min(qpInt(req, 'perPage', DEFAULT_BATCH_PER_PAGE), MAX_BATCH_PER_PAGE)
263
- );
264
- const result = await storage.queryBatchEntries(batchId, {
265
- type,
266
- excludeTypes: scope === 'other' ? REQUEST_BATCH_DEFAULT_EXCLUDED_TYPES : undefined,
267
- page,
268
- perPage,
269
- countsOnly,
270
- });
271
- res.json({ ok: true, ...result });
272
- return;
273
- } catch (err) {
274
- res.setStatus(500).json({ error: (err as Error).message });
275
- return;
276
- }
277
- }
278
-
279
- res.setStatus(400).json({ error: 'batchId required' });
280
- }
281
-
282
- export async function getStats(_req: IRequest, res: IResponse): Promise<void> {
283
- const storage = getStorage(res);
284
- if (storage === null) return;
285
- try {
286
- const stats = await storage.stats();
287
- res.json({ ok: true, stats });
288
- } catch (err) {
289
- res.setStatus(500).json({ error: (err as Error).message });
290
- }
291
- }
292
-
293
- export async function clearEntries(_req: IRequest, res: IResponse): Promise<void> {
294
- const storage = getStorage(res);
295
- if (storage === null) return;
296
- try {
297
- await storage.clear();
298
- res.json({ ok: true });
299
- } catch (err) {
300
- res.setStatus(500).json({ error: (err as Error).message });
301
- }
302
- }
303
-
304
- // ---------------------------------------------------------------------------
305
- // Monitoring handlers
306
- // ---------------------------------------------------------------------------
307
-
308
- export async function getMonitoring(_req: IRequest, res: IResponse): Promise<void> {
309
- const storage = getStorage(res);
310
- if (storage === null) return;
311
- try {
312
- const tags = await storage.getMonitoring();
313
- res.json({ ok: true, tags });
314
- } catch (err) {
315
- res.setStatus(500).json({ error: (err as Error).message });
316
- }
317
- }
318
-
319
- export async function addMonitoring(req: IRequest, res: IResponse): Promise<void> {
320
- const storage = getStorage(res);
321
- if (storage === null) return;
322
- const tag = req.getParam('tag');
323
- if (tag) {
324
- try {
325
- await storage.addMonitoring(tag);
326
- res.json({ ok: true });
327
- return;
328
- } catch (err) {
329
- res.setStatus(500).json({ error: (err as Error).message });
330
- return;
331
- }
332
- }
333
-
334
- res.setStatus(400).json({ error: 'tag required' });
335
- }
336
-
337
- export async function removeMonitoring(req: IRequest, res: IResponse): Promise<void> {
338
- const storage = getStorage(res);
339
- if (storage === null) return;
340
- const tag = req.getParam('tag');
341
- if (tag) {
342
- try {
343
- await storage.removeMonitoring(tag);
344
- res.json({ ok: true });
345
- return;
346
- } catch (err) {
347
- res.setStatus(500).json({ error: (err as Error).message });
348
- return;
349
- }
350
- }
351
-
352
- res.setStatus(400).json({ error: 'tag required' });
353
- }
@@ -1,114 +0,0 @@
1
- /**
2
- * Dashboard route registrar for @zintrust/trace.
3
- * Mounts the SPA + all REST API endpoints under the configured basePath.
4
- * Auth is NOT applied here — callers add middleware via routeOptions.
5
- */
6
- import {
7
- appConfig,
8
- ErrorFactory,
9
- Router,
10
- useDatabase,
11
- type IRouter,
12
- type RouteOptions,
13
- } from '@zintrust/core';
14
- import { TraceConfig } from '../config';
15
- import { TraceStorage } from '../storage';
16
- import {
17
- assertTraceConnectionResolved,
18
- resolveDashboardTraceConnectionName,
19
- } from '../TraceConnection';
20
- import type { ITraceStorage } from '../types';
21
- import {
22
- addMonitoring,
23
- clearEntries,
24
- getBatch,
25
- getEntry,
26
- getMonitoring,
27
- getStats,
28
- listEntries,
29
- removeMonitoring,
30
- setHandlerStorage,
31
- } from './handlers';
32
- import { buildDashboardHtml } from './ui';
33
-
34
- type HtmlResponse = {
35
- html(body: string): void;
36
- };
37
-
38
- export type TraceDashboardOptions = {
39
- /** Base path for the dashboard, e.g. '/trace'. Defaults to '/trace'. */
40
- basePath?: string;
41
- /** Optional ZinTrust middleware names to apply to all routes. */
42
- middleware?: ReadonlyArray<string>;
43
- };
44
-
45
- export type TraceDashboardRegistrationOptions = TraceDashboardOptions & {
46
- /** Optional trace storage connection override. Defaults to TraceConfig / runtime default. */
47
- connectionName?: string;
48
- };
49
-
50
- export const registerTraceRoutes = (
51
- router: IRouter,
52
- storage: ITraceStorage,
53
- options: TraceDashboardOptions = {}
54
- ): void => {
55
- setHandlerStorage(storage);
56
-
57
- const base = options.basePath ?? '/trace';
58
- const routeOptions: RouteOptions | undefined =
59
- (options.middleware?.length ?? 0) > 0
60
- ? ({ middleware: options.middleware } as RouteOptions)
61
- : undefined;
62
-
63
- // SPA shell
64
- Router.get(
65
- router,
66
- base,
67
- (_req: unknown, res: HtmlResponse) => {
68
- res.html(buildDashboardHtml(base, appConfig.name));
69
- },
70
- routeOptions
71
- );
72
- // Serve the SPA for any /<basePath>/* sub-path (client-side routing)
73
- Router.get(
74
- router,
75
- `${base}/*`,
76
- (_req: unknown, res: HtmlResponse) => {
77
- res.html(buildDashboardHtml(base, appConfig.name));
78
- },
79
- routeOptions
80
- );
81
-
82
- // REST API
83
- Router.group(router, `${base}/api`, (r: IRouter) => {
84
- Router.get(r, '/entries', listEntries, routeOptions);
85
- Router.get(r, '/entries/:uuid', getEntry, routeOptions);
86
- Router.del(r, '/entries', clearEntries, routeOptions);
87
- Router.get(r, '/batch/:batchId', getBatch, routeOptions);
88
- Router.get(r, '/stats', getStats, routeOptions);
89
- Router.get(r, '/monitoring', getMonitoring, routeOptions);
90
- Router.post(r, '/monitoring/:tag', addMonitoring, routeOptions);
91
- Router.del(r, '/monitoring/:tag', removeMonitoring, routeOptions);
92
- });
93
- };
94
-
95
- export const registerTraceDashboard = (
96
- router: IRouter,
97
- options: TraceDashboardRegistrationOptions = {}
98
- ): void => {
99
- const connectionName = resolveDashboardTraceConnectionName(
100
- { ErrorFactory },
101
- {
102
- explicitConnectionName: options.connectionName,
103
- configuredConnectionName: TraceConfig.merge().connection,
104
- }
105
- );
106
- const db = useDatabase(undefined, connectionName);
107
- assertTraceConnectionResolved({ ErrorFactory }, db, {
108
- connectionName,
109
- envKey: 'TRACE_DB_CONNECTION',
110
- });
111
- const storage = TraceStorage.resolveStorage(db);
112
-
113
- registerTraceRoutes(router, storage, options);
114
- };