Skip to content

Commit 596dcee

Browse files
committed
fix order with otel instrumentation
1 parent ccda7aa commit 596dcee

File tree

2 files changed

+88
-30
lines changed

2 files changed

+88
-30
lines changed

packages/node/src/integrations/http/SentryHttpInstrumentation.ts

+80-23
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { subscribe } from 'node:diagnostics_channel';
1+
/* eslint-disable max-lines */
2+
import type { ChannelListener } from 'node:diagnostics_channel';
3+
import { subscribe, unsubscribe } from 'node:diagnostics_channel';
24
import type * as http from 'node:http';
5+
import type * as https from 'node:https';
36
import type { EventEmitter } from 'node:stream';
47
import { context, propagation } from '@opentelemetry/api';
58
import { VERSION } from '@opentelemetry/core';
69
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
7-
import { InstrumentationBase } from '@opentelemetry/instrumentation';
10+
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
811
import type { AggregationCounts, Client, SanitizedRequestData, Scope } from '@sentry/core';
912
import {
1013
addBreadcrumb,
@@ -26,6 +29,9 @@ import { getRequestUrl } from '../../utils/getRequestUrl';
2629

2730
const INSTRUMENTATION_NAME = '@sentry/instrumentation-http';
2831

32+
type Http = typeof http;
33+
type Https = typeof https;
34+
2935
export type SentryHttpInstrumentationOptions = InstrumentationConfig & {
3036
/**
3137
* Whether breadcrumbs should be recorded for requests.
@@ -101,29 +107,80 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
101107
}
102108

103109
/** @inheritdoc */
104-
public init(): [] {
105-
subscribe('http.server.request.start', data => {
106-
const server = (data as { server: http.Server }).server;
107-
this._patchServerEmit(server);
108-
});
109-
110-
subscribe('http.client.response.finish', data => {
111-
const request = (data as { request: http.ClientRequest }).request;
112-
const response = (data as { response: http.IncomingMessage }).response;
110+
public init(): [InstrumentationNodeModuleDefinition, InstrumentationNodeModuleDefinition] {
111+
// Nothing to do here
113112

114-
this._onOutgoingRequestFinish(request, response);
115-
});
113+
const handledRequests = new WeakSet<http.ClientRequest>();
116114

117-
// When an error happens, we still want to have a breadcrumb
118-
// In this case, `http.client.response.finish` is not triggered
119-
subscribe('http.client.request.error', data => {
120-
const request = (data as { request: http.ClientRequest }).request;
121-
// there is no response object here, we only have access to request & error :(
115+
const handleOutgoingRequestFinishOnce = (request: http.ClientRequest, response?: http.IncomingMessage): void => {
116+
if (handledRequests.has(request)) {
117+
return;
118+
}
122119

123-
this._onOutgoingRequestFinish(request, undefined);
124-
});
120+
handledRequests.add(request);
121+
this._onOutgoingRequestFinish(request, response);
122+
};
125123

126-
return [];
124+
const onHttpServerRequestStart = ((_data: unknown) => {
125+
const data = _data as { server: http.Server };
126+
this._patchServerEmitOnce(data.server);
127+
}) satisfies ChannelListener;
128+
129+
const onHttpsServerRequestStart = ((_data: unknown) => {
130+
const data = _data as { server: http.Server };
131+
this._patchServerEmitOnce(data.server);
132+
}) satisfies ChannelListener;
133+
134+
const onHttpClientResponseFinish = ((_data: unknown) => {
135+
const data = _data as { request: http.ClientRequest; response: http.IncomingMessage };
136+
handleOutgoingRequestFinishOnce(data.request, data.response);
137+
}) satisfies ChannelListener;
138+
139+
const onHttpClientRequestError = ((_data: unknown) => {
140+
const data = _data as { request: http.ClientRequest };
141+
handleOutgoingRequestFinishOnce(data.request, undefined);
142+
}) satisfies ChannelListener;
143+
144+
return [
145+
new InstrumentationNodeModuleDefinition(
146+
'http',
147+
['*'],
148+
(moduleExports: Http): Http => {
149+
subscribe('http.server.request.start', onHttpServerRequestStart);
150+
subscribe('http.client.response.finish', onHttpClientResponseFinish);
151+
152+
// When an error happens, we still want to have a breadcrumb
153+
// In this case, `http.client.response.finish` is not triggered
154+
subscribe('http.client.request.error', onHttpClientRequestError);
155+
156+
return moduleExports;
157+
},
158+
() => {
159+
unsubscribe('http.server.request.start', onHttpServerRequestStart);
160+
unsubscribe('http.client.response.finish', onHttpClientResponseFinish);
161+
unsubscribe('http.client.request.error', onHttpClientRequestError);
162+
},
163+
),
164+
new InstrumentationNodeModuleDefinition(
165+
'https',
166+
['*'],
167+
(moduleExports: Https): Https => {
168+
subscribe('http.server.request.start', onHttpsServerRequestStart);
169+
subscribe('http.client.response.finish', onHttpClientResponseFinish);
170+
171+
// When an error happens, we still want to have a breadcrumb
172+
// In this case, `http.client.response.finish` is not triggered
173+
subscribe('http.client.request.error', onHttpClientRequestError);
174+
175+
return moduleExports;
176+
},
177+
() => {
178+
unsubscribe('http.server.request.start', onHttpsServerRequestStart);
179+
unsubscribe('http.client.response.finish', onHttpClientResponseFinish);
180+
unsubscribe('http.client.request.error', onHttpClientRequestError);
181+
},
182+
),
183+
];
127184
}
128185

129186
/**
@@ -150,7 +207,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
150207
* Patch a server.emit function to handle process isolation for incoming requests.
151208
* This will only patch the emit function if it was not already patched.
152209
*/
153-
private _patchServerEmit(server: http.Server): void {
210+
private _patchServerEmitOnce(server: http.Server): void {
154211
// eslint-disable-next-line @typescript-eslint/unbound-method
155212
const originalEmit = server.emit;
156213

@@ -159,7 +216,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
159216
return;
160217
}
161218

162-
DEBUG_BUILD && logger.log(INSTRUMENTATION_NAME, 'Patching server.emit');
219+
DEBUG_BUILD && logger.log(INSTRUMENTATION_NAME, 'Patching server.emit!!!');
163220

164221
// eslint-disable-next-line @typescript-eslint/no-this-alias
165222
const instrumentation = this;

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

+8-7
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
151151
return {
152152
name: INTEGRATION_NAME,
153153
setupOnce() {
154+
// TODO: get rid of this too
154155
// Below, we instrument the Node.js HTTP API three times. 2 times Sentry-specific, 1 time OTEL specific.
155156
// Due to timing reasons, we sometimes need to apply Sentry instrumentation _before_ we apply the OTEL
156157
// instrumentation (e.g. to flush on serverless platforms), and sometimes we need to apply Sentry instrumentation
@@ -165,19 +166,19 @@ export const httpIntegration = defineIntegration((options: HttpOptions = {}) =>
165166

166167
const instrumentSpans = _shouldInstrumentSpans(options, getClient<NodeClient>()?.getOptions());
167168

168-
// This is the "regular" OTEL instrumentation that emits spans
169-
if (instrumentSpans) {
170-
const instrumentationConfig = getConfigWithDefaults(options);
171-
instrumentOtelHttp(instrumentationConfig);
172-
}
173-
174-
// This is Sentry-specific instrumentation that is applied _after_ any OTEL instrumentation.
169+
// This is Sentry-specific instrumentation for request isolation and breadcrumbs
175170
instrumentSentryHttp({
176171
...options,
177172
// If spans are not instrumented, it means the HttpInstrumentation has not been added
178173
// In that case, we want to handle incoming trace extraction ourselves
179174
extractIncomingTraceFromHeader: !instrumentSpans,
180175
});
176+
177+
// This is the "regular" OTEL instrumentation that emits spans
178+
if (instrumentSpans) {
179+
const instrumentationConfig = getConfigWithDefaults(options);
180+
instrumentOtelHttp(instrumentationConfig);
181+
}
181182
},
182183
};
183184
});

0 commit comments

Comments
 (0)