Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 67942ee

Browse files
committed
WIP WIP
1 parent 08fb932 commit 67942ee

File tree

2 files changed

+254
-8
lines changed

2 files changed

+254
-8
lines changed

‎packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts

Lines changed: 247 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
/* eslint-disable max-lines */
22
import type { ChannelListener } from 'node:diagnostics_channel';
33
import { subscribe, unsubscribe } from 'node:diagnostics_channel';
4+
import { errorMonitor } from 'node:events';
45
import type * as http from 'node:http';
56
import type * as https from 'node:https';
67
import type { EventEmitter } from 'node:stream';
7-
import { context, propagation } from '@opentelemetry/api';
8+
import { context, propagation,SpanStatusCode,trace } from '@opentelemetry/api';
89
import { isTracingSuppressed } from '@opentelemetry/core';
910
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
1011
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
11-
import type { AggregationCounts, Client, SanitizedRequestData, Scope } from '@sentry/core';
12+
import {
13+
ATTR_HTTP_RESPONSE_STATUS_CODE,
14+
ATTR_NETWORK_PEER_ADDRESS,
15+
ATTR_NETWORK_PEER_PORT,
16+
ATTR_NETWORK_PROTOCOL_VERSION,
17+
ATTR_NETWORK_TRANSPORT,
18+
ATTR_URL_FULL,
19+
ATTR_USER_AGENT_ORIGINAL,
20+
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH,
21+
SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
22+
} from '@opentelemetry/semantic-conventions';
23+
import type { AggregationCounts, Client, SanitizedRequestData, Scope, SpanAttributes, SpanStatus } from '@sentry/core';
1224
import {
1325
addBreadcrumb,
1426
addNonEnumerableProperty,
@@ -17,26 +29,32 @@ import {
1729
getBreadcrumbLogLevelFromHttpStatusCode,
1830
getClient,
1931
getCurrentScope,
32+
getHttpSpanDetailsFromUrlObject,
2033
getIsolationScope,
2134
getSanitizedUrlString,
35+
getSpanStatusFromHttpCode,
2236
getTraceData,
2337
httpRequestToRequestData,
2438
isError,
2539
LRUMap,
40+
parseStringToURLObject,
2641
parseUrl,
2742
SDK_VERSION,
43+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
44+
startInactiveSpan,
2845
stripUrlQueryAndFragment,
2946
withIsolationScope,
3047
} from '@sentry/core';
3148
import { shouldPropagateTraceForUrl } from '@sentry/opentelemetry';
3249
import { DEBUG_BUILD } from '../../debug-build';
3350
import { mergeBaggageHeaders } from '../../utils/baggage';
34-
import { getRequestUrl } from '../../utils/getRequestUrl';
3551

3652
const INSTRUMENTATION_NAME = '@sentry/instrumentation-http';
3753

3854
type Http = typeof http;
3955
type Https = typeof https;
56+
type IncomingHttpHeaders = http.IncomingHttpHeaders;
57+
type OutgoingHttpHeaders = http.OutgoingHttpHeaders;
4058

4159
export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
4260
/**
@@ -46,6 +64,13 @@ export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
4664
*/
4765
breadcrumbs?: boolean;
4866

67+
/**
68+
* Whether to create spans for outgoing requests.
69+
*
70+
* @default `true`
71+
*/
72+
spans?: boolean;
73+
4974
/**
5075
* Whether to extract the trace ID from the `sentry-trace` header for incoming requests.
5176
* By default this is done by the HttpInstrumentation, but if that is not added (e.g. because tracing is disabled, ...)
@@ -169,6 +194,11 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
169194
this._onOutgoingRequestCreated(data.request);
170195
}) satisfies ChannelListener;
171196

197+
const onHttpClientRequestStart = ((_data: unknown) => {
198+
const data = _data as { request: http.ClientRequest };
199+
this._onOutgoingRequestStart(data.request);
200+
}) satisfies ChannelListener;
201+
172202
const wrap = <T extends Http | Https>(moduleExports: T): T => {
173203
if (hasRegisteredHandlers) {
174204
return moduleExports;
@@ -187,7 +217,10 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
187217
// Before that, outgoing requests are not patched
188218
// and trace headers are not propagated, sadly.
189219
if (this.getConfig().propagateTraceInOutgoingRequests) {
220+
subscribe('http.client.request.start', onHttpClientRequestStart);
190221
subscribe('http.client.request.created', onHttpClientRequestCreated);
222+
} else {
223+
// TODO: monkey patch this on older node versions :sad:
191224
}
192225

193226
return moduleExports;
@@ -198,6 +231,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
198231
unsubscribe('http.client.response.finish', onHttpClientResponseFinish);
199232
unsubscribe('http.client.request.error', onHttpClientRequestError);
200233
unsubscribe('http.client.request.created', onHttpClientRequestCreated);
234+
unsubscribe('http.client.request.start', onHttpClientRequestStart);
201235
};
202236

203237
/**
@@ -214,6 +248,116 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
214248
];
215249
}
216250

251+
/**
252+
* This is triggered when an outgoing request starts.
253+
* It has access to the request object, and can mutate it before the request is sent.
254+
*/
255+
private _onOutgoingRequestStart(request: http.ClientRequest): void {
256+
DEBUG_BUILD && debug.log(INSTRUMENTATION_NAME, 'Handling started outgoing request');
257+
258+
const _spans = this.getConfig().spans;
259+
const spansEnabled = typeof _spans === 'undefined' ? true : _spans;
260+
261+
const shouldIgnore = this._ignoreOutgoingRequestsMap.get(request) ?? this._shouldIgnoreOutgoingRequest(request);
262+
this._ignoreOutgoingRequestsMap.set(request, shouldIgnore);
263+
264+
if (spansEnabled && !shouldIgnore) {
265+
this._startSpanForOutgoingRequest(request);
266+
}
267+
}
268+
269+
/**
270+
* Start a span for an outgoing request.
271+
* The span wraps the callback of the request, and ends when the response is finished.
272+
*/
273+
private _startSpanForOutgoingRequest(request: http.ClientRequest): void {
274+
// We monkey-patch `req.once('response'), which is used to trigger the callback of the request
275+
// eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation
276+
const originalOnce = request.once;
277+
278+
const [name, attributes] = _getOutgoingRequestSpanData(request);
279+
280+
const span = startInactiveSpan({
281+
name,
282+
attributes,
283+
onlyIfParent: true,
284+
});
285+
286+
const newOnce = new Proxy(originalOnce, {
287+
apply(target, thisArg, args: Parameters<typeof originalOnce>) {
288+
const [event] = args;
289+
if (event !== 'response') {
290+
return target.apply(thisArg, args);
291+
}
292+
293+
const parentContext = context.active();
294+
const requestContext = trace.setSpan(parentContext, span);
295+
296+
context.with(requestContext, () => {
297+
return target.apply(thisArg, args);
298+
});
299+
},
300+
});
301+
302+
// eslint-disable-next-line deprecation/deprecation
303+
request.once = newOnce;
304+
305+
/**
306+
* Determines if the request has errored or the response has ended/errored.
307+
*/
308+
let responseFinished = false;
309+
310+
const endSpan = (status: SpanStatus): void => {
311+
if (responseFinished) {
312+
return;
313+
}
314+
responseFinished = true;
315+
316+
span.setStatus(status);
317+
span.end();
318+
};
319+
320+
request.prependListener('response', response => {
321+
if (request.listenerCount('response') <= 1) {
322+
response.resume();
323+
}
324+
325+
context.bind(context.active(), response);
326+
327+
const additionalAttributes = _getOutgoingRequestEndedSpanData(response);
328+
span.setAttributes(additionalAttributes);
329+
330+
const endHandler = (forceError: boolean = false): void => {
331+
this._diag.debug('outgoingRequest on end()');
332+
333+
const status =
334+
// eslint-disable-next-line deprecation/deprecation
335+
forceError || typeof response.statusCode !== 'number' || (response.aborted && !response.complete)
336+
? { code: SpanStatusCode.ERROR }
337+
: getSpanStatusFromHttpCode(response.statusCode);
338+
339+
endSpan(status);
340+
};
341+
342+
response.on('end', () => {
343+
endHandler();
344+
});
345+
response.on(errorMonitor, error => {
346+
this._diag.debug('outgoingRequest on response error()', error);
347+
endHandler(true);
348+
});
349+
});
350+
351+
// Fallback if proper response end handling above fails
352+
request.on('close', () => {
353+
endSpan({ code: SpanStatusCode.UNSET });
354+
});
355+
request.on(errorMonitor, error => {
356+
this._diag.debug('outgoingRequest on request error()', error);
357+
endSpan({ code: SpanStatusCode.ERROR });
358+
});
359+
}
360+
217361
/**
218362
* This is triggered when an outgoing request finishes.
219363
* It has access to the final request and response objects.
@@ -661,3 +805,103 @@ const clientToRequestSessionAggregatesMap = new Map<
661805
Client,
662806
{ [timestampRoundedToSeconds: string]: { exited: number; crashed: number; errored: number } }
663807
>();
808+
809+
function _getOutgoingRequestSpanData(request: http.ClientRequest): [string, SpanAttributes] {
810+
const url = getRequestUrl(request);
811+
812+
const [name, attributes] = getHttpSpanDetailsFromUrlObject(
813+
parseStringToURLObject(url),
814+
'client',
815+
'auto.http.otel.http',
816+
request,
817+
);
818+
819+
const userAgent = request.getHeader('user-agent');
820+
821+
return [
822+
name,
823+
{
824+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client',
825+
'otel.kind': 'CLIENT',
826+
[ATTR_USER_AGENT_ORIGINAL]: userAgent,
827+
[ATTR_URL_FULL]: url,
828+
'http.url': url,
829+
'http.method': request.method,
830+
'http.target': request.path || '/',
831+
'net.peer.name': request.host,
832+
'http.host': request.getHeader('host'),
833+
...attributes,
834+
},
835+
];
836+
}
837+
838+
function getRequestUrl(request: http.ClientRequest): string {
839+
const hostname = request.getHeader('host') || request.host;
840+
const protocol = request.protocol;
841+
const path = request.path;
842+
843+
return `${protocol}//${hostname}${path}`;
844+
}
845+
846+
function _getOutgoingRequestEndedSpanData(response: http.IncomingMessage): SpanAttributes {
847+
const { statusCode, statusMessage, httpVersion, socket } = response;
848+
849+
const transport = httpVersion.toUpperCase() !== 'QUIC' ? 'ip_tcp' : 'ip_udp';
850+
851+
const additionalAttributes: SpanAttributes = {
852+
[ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode,
853+
[ATTR_NETWORK_PROTOCOL_VERSION]: httpVersion,
854+
'http.flavor': httpVersion,
855+
[ATTR_NETWORK_TRANSPORT]: transport,
856+
'net.transport': transport,
857+
['http.status_text']: statusMessage?.toUpperCase(),
858+
'http.status_code': statusCode,
859+
...getResponseContentLengthAttributes(response),
860+
};
861+
862+
if (socket) {
863+
const { remoteAddress, remotePort } = socket;
864+
865+
additionalAttributes[ATTR_NETWORK_PEER_ADDRESS] = remoteAddress;
866+
additionalAttributes[ATTR_NETWORK_PEER_PORT] = remotePort;
867+
additionalAttributes['net.peer.ip'] = remoteAddress;
868+
additionalAttributes['net.peer.port'] = remotePort;
869+
}
870+
871+
return additionalAttributes;
872+
}
873+
874+
function getResponseContentLengthAttributes(response: http.IncomingMessage): SpanAttributes {
875+
const length = getContentLength(response.headers);
876+
if (length == null) {
877+
return {};
878+
}
879+
880+
if (isCompressed(response.headers)) {
881+
// eslint-disable-next-line deprecation/deprecation
882+
return { [SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH]: length };
883+
} else {
884+
// eslint-disable-next-line deprecation/deprecation
885+
return { [SEMATTRS_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED]: length };
886+
}
887+
}
888+
889+
function getContentLength(headers: http.OutgoingHttpHeaders): number | undefined {
890+
const contentLengthHeader = headers['content-length'];
891+
if (typeof contentLengthHeader !== 'string') {
892+
return contentLengthHeader;
893+
}
894+
895+
const contentLength = parseInt(contentLengthHeader, 10);
896+
if (isNaN(contentLength)) {
897+
return undefined;
898+
}
899+
900+
return contentLength;
901+
}
902+
903+
function isCompressed(headers: OutgoingHttpHeaders | IncomingHttpHeaders): boolean {
904+
const encoding = headers['content-encoding'];
905+
906+
return !!encoding && encoding !== 'identity';
907+
}

‎packages/node/src/integrations/http/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import type { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-h
44
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
55
import type { Span } from '@sentry/core';
66
import { defineIntegration, getClient, hasSpansEnabled } from '@sentry/core';
7-
import type { HTTPModuleRequestIncomingMessage, NodeClient } from '@sentry/node-core';
7+
import type { HTTPModuleRequestIncomingMessage, NodeClient,SentryHttpInstrumentationOptions } from '@sentry/node-core';
88
import {
9-
type SentryHttpInstrumentationOptions,
109
addOriginToSpan,
1110
generateInstrumentOnce,
1211
getRequestUrl,
@@ -19,6 +18,8 @@ const INTEGRATION_NAME = 'Http';
1918

2019
const INSTRUMENTATION_NAME = '@opentelemetry_sentry-patched/instrumentation-http';
2120

21+
const FULLY_SUPPORTS_HTTP_DIAGNOSTICS_CHANNEL = NODE_VERSION.major >= 22;
22+
2223
interface HttpOptions {
2324
/**
2425
* Whether breadcrumbs should be recorded for outgoing requests.
@@ -200,9 +201,8 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
200201
// If spans are not instrumented, it means the HttpInstrumentation has not been added
201202
// In that case, we want to handle incoming trace extraction ourselves
202203
extractIncomingTraceFromHeader: !instrumentSpans,
203-
// If spans are not instrumented, it means the HttpInstrumentation has not been added
204-
// In that case, we want to handle trace propagation ourselves
205-
propagateTraceInOutgoingRequests: !instrumentSpans,
204+
// on older versions, this is handled by the Otel instrumentation
205+
propagateTraceInOutgoingRequests: FULLY_SUPPORTS_HTTP_DIAGNOSTICS_CHANNEL,
206206
});
207207

208208
// This is the "regular" OTEL instrumentation that emits spans
@@ -257,6 +257,8 @@ function getConfigWithDefaults(options: Partial<HttpOptions> = {}): HttpInstrume
257257
...options.instrumentation?._experimentalConfig,
258258

259259
disableIncomingRequestInstrumentation: options.disableIncomingRequestSpans,
260+
// This is handled by the SentryHttpInstrumentation on Node 22+
261+
disableOutgoingRequestInstrumentation: !FULLY_SUPPORTS_HTTP_DIAGNOSTICS_CHANNEL,
260262

261263
ignoreOutgoingRequestHook: request => {
262264
const url = getRequestUrl(request);

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /