@zintrust/trace 1.6.6 → 1.6.7

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 (49) hide show
  1. package/package.json +2 -3
  2. package/src/TraceConnection.ts +0 -182
  3. package/src/cli-register.ts +0 -63
  4. package/src/config.ts +0 -383
  5. package/src/context.ts +0 -101
  6. package/src/dashboard/handlers.ts +0 -353
  7. package/src/dashboard/routes.ts +0 -114
  8. package/src/dashboard/ui.ts +0 -1262
  9. package/src/dashboard/zintrust-debuger.svg +0 -30
  10. package/src/index.ts +0 -102
  11. package/src/ingest/TraceIngestGateway.ts +0 -414
  12. package/src/plugin.ts +0 -9
  13. package/src/register.ts +0 -702
  14. package/src/storage/ProxyTraceStorage.ts +0 -190
  15. package/src/storage/TraceContentBudget.ts +0 -493
  16. package/src/storage/TraceContentRedaction.ts +0 -44
  17. package/src/storage/TraceEntryFiltering.ts +0 -50
  18. package/src/storage/TraceServiceTag.ts +0 -56
  19. package/src/storage/TraceStorage.ts +0 -543
  20. package/src/storage/TraceWriteDiagnostics.ts +0 -289
  21. package/src/storage/index.ts +0 -4
  22. package/src/types.ts +0 -430
  23. package/src/ui.ts +0 -9
  24. package/src/utils/authTag.ts +0 -20
  25. package/src/utils/entryFilter.ts +0 -131
  26. package/src/utils/familyHash.ts +0 -8
  27. package/src/utils/redact.ts +0 -112
  28. package/src/utils/requestFilter.ts +0 -79
  29. package/src/utils/stackFrame.ts +0 -44
  30. package/src/watchers/AuthWatcher.ts +0 -53
  31. package/src/watchers/BatchWatcher.ts +0 -55
  32. package/src/watchers/CacheWatcher.ts +0 -72
  33. package/src/watchers/CommandWatcher.ts +0 -58
  34. package/src/watchers/DumpWatcher.ts +0 -45
  35. package/src/watchers/EventWatcher.ts +0 -46
  36. package/src/watchers/ExceptionWatcher.ts +0 -130
  37. package/src/watchers/GateWatcher.ts +0 -53
  38. package/src/watchers/HttpClientWatcher.ts +0 -219
  39. package/src/watchers/HttpWatcher.ts +0 -249
  40. package/src/watchers/JobWatcher.ts +0 -124
  41. package/src/watchers/LogWatcher.ts +0 -120
  42. package/src/watchers/MailWatcher.ts +0 -65
  43. package/src/watchers/MiddlewareWatcher.ts +0 -54
  44. package/src/watchers/ModelWatcher.ts +0 -60
  45. package/src/watchers/NotificationWatcher.ts +0 -60
  46. package/src/watchers/QueryWatcher.ts +0 -105
  47. package/src/watchers/RedisWatcher.ts +0 -42
  48. package/src/watchers/ScheduleWatcher.ts +0 -57
  49. package/src/watchers/ViewWatcher.ts +0 -40
