Skip to content

Commit 6688ca2

Browse files
authored
Merge 0277808 into 6676414
2 parents 6676414 + 0277808 commit 6688ca2

File tree

3 files changed

+84
-12
lines changed

3 files changed

+84
-12
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/
1313

1414
### :bug: (Bug Fix)
1515

16+
* fix(http-instrumentation): stop listening to `request`'s `close` event once it has emitted `response` [#3625](https://github.com/open-telemetry/opentelemetry-js/pull/3625) @SimenB
1617
* fix(sdk-node): fix initialization in bundled environments by not loading @opentelemetry/exporter-jaeger [#3739](https://github.com/open-telemetry/opentelemetry-js/pull/3739) @pichlermarc
1718

1819
### :books: (Refine Doc)

experimental/packages/opentelemetry-instrumentation-http/src/http.ts

+35-10
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,11 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
315315
this._callRequestHook(span, request);
316316
}
317317

318+
/**
319+
* Determines if the request has errored or the response has ended/errored.
320+
*/
321+
let responseFinished = false;
322+
318323
/*
319324
* User 'response' event listeners can be added before our listener,
320325
* force our listener to be the first, so response emitter is bound
@@ -323,6 +328,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
323328
request.prependListener(
324329
'response',
325330
(response: http.IncomingMessage & { aborted?: boolean }) => {
331+
this._diag.debug('outgoingRequest on response()');
326332
const responseAttributes =
327333
utils.getOutgoingRequestAttributesOnResponse(response);
328334
span.setAttributes(responseAttributes);
@@ -344,9 +350,13 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
344350
);
345351

346352
context.bind(context.active(), response);
347-
this._diag.debug('outgoingRequest on response()');
348-
response.on('end', () => {
353+
354+
const endHandler = () => {
349355
this._diag.debug('outgoingRequest on end()');
356+
if (responseFinished) {
357+
return;
358+
}
359+
responseFinished = true;
350360
let status: SpanStatus;
351361

352362
if (response.aborted && !response.complete) {
@@ -381,15 +391,24 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
381391
startTime,
382392
metricAttributes
383393
);
384-
});
394+
};
395+
396+
response.on('end', endHandler);
397+
// See https://github.com/open-telemetry/opentelemetry-js/pull/3625#issuecomment-1475673533
398+
if (semver.lt(process.version, '16.0.0')) {
399+
response.on('close', endHandler);
400+
}
385401
response.on(errorMonitor, (error: Err) => {
386402
this._diag.debug('outgoingRequest on error()', error);
403+
if (responseFinished) {
404+
return;
405+
}
406+
responseFinished = true;
387407
utils.setSpanWithError(span, error);
388-
const code = utils.parseResponseStatus(
389-
SpanKind.CLIENT,
390-
response.statusCode
391-
);
392-
span.setStatus({ code, message: error.message });
408+
span.setStatus({
409+
code: SpanStatusCode.ERROR,
410+
message: error.message,
411+
});
393412
this._closeHttpSpan(
394413
span,
395414
SpanKind.CLIENT,
@@ -401,12 +420,18 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
401420
);
402421
request.on('close', () => {
403422
this._diag.debug('outgoingRequest on request close()');
404-
if (!request.aborted) {
405-
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
423+
if (request.aborted || responseFinished) {
424+
return;
406425
}
426+
responseFinished = true;
427+
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
407428
});
408429
request.on(errorMonitor, (error: Err) => {
409430
this._diag.debug('outgoingRequest on request error()', error);
431+
if (responseFinished) {
432+
return;
433+
}
434+
responseFinished = true;
410435
utils.setSpanWithError(span, error);
411436
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
412437
});

experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-enable.test.ts

+48-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
import {
1717
SpanStatusCode,
1818
context,
19+
diag,
1920
propagation,
2021
Span as ISpan,
2122
SpanKind,
2223
trace,
2324
SpanAttributes,
25+
DiagConsoleLogger,
2426
} from '@opentelemetry/api';
2527
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
2628
import {
@@ -269,6 +271,14 @@ describe('HttpInstrumentation', () => {
269271
// hang the request.
270272
return;
271273
}
274+
if (request.url?.includes('/destroy-request')) {
275+
// force flush http response header to trigger client response callback
276+
response.write('');
277+
setTimeout(() => {
278+
request.socket.destroy();
279+
}, 100);
280+
return;
281+
}
272282
if (request.url?.includes('/ignored')) {
273283
provider.getTracer('test').startSpan('some-span').end();
274284
}
@@ -861,19 +871,20 @@ describe('HttpInstrumentation', () => {
861871
});
862872

863873
it('should have 2 ended span when client prematurely close', async () => {
864-
const promise = new Promise<void>((resolve, reject) => {
874+
const promise = new Promise<void>(resolve => {
865875
const req = http.get(
866876
`${protocol}://${hostname}:${serverPort}/hang`,
867877
res => {
868878
res.on('close', () => {});
879+
res.on('error', () => {});
869880
}
870881
);
871882
// close the socket.
872883
setTimeout(() => {
873884
req.destroy();
874885
}, 10);
875886

876-
req.on('error', reject);
887+
req.on('error', () => {});
877888

878889
req.on('close', () => {
879890
// yield to server to end the span.
@@ -919,6 +930,41 @@ describe('HttpInstrumentation', () => {
919930
assert.strictEqual(clientSpan.status.code, SpanStatusCode.ERROR);
920931
assert.ok(Object.keys(clientSpan.attributes).length >= 6);
921932
});
933+
934+
it('should not end span multiple times if request socket destroyed before response completes', async () => {
935+
const warnMessages: string[] = [];
936+
diag.setLogger({
937+
...new DiagConsoleLogger(),
938+
warn: message => {
939+
warnMessages.push(message);
940+
},
941+
});
942+
const promise = new Promise<void>(resolve => {
943+
const req = http.request(
944+
`${protocol}://${hostname}:${serverPort}/destroy-request`,
945+
{
946+
// Allow `req.write()`.
947+
method: 'POST',
948+
},
949+
res => {
950+
res.on('end', () => {});
951+
res.on('close', () => {
952+
resolve();
953+
});
954+
res.on('error', () => {});
955+
}
956+
);
957+
// force flush http request header to trigger client response callback
958+
req.write('');
959+
req.on('error', () => {});
960+
});
961+
962+
await promise;
963+
964+
diag.disable();
965+
966+
assert.deepStrictEqual(warnMessages, []);
967+
});
922968
});
923969

924970
describe('with require parent span', () => {

0 commit comments

Comments
 (0)