-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathhttp.ts
More file actions
369 lines (322 loc) · 14.9 KB
/
http.ts
File metadata and controls
369 lines (322 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
import type { ClientRequest, IncomingMessage, RequestOptions, ServerResponse } from 'node:http';
import { diag } from '@opentelemetry/api';
import type { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import type { Span } from '@sentry/core';
import {
defineIntegration,
getClient,
hasSpansEnabled,
SEMANTIC_ATTRIBUTE_URL_FULL,
stripDataUrlContent,
} from '@sentry/core';
import type { HTTPModuleRequestIncomingMessage, NodeClient, SentryHttpInstrumentationOptions } from '@sentry/node-core';
import {
addOriginToSpan,
generateInstrumentOnce,
getRequestUrl,
httpServerIntegration,
httpServerSpansIntegration,
NODE_VERSION,
SentryHttpInstrumentation,
} from '@sentry/node-core';
import type { NodeClientOptions } from '../types';
const INTEGRATION_NAME = 'Http';
const INSTRUMENTATION_NAME = '@opentelemetry_sentry-patched/instrumentation-http';
// The `http.client.request.created` diagnostics channel, needed for trace propagation,
// was added in Node 22.12.0 (backported from 23.2.0). Earlier 22.x versions don't have it.
const FULLY_SUPPORTS_HTTP_DIAGNOSTICS_CHANNEL =
(NODE_VERSION.major === 22 && NODE_VERSION.minor >= 12) ||
(NODE_VERSION.major === 23 && NODE_VERSION.minor >= 2) ||
NODE_VERSION.major >= 24;
interface HttpOptions {
/**
* Whether breadcrumbs should be recorded for outgoing requests.
* Defaults to true
*/
breadcrumbs?: boolean;
/**
* If set to false, do not emit any spans.
* This will ensure that the default HttpInstrumentation from OpenTelemetry is not setup,
* only the Sentry-specific instrumentation for request isolation is applied.
*
* If `skipOpenTelemetrySetup: true` is configured, this defaults to `false`, otherwise it defaults to `true`.
*/
spans?: boolean;
/**
* Whether the integration should create [Sessions](https://docs.sentry.io/product/releases/health/#sessions) for incoming requests to track the health and crash-free rate of your releases in Sentry.
* Read more about Release Health: https://docs.sentry.io/product/releases/health/
*
* Defaults to `true`.
*/
trackIncomingRequestsAsSessions?: boolean;
/**
* Number of milliseconds until sessions tracked with `trackIncomingRequestsAsSessions` will be flushed as a session aggregate.
*
* Defaults to `60000` (60s).
*/
sessionFlushingDelayMS?: number;
/**
* Whether to inject trace propagation headers (sentry-trace, baggage, traceparent) into outgoing HTTP requests.
*
* When set to `false`, Sentry will not inject any trace propagation headers, but will still create breadcrumbs
* (if `breadcrumbs` is enabled). This is useful when `skipOpenTelemetrySetup: true` is configured and you want
* to avoid duplicate trace headers being injected by both Sentry and OpenTelemetry's HttpInstrumentation.
*
* @default `true`
*/
tracePropagation?: boolean;
/**
* Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`.
* This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled.
*
* The `url` param contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request.
* For example: `'https://someService.com/users/details?id=123'`
*
* The `request` param contains the original {@type RequestOptions} object used to make the outgoing request.
* You can use it to filter on additional properties like method, headers, etc.
*/
ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean;
/**
* Do not capture spans for incoming HTTP requests to URLs where the given callback returns `true`.
* Spans will be non recording if tracing is disabled.
*
* The `urlPath` param consists of the URL path and query string (if any) of the incoming request.
* For example: `'/users/details?id=123'`
*
* The `request` param contains the original {@type IncomingMessage} object of the incoming request.
* You can use it to filter on additional properties like method, headers, etc.
*/
ignoreIncomingRequests?: (urlPath: string, request: IncomingMessage) => boolean;
/**
* A hook that can be used to mutate the span for incoming requests.
* This is triggered after the span is created, but before it is recorded.
*/
incomingRequestSpanHook?: (span: Span, request: IncomingMessage, response: ServerResponse) => void;
/**
* Whether to automatically ignore common static asset requests like favicon.ico, robots.txt, etc.
* This helps reduce noise in your transactions.
*
* @default `true`
*/
ignoreStaticAssets?: boolean;
/**
* Do not capture spans for incoming HTTP requests with the given status codes.
* By default, spans with some 3xx and 4xx status codes are ignored (see @default).
* Expects an array of status codes or a range of status codes, e.g. [[300,399], 404] would ignore 3xx and 404 status codes.
*
* @default `[[401, 404], [301, 303], [305, 399]]`
*/
dropSpansForIncomingRequestStatusCodes?: (number | [number, number])[];
/**
* Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`.
* This can be useful for long running requests where the body is not needed and we want to avoid capturing it.
*
* @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the incoming request.
* @param request Contains the {@type RequestOptions} object used to make the incoming request.
*/
ignoreIncomingRequestBody?: (url: string, request: RequestOptions) => boolean;
/**
* Controls the maximum size of incoming HTTP request bodies attached to events.
*
* Available options:
* - 'none': No request bodies will be attached
* - 'small': Request bodies up to 1,000 bytes will be attached
* - 'medium': Request bodies up to 10,000 bytes will be attached (default)
* - 'always': Request bodies will always be attached
*
* Note that even with 'always' setting, bodies exceeding 1MB will never be attached
* for performance and security reasons.
*
* @default 'medium'
*/
maxIncomingRequestBodySize?: 'none' | 'small' | 'medium' | 'always';
/**
* If true, do not generate spans for incoming requests at all.
* This is used by Remix to avoid generating spans for incoming requests, as it generates its own spans.
*/
disableIncomingRequestSpans?: boolean;
/**
* Additional instrumentation options that are passed to the underlying HttpInstrumentation.
*/
instrumentation?: {
requestHook?: (span: Span, req: ClientRequest | HTTPModuleRequestIncomingMessage) => void;
responseHook?: (span: Span, response: HTTPModuleRequestIncomingMessage | ServerResponse) => void;
applyCustomAttributesOnSpan?: (
span: Span,
request: ClientRequest | HTTPModuleRequestIncomingMessage,
response: HTTPModuleRequestIncomingMessage | ServerResponse,
) => void;
};
}
export const instrumentSentryHttp = generateInstrumentOnce<SentryHttpInstrumentationOptions>(
`${INTEGRATION_NAME}.sentry`,
options => {
return new SentryHttpInstrumentation(options);
},
);
export const instrumentOtelHttp = generateInstrumentOnce<HttpInstrumentationConfig>(INTEGRATION_NAME, config => {
const instrumentation = new HttpInstrumentation({
...config,
// This is hard-coded and can never be overridden by the user
disableIncomingRequestInstrumentation: true,
});
// We want to update the logger namespace so we can better identify what is happening here
try {
instrumentation['_diag'] = diag.createComponentLogger({
namespace: INSTRUMENTATION_NAME,
});
// @ts-expect-error We are writing a read-only property here...
instrumentation.instrumentationName = INSTRUMENTATION_NAME;
} catch {
// ignore errors here...
}
// The OTel HttpInstrumentation (>=0.213.0) has a guard (`_httpPatched`/`_httpsPatched`)
// that prevents patching `http`/`https` when loaded by both CJS `require()` and ESM `import`.
// In environments like AWS Lambda, the runtime loads `http` via CJS first (for the Runtime API),
// and then the user's ESM handler imports `node:http`. The guard blocks ESM patching after CJS,
// which breaks HTTP spans for ESM handlers. We disable this guard to allow both to be patched.
// TODO(andrei): Remove once https://github.com/open-telemetry/opentelemetry-js/issues/6489 is fixed.
try {
const noopDescriptor = { get: () => false, set: () => {} };
Object.defineProperty(instrumentation, '_httpPatched', noopDescriptor);
Object.defineProperty(instrumentation, '_httpsPatched', noopDescriptor);
} catch {
// ignore errors here...
}
return instrumentation;
});
/** Exported only for tests. */
export function _shouldUseOtelHttpInstrumentation(
options: HttpOptions,
clientOptions: Partial<NodeClientOptions> = {},
): boolean {
// If `spans` is passed in, it takes precedence
// Else, we by default emit spans, unless `skipOpenTelemetrySetup` is set to `true` or spans are not enabled
if (typeof options.spans === 'boolean') {
return options.spans;
}
if (clientOptions.skipOpenTelemetrySetup) {
return false;
}
// IMPORTANT: We only disable span instrumentation when spans are not enabled _and_ we are on a Node version
// that fully supports the necessary diagnostics channels for trace propagation
if (!hasSpansEnabled(clientOptions) && FULLY_SUPPORTS_HTTP_DIAGNOSTICS_CHANNEL) {
return false;
}
return true;
}
/**
* The http integration instruments Node's internal http and https modules.
* It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span.
*/
export const httpIntegration = defineIntegration((options: HttpOptions = {}) => {
const spans = options.spans ?? true;
const disableIncomingRequestSpans = options.disableIncomingRequestSpans;
const serverOptions = {
sessions: options.trackIncomingRequestsAsSessions,
sessionFlushingDelayMS: options.sessionFlushingDelayMS,
ignoreRequestBody: options.ignoreIncomingRequestBody,
maxRequestBodySize: options.maxIncomingRequestBodySize,
} satisfies Parameters<typeof httpServerIntegration>[0];
const serverSpansOptions = {
ignoreIncomingRequests: options.ignoreIncomingRequests,
ignoreStaticAssets: options.ignoreStaticAssets,
ignoreStatusCodes: options.dropSpansForIncomingRequestStatusCodes,
instrumentation: options.instrumentation,
onSpanCreated: options.incomingRequestSpanHook,
} satisfies Parameters<typeof httpServerSpansIntegration>[0];
const server = httpServerIntegration(serverOptions);
const serverSpans = httpServerSpansIntegration(serverSpansOptions);
const enableServerSpans = spans && !disableIncomingRequestSpans;
return {
name: INTEGRATION_NAME,
setup(client: NodeClient) {
const clientOptions = client.getOptions();
if (enableServerSpans && hasSpansEnabled(clientOptions)) {
serverSpans.setup(client);
}
},
setupOnce() {
const clientOptions = (getClient<NodeClient>()?.getOptions() || {}) satisfies Partial<NodeClientOptions>;
const useOtelHttpInstrumentation = _shouldUseOtelHttpInstrumentation(options, clientOptions);
server.setupOnce();
const sentryHttpInstrumentationOptions = {
breadcrumbs: options.breadcrumbs,
propagateTraceInOutgoingRequests:
typeof options.tracePropagation === 'boolean'
? options.tracePropagation
: FULLY_SUPPORTS_HTTP_DIAGNOSTICS_CHANNEL || !useOtelHttpInstrumentation,
createSpansForOutgoingRequests: FULLY_SUPPORTS_HTTP_DIAGNOSTICS_CHANNEL,
spans: options.spans,
ignoreOutgoingRequests: options.ignoreOutgoingRequests,
outgoingRequestHook: (span: Span, request: ClientRequest) => {
// Sanitize data URLs to prevent long base64 strings in span attributes
const url = getRequestUrl(request);
if (url.startsWith('data:')) {
const sanitizedUrl = stripDataUrlContent(url);
span.setAttribute('http.url', sanitizedUrl);
span.setAttribute(SEMANTIC_ATTRIBUTE_URL_FULL, sanitizedUrl);
span.updateName(`${request.method || 'GET'} ${sanitizedUrl}`);
}
options.instrumentation?.requestHook?.(span, request);
},
outgoingResponseHook: options.instrumentation?.responseHook,
outgoingRequestApplyCustomAttributes: options.instrumentation?.applyCustomAttributesOnSpan,
} satisfies SentryHttpInstrumentationOptions;
// This is Sentry-specific instrumentation for outgoing request breadcrumbs & trace propagation
instrumentSentryHttp(sentryHttpInstrumentationOptions);
// This is the "regular" OTEL instrumentation that emits outgoing request spans
if (useOtelHttpInstrumentation) {
const instrumentationConfig = getConfigWithDefaults(options);
instrumentOtelHttp(instrumentationConfig);
}
},
processEvent(event) {
// Note: We always run this, even if spans are disabled
// The reason being that e.g. the remix integration disables span creation here but still wants to use the ignore status codes option
return serverSpans.processEvent(event);
},
};
});
function getConfigWithDefaults(options: Partial<HttpOptions> = {}): HttpInstrumentationConfig {
const instrumentationConfig = {
// This is handled by the SentryHttpInstrumentation on Node 22+
disableOutgoingRequestInstrumentation: FULLY_SUPPORTS_HTTP_DIAGNOSTICS_CHANNEL,
ignoreOutgoingRequestHook: request => {
const url = getRequestUrl(request);
if (!url) {
return false;
}
const _ignoreOutgoingRequests = options.ignoreOutgoingRequests;
if (_ignoreOutgoingRequests?.(url, request)) {
return true;
}
return false;
},
requireParentforOutgoingSpans: false,
requestHook: (span, req) => {
addOriginToSpan(span, 'auto.http.otel.http');
// Sanitize data URLs to prevent long base64 strings in span attributes
const url = getRequestUrl(req as ClientRequest);
if (url.startsWith('data:')) {
const sanitizedUrl = stripDataUrlContent(url);
span.setAttribute('http.url', sanitizedUrl);
span.setAttribute(SEMANTIC_ATTRIBUTE_URL_FULL, sanitizedUrl);
span.updateName(`${(req as ClientRequest).method || 'GET'} ${sanitizedUrl}`);
}
options.instrumentation?.requestHook?.(span, req);
},
responseHook: (span, res) => {
options.instrumentation?.responseHook?.(span, res);
},
applyCustomAttributesOnSpan: (
span: Span,
request: ClientRequest | HTTPModuleRequestIncomingMessage,
response: HTTPModuleRequestIncomingMessage | ServerResponse,
) => {
options.instrumentation?.applyCustomAttributesOnSpan?.(span, request, response);
},
} satisfies HttpInstrumentationConfig;
return instrumentationConfig;
}