@@ -1,46 +0,0 @@
1
- import { TraceContext } from '../context';
2
- import type { EventContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
3
- import { EntryType } from '../types';
4
- import { AuthTag } from '../utils/authTag';
5
- import { RequestFilter } from '../utils/requestFilter';
6
-
7
- let _storage: ITraceWatcherConfig['storage'] | null = null;
8
- let _ignoreRoutes: string[] = [];
9
- let _ignorePaths: string[] = [];
10
-
11
- const emit = (name: string, listenerCount: number, payload?: unknown): void => {
12
- if (!_storage) return;
13
- if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes, _ignorePaths)) return;
14
- const content: EventContent = {
15
- name,
16
- payload,
17
- listenerCount,
18
- hostname: TraceContext.getHostname(),
19
- };
20
- _storage
21
- .writeEntry({
22
- uuid: crypto.randomUUID(),
23
- batchId: TraceContext.getBatchId(),
24
- type: EntryType.EVENT,
25
- content,
26
- tags: AuthTag.append([name]),
27
- isLatest: true,
28
- createdAt: TraceContext.now(),
29
- })
30
- .catch(() => undefined);
31
- };
32
-
33
- export const EventWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
34
- emit,
35
- register({ storage, config }: ITraceWatcherConfig): () => void {
36
- if (config.watchers.event === false) return () => undefined;
37
- _storage = storage;
38
- _ignoreRoutes = config.ignoreRoutes;
39
- _ignorePaths = config.ignorePaths;
40
- return () => {
41
- _storage = null;
42
- _ignoreRoutes = [];
43
- _ignorePaths = [];
44
- };
45
- },
46
- });
@@ -1,130 +0,0 @@
1
- /**
2
- * ExceptionWatcher — captures unhandled exceptions by hooking into the
3
- * framework error middleware. Core must call ExceptionWatcher.capture()
4
- * from within its error handler, or the register() side-effect adds a
5
- * process-level unhandledRejection/uncaughtException listener as fallback.
6
- */
7
- import { TraceContext } from '../context';
8
- import type { ExceptionContent, ITraceWatcher, ITraceWatcherConfig } from '../types';
9
- import { EntryType } from '../types';
10
- import { AuthTag } from '../utils/authTag';
11
- import { familyHash } from '../utils/familyHash';
12
- import { RequestFilter } from '../utils/requestFilter';
13
- import { parseStackFrameLine } from '../utils/stackFrame';
14
-
15
- type ExceptionCaptureContext = {
16
- batchId?: string;
17
- hostname?: string;
18
- path?: string;
19
- userId?: string;
20
- };
21
-
22
- const getLinePreview = (_file: string, _line: number): Record<string, string> => {
23
- return {};
24
- };
25
-
26
- const buildContent = (err: Error, context?: ExceptionCaptureContext): ExceptionContent => {
27
- const stack = err.stack ?? '';
28
- const trace: ExceptionContent['trace'] = stack
29
- .split('\n')
30
- .slice(1)
31
- .map(parseStackFrameLine)
32
- .filter((x): x is { file: string; line: number } => x !== null)
33
- .slice(0, 20);
34
-
35
- const firstFrame = trace[0];
36
-
37
- return {
38
- class: err.constructor?.name ?? 'Error',
39
- file: firstFrame?.file ?? 'unknown',
40
- line: firstFrame?.line ?? 0,
41
- message: err.message,
42
- trace,
43
- linePreview: firstFrame ? getLinePreview(firstFrame.file, firstFrame.line) : {},
44
- occurrences: 1,
45
- hostname: context?.hostname ?? TraceContext.getHostname(),
46
- userId: context?.userId ?? TraceContext.getUserId(),
47
- };
48
- };
49
-
50
- let _storage: ITraceWatcherConfig['storage'] | null = null;
51
- let _listenerRefCount = 0;
52
- let _ignoreRoutes: string[] = [];
53
- let _ignorePaths: string[] = [];
54
-
55
- const handleUncaughtException = (error: unknown): void => {
56
- captureException(error);
57
- };
58
-
59
- const handleUnhandledRejection = (reason: unknown): void => {
60
- captureException(reason);
61
- };
62
-
63
- const registerProcessListeners = (): void => {
64
- if (typeof process === 'undefined') return;
65
- process.on('uncaughtException', handleUncaughtException);
66
- process.on('unhandledRejection', handleUnhandledRejection);
67
- };
68
-
69
- const unregisterProcessListeners = (): void => {
70
- if (typeof process === 'undefined') return;
71
- process.off('uncaughtException', handleUncaughtException);
72
- process.off('unhandledRejection', handleUnhandledRejection);
73
- };
74
-
75
- const captureException = (err: unknown, context?: ExceptionCaptureContext): void => {
76
- const storage = _storage;
77
- if (!storage) return;
78
- if (!(err instanceof Error)) return;
79
- if (context?.path !== undefined) {
80
- if (RequestFilter.matchesIgnoredPath(context.path, _ignoreRoutes, _ignorePaths)) return;
81
- } else if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes, _ignorePaths)) {
82
- return;
83
- }
84
-
85
- const content = buildContent(err, context);
86
- const hash = familyHash(`${content.class}:${content.file}:${content.line}`);
87
- const uuid = crypto.randomUUID();
88
-
89
- storage
90
- .writeEntry({
91
- uuid,
92
- batchId: context?.batchId ?? TraceContext.getBatchId(),
93
- familyHash: hash,
94
- type: EntryType.EXCEPTION,
95
- content,
96
- tags: AuthTag.append([content.class]),
97
- isLatest: true,
98
- createdAt: TraceContext.now(),
99
- })
100
- .then(() => storage.markFamilyStale(hash, uuid))
101
- .catch(() => undefined);
102
- };
103
-
104
- export const ExceptionWatcher: ITraceWatcher & {
105
- capture: (err: unknown, context?: ExceptionCaptureContext) => void;
106
- } = Object.freeze({
107
- capture: captureException,
108
-
109
- register({ storage, config }: ITraceWatcherConfig): () => void {
110
- if (config.watchers.exception === false) return () => undefined;
111
- _storage = storage;
112
- _ignoreRoutes = config.ignoreRoutes;
113
- _ignorePaths = config.ignorePaths;
114
-
115
- if (_listenerRefCount === 0) {
116
- registerProcessListeners();
117
- }
118
- _listenerRefCount += 1;
119
-
120
- return () => {
121
- _listenerRefCount = Math.max(0, _listenerRefCount - 1);
122
- if (_listenerRefCount === 0) {
123
- unregisterProcessListeners();
124
- }
125
- _storage = null;
126
- _ignoreRoutes = [];
127
- _ignorePaths = [];
128
- };
129
- },
130
- });
@@ -1,53 +0,0 @@
1
- import { TraceContext } from '../context';
2
- import type { GateContent, 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
- let _ignorePaths: string[] = [];
9
-
10
- const emit = (
11
- ability: string,
12
- result: GateContent['result'],
13
- userId?: string,
14
- subject?: string
15
- ): void => {
16
- if (!_storage) return;
17
- if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes, _ignorePaths)) return;
18
- const tags: string[] = [ability, result];
19
- if (userId) tags.push(`Auth:${userId}`);
20
- const content: GateContent = {
21
- ability,
22
- result,
23
- userId,
24
- subject,
25
- hostname: TraceContext.getHostname(),
26
- };
27
- _storage
28
- .writeEntry({
29
- uuid: crypto.randomUUID(),
30
- batchId: TraceContext.getBatchId(),
31
- type: EntryType.GATE,
32
- content,
33
- tags,
34
- isLatest: true,
35
- createdAt: TraceContext.now(),
36
- })
37
- .catch(() => undefined);
38
- };
39
-
40
- export const GateWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
41
- emit,
42
- register({ storage, config }: ITraceWatcherConfig): () => void {
43
- if (config.watchers.gate === false) return () => undefined;
44
- _storage = storage;
45
- _ignoreRoutes = config.ignoreRoutes;
46
- _ignorePaths = config.ignorePaths;
47
- return () => {
48
- _storage = null;
49
- _ignoreRoutes = [];
50
- _ignorePaths = [];
51
- };
52
- },
53
- });
@@ -1,219 +0,0 @@
1
- import { TraceContext } from '../context';
2
- import {
3
- EntryType,
4
- type ClientRequestContent,
5
- type ClientRequestTraceInput,
6
- type ITraceWatcher,
7
- type ITraceWatcherConfig,
8
- type TraceClientRequestCaptureRule,
9
- type TraceClientRequestWatcherConfig,
10
- } from '../types';
11
- import { AuthTag } from '../utils/authTag';
12
- import { redactHeaders, redactUnknown } from '../utils/redact';
13
- import { RequestFilter } from '../utils/requestFilter';
14
-
15
- let _storage: ITraceWatcherConfig['storage'] | null = null;
16
- let _redactHeaderNames: string[] = [];
17
- let _redactBodyFields: string[] = [];
18
- let _ignoreRoutes: string[] = [];
19
- let _ignorePaths: string[] = [];
20
- let _clientRequestWatcher: TraceClientRequestWatcherConfig | undefined;
21
-
22
- const isObjectValue = (value: unknown): value is Record<string, unknown> => {
23
- return typeof value === 'object' && value !== null && !Array.isArray(value);
24
- };
25
-
26
- const resolveSource = (value: unknown): string | undefined => {
27
- if (typeof value !== 'string') return undefined;
28
- const normalized = value.trim().toLowerCase();
29
- return normalized === '' ? undefined : normalized;
30
- };
31
-
32
- const resolveSourceRule = (
33
- source: string | undefined
34
- ): TraceClientRequestCaptureRule | undefined => {
35
- if (source === undefined) return undefined;
36
- return _clientRequestWatcher?.sources?.[source];
37
- };
38
-
39
- const shouldCaptureField = (
40
- field: keyof Pick<
41
- TraceClientRequestCaptureRule,
42
- 'requestHeaders' | 'requestBody' | 'responseHeaders' | 'responseBody'
43
- >,
44
- sourceRule: TraceClientRequestCaptureRule | undefined
45
- ): boolean => {
46
- const scoped = sourceRule?.[field];
47
- if (typeof scoped === 'boolean') return scoped;
48
- const global = _clientRequestWatcher?.[field];
49
- if (typeof global === 'boolean') return global;
50
- return true;
51
- };
52
-
53
- const buildRequestHeaders = (
54
- requestHeaders: Record<string, string>,
55
- sourceRule: TraceClientRequestCaptureRule | undefined
56
- ): Pick<ClientRequestContent, 'requestHeaders'> => {
57
- return shouldCaptureField('requestHeaders', sourceRule)
58
- ? { requestHeaders: redactHeaders(requestHeaders, _redactHeaderNames) }
59
- : { requestHeaders: {} };
60
- };
61
-
62
- const buildRequestBody = (
63
- requestBody: unknown,
64
- sourceRule: TraceClientRequestCaptureRule | undefined
65
- ): Partial<Pick<ClientRequestContent, 'requestBody'>> => {
66
- if (requestBody === undefined) return {};
67
- if (!shouldCaptureField('requestBody', sourceRule)) return {};
68
- return { requestBody: redactUnknown(requestBody, _redactBodyFields) };
69
- };
70
-
71
- const buildResponseHeaders = (
72
- responseHeaders: Record<string, string> | undefined,
73
- sourceRule: TraceClientRequestCaptureRule | undefined
74
- ): Partial<Pick<ClientRequestContent, 'responseHeaders'>> => {
75
- if (responseHeaders === undefined) return {};
76
- if (!shouldCaptureField('responseHeaders', sourceRule)) return {};
77
- return { responseHeaders: redactHeaders(responseHeaders, _redactHeaderNames) };
78
- };
79
-
80
- const buildResponseBody = (
81
- responseBody: unknown,
82
- sourceRule: TraceClientRequestCaptureRule | undefined
83
- ): Partial<Pick<ClientRequestContent, 'responseBody'>> => {
84
- if (responseBody === undefined) return {};
85
- if (!shouldCaptureField('responseBody', sourceRule)) return {};
86
- return { responseBody: redactUnknown(responseBody, _redactBodyFields) };
87
- };
88
-
89
- const applySource = (content: ClientRequestContent, normalizedSource: string | undefined): void => {
90
- if (normalizedSource !== undefined) {
91
- content.source = normalizedSource;
92
- }
93
- };
94
-
95
- const applyResponseStatus = (
96
- content: ClientRequestContent,
97
- responseStatus: number | undefined
98
- ): void => {
99
- if (responseStatus !== undefined) {
100
- content.responseStatus = responseStatus;
101
- }
102
- };
103
-
104
- const applyError = (content: ClientRequestContent, error: unknown): void => {
105
- if (typeof error === 'string' && error !== '') {
106
- content.error = error;
107
- }
108
- };
109
-
110
- const mergePartialContent = (
111
- content: ClientRequestContent,
112
- partial: Partial<ClientRequestContent>
113
- ): void => {
114
- Object.assign(content, partial);
115
- };
116
-
117
- const buildClientRequestContent = (
118
- input: ClientRequestTraceInput,
119
- sourceRule: TraceClientRequestCaptureRule | undefined,
120
- normalizedSource: string | undefined
121
- ): ClientRequestContent => {
122
- const content: ClientRequestContent = {
123
- method: input.method.toUpperCase(),
124
- url: input.url,
125
- requestHeaders: {},
126
- duration: input.duration,
127
- hostname: TraceContext.getHostname(),
128
- };
129
-
130
- applySource(content, normalizedSource);
131
- mergePartialContent(content, buildRequestHeaders(input.requestHeaders, sourceRule));
132
- mergePartialContent(content, buildRequestBody(input.requestBody, sourceRule));
133
- applyResponseStatus(content, input.responseStatus);
134
- mergePartialContent(content, buildResponseHeaders(input.responseHeaders, sourceRule));
135
- mergePartialContent(content, buildResponseBody(input.responseBody, sourceRule));
136
- applyError(content, input.error);
137
-
138
- return content;
139
- };
140
-
141
- const isWatcherEnabled = (
142
- value: ITraceWatcherConfig['config']['watchers']['clientRequest']
143
- ): boolean => {
144
- if (value === false) return false;
145
- if (isObjectValue(value) && value.enabled === false) return false;
146
- return true;
147
- };
148
-
149
- const emit = ({
150
- source,
151
- method,
152
- url,
153
- requestHeaders,
154
- responseStatus,
155
- duration,
156
- requestBody,
157
- responseHeaders,
158
- responseBody,
159
- error,
160
- }: ClientRequestTraceInput): void => {
161
- if (!_storage) return;
162
- if (RequestFilter.shouldIgnoreCurrentRequest(_ignoreRoutes, _ignorePaths)) return;
163
- const normalizedSource = resolveSource(source);
164
- const sourceRule = resolveSourceRule(normalizedSource);
165
- if (sourceRule?.enabled === false) return;
166
- const tags = AuthTag.append([method.toUpperCase()]);
167
- if ((responseStatus ?? 0) >= 400 || error) tags.push('failed');
168
- if (normalizedSource !== undefined) tags.push(normalizedSource);
169
- const content = buildClientRequestContent(
170
- {
171
- source,
172
- method,
173
- url,
174
- requestHeaders,
175
- responseStatus,
176
- duration,
177
- requestBody,
178
- responseHeaders,
179
- responseBody,
180
- error,
181
- },
182
- sourceRule,
183
- normalizedSource
184
- );
185
- _storage
186
- .writeEntry({
187
- uuid: crypto.randomUUID(),
188
- batchId: TraceContext.getBatchId(),
189
- type: EntryType.CLIENT_REQUEST,
190
- content,
191
- tags,
192
- isLatest: true,
193
- createdAt: TraceContext.now(),
194
- })
195
- .catch(() => undefined);
196
- };
197
-
198
- export const HttpClientWatcher: ITraceWatcher & { emit: typeof emit } = Object.freeze({
199
- emit,
200
- register({ storage, config }: ITraceWatcherConfig): () => void {
201
- if (!isWatcherEnabled(config.watchers.clientRequest)) return () => undefined;
202
- _storage = storage;
203
- _clientRequestWatcher =
204
- typeof config.watchers.clientRequest === 'object' && config.watchers.clientRequest !== null
205
- ? config.watchers.clientRequest
206
- : undefined;
207
- _redactHeaderNames = [...(config.redaction?.keys ?? []), ...(config.redaction?.headers ?? [])];
208
- _redactBodyFields = [...(config.redaction?.keys ?? []), ...(config.redaction?.body ?? [])];
209
- _ignoreRoutes = config.ignoreRoutes;
210
- _ignorePaths = config.ignorePaths;
211
- return () => {
212
- _storage = null;
213
- _clientRequestWatcher = undefined;
214
- _redactBodyFields = [];
215
- _ignoreRoutes = [];
216
- _ignorePaths = [];
217
- };
218
- },
219
- });
@@ -1,249 +0,0 @@
1
- /**
2
- * HttpWatcher — records inbound HTTP requests as trace entries.
3
- * Registers as a global middleware via Kernel.registerGlobalMiddleware().
4
- */
5
- import type { IRequest, IResponse } from '@zintrust/core';
6
- import { Logger } from '@zintrust/core';
7
- import { TraceContext } from '../context';
8
- import type { ITraceConfig, ITraceWatcher, ITraceWatcherConfig, RequestContent } from '../types';
9
- import { EntryType } from '../types';
10
- import { AuthTag } from '../utils/authTag';
11
- import { redactHeaders, redactObject, redactUnknown } from '../utils/redact';
12
- import { RequestFilter } from '../utils/requestFilter';
13
-
14
- const normalizeHeaders = (headers: IRequest['headers']): Record<string, string> => {
15
- if (!headers) return {};
16
-
17
- return Object.fromEntries(
18
- Object.entries(headers).flatMap(([key, value]) => {
19
- if (typeof value === 'string') return [[key, value]];
20
- if (Array.isArray(value)) return [[key, value.join(', ')]];
21
- return [];
22
- })
23
- );
24
- };
25
-
26
- const normalizeHeaderValue = (value: string | string[]): string => {
27
- return Array.isArray(value) ? value.join(', ') : value;
28
- };
29
-
30
- const resolveRouteMiddleware = (req: IRequest): string[] => {
31
- const middleware = req.context?.['traceRouteMiddleware'];
32
- return Array.isArray(middleware)
33
- ? middleware.filter((value): value is string => typeof value === 'string')
34
- : [];
35
- };
36
-
37
- const resolveRequestPayload = (req: IRequest, config: ITraceConfig): unknown => {
38
- const redactFields = [...config.redaction.keys, ...config.redaction.body];
39
- const requestBody = typeof req.getBody === 'function' ? req.getBody() : req.body;
40
-
41
- if (requestBody === undefined || requestBody === null) return {};
42
- if (typeof requestBody === 'object') {
43
- return redactObject(requestBody as Record<string, unknown>, redactFields);
44
- }
45
-
46
- return redactUnknown(requestBody, redactFields);
47
- };
48
-
49
- type ResponseCapture = {
50
- headers: Record<string, string>;
51
- body?: unknown;
52
- restore(): void;
53
- };
54
-
55
- type RawResponseWithLifecycle = ReturnType<IResponse['getRaw']> & {
56
- once?: (event: 'finish' | 'close', listener: () => void) => unknown;
57
- off?: (event: 'finish' | 'close', listener: () => void) => unknown;
58
- writableEnded?: boolean;
59
- finished?: boolean;
60
- };
61
-
62
- type CompletionHandlerRegistration = {
63
- attached: boolean;
64
- cleanup(): void;
65
- };
66
-
67
- const registerCompletionHandler = (
68
- response: IResponse,
69
- onComplete: () => void
70
- ): CompletionHandlerRegistration => {
71
- const raw: RawResponseWithLifecycle = response.getRaw();
72
- if (typeof raw.once !== 'function') {
73
- return { attached: false, cleanup: () => undefined };
74
- }
75
-
76
- let completed = false;
77
-
78
- const cleanup = (): void => {
79
- if (typeof raw.off === 'function') {
80
- raw.off('finish', markCompleted);
81
- raw.off('close', markCompleted);
82
- }
83
- };
84
-
85
- const markCompleted = (): void => {
86
- if (completed) return;
87
- completed = true;
88
- cleanup();
89
- onComplete();
90
- };
91
-
92
- raw.once('finish', markCompleted);
93
- raw.once('close', markCompleted);
94
-
95
- return { attached: true, cleanup };
96
- };
97
-
98
- const isResponseComplete = (response: IResponse): boolean => {
99
- const raw: RawResponseWithLifecycle = response.getRaw();
100
- return raw.writableEnded === true || raw.finished === true;
101
- };
102
-
103
- const captureResponse = (response: IResponse, config: ITraceConfig): ResponseCapture => {
104
- const headers: Record<string, string> = {};
105
- const redactFields = [...config.redaction.keys, ...config.redaction.body];
106
-
107
- const originalSetHeader = response.setHeader;
108
- const originalJson = response.json;
109
- const originalText = response.text;
110
- const originalHtml = response.html;
111
- const originalSend = response.send;
112
-
113
- const capture: ResponseCapture = {
114
- headers,
115
- body: undefined,
116
- restore(): void {
117
- response.setHeader = originalSetHeader;
118
- response.json = originalJson;
119
- response.text = originalText;
120
- response.html = originalHtml;
121
- response.send = originalSend;
122
- },
123
- };
124
-
125
- response.setHeader = function setHeader(name: string, value: string | string[]): IResponse {
126
- headers[name] = normalizeHeaderValue(value);
127
- return originalSetHeader.call(this, name, value);
128
- };
129
-
130
- response.json = function json(data: unknown): void {
131
- capture.body = redactUnknown(data, redactFields);
132
- originalJson.call(this, data);
133
- };
134
-
135
- response.text = function text(value: string): void {
136
- capture.body = value;
137
- originalText.call(this, value);
138
- };
139
-
140
- response.html = function html(value: string): void {
141
- capture.body = value;
142
- originalHtml.call(this, value);
143
- };
144
-
145
- response.send = function send(data: string | Buffer): void {
146
- capture.body = typeof data === 'string' ? data : `[binary ${data.length} bytes]`;
147
- originalSend.call(this, data);
148
- };
149
-
150
- return capture;
151
- };
152
-
153
- const buildEntry = (
154
- req: IRequest,
155
- res: IResponse,
156
- start: number,
157
- config: ITraceConfig,
158
- responseCapture: ResponseCapture
159
- ): RequestContent => {
160
- const headers = redactHeaders(normalizeHeaders(req.headers), [
161
- ...config.redaction.keys,
162
- ...config.redaction.headers,
163
- ]);
164
-
165
- return {
166
- method: req.getMethod(),
167
- uri: req.getPath(),
168
- headers,
169
- payload: resolveRequestPayload(req, config),
170
- responseStatus: res.getStatus(),
171
- responseHeaders: redactHeaders(responseCapture.headers, [
172
- ...config.redaction.keys,
173
- ...config.redaction.headers,
174
- ]),
175
- responseBody: responseCapture.body,
176
- duration: Date.now() - start,
177
- memory: TraceContext.getMemory(),
178
- middleware: resolveRouteMiddleware(req),
179
- hostname: TraceContext.getHostname(),
180
- userId: TraceContext.getUserId(),
181
- };
182
- };
183
-
184
- const shouldIgnore = (req: IRequest, config: ITraceConfig): boolean => {
185
- return RequestFilter.matchesIgnoredPath(req.getPath(), config);
186
- };
187
-
188
- const isWatcherEnabled = (config: ITraceConfig): boolean => config.watchers.request !== false;
189
-
190
- export const HttpWatcher: ITraceWatcher = Object.freeze({
191
- register({ storage, config, registerMiddleware }: ITraceWatcherConfig): () => void {
192
- if (!isWatcherEnabled(config)) return () => undefined;
193
- if (!registerMiddleware) return () => undefined;
194
-
195
- const middleware: Parameters<
196
- NonNullable<ITraceWatcherConfig['registerMiddleware']>
197
- >[0] = async (req: unknown, res: unknown, next: () => Promise<void>): Promise<void> => {
198
- const request = req as IRequest;
199
- const response = res as IResponse;
200
-
201
- if (shouldIgnore(request, config)) return next();
202
-
203
- const start = TraceContext.now();
204
- const batchId = TraceContext.getBatchId();
205
- const responseCapture = captureResponse(response, config);
206
- let didPersist = false;
207
-
208
- const persistEntry = (): void => {
209
- if (didPersist) return;
210
- didPersist = true;
211
-
212
- const content = buildEntry(request, response, start, config, responseCapture);
213
- const tags = AuthTag.append([]);
214
- if (content.responseStatus >= 500) tags.push('failed');
215
-
216
- responseCapture.restore();
217
-
218
- const entry = {
219
- uuid: crypto.randomUUID(),
220
- batchId,
221
- type: EntryType.REQUEST,
222
- content,
223
- tags,
224
- isLatest: true,
225
- createdAt: TraceContext.now(),
226
- };
227
-
228
- storage.writeEntry(entry).catch((error: unknown) => {
229
- Logger.warn('[trace] HttpWatcher writeEntry failed', {
230
- method: content.method,
231
- uri: content.uri,
232
- entryUuid: entry.uuid,
233
- error: error instanceof Error ? error.message : String(error),
234
- });
235
- }); // fire-and-forget
236
- };
237
-
238
- const completionHandler = registerCompletionHandler(response, persistEntry);
239
- await next();
240
-
241
- if (!completionHandler.attached || isResponseComplete(response)) {
242
- persistEntry();
243
- }
244
- };
245
-
246
- registerMiddleware(middleware);
247
- return () => undefined;
248
- },
249
- });