Skip to content

Commit 52d3dc3

Browse files
OlivierAlbertinimayurkale22
authored andcommitted
fix(plugin-http): ensure no leaks (#398)
* fix(plugin-http): ensure no leaks closes #397 Signed-off-by: Olivier Albertini <[email protected]> * fix: add @Flarna recommandations Signed-off-by: Olivier Albertini <[email protected]>
1 parent 8b20e41 commit 52d3dc3

File tree

7 files changed

+118
-78
lines changed

7 files changed

+118
-78
lines changed

packages/opentelemetry-plugin-http/package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@
3838
"access": "public"
3939
},
4040
"devDependencies": {
41-
"@types/got": "^9.6.6",
41+
"@types/got": "^9.6.7",
4242
"@types/mocha": "^5.2.7",
43-
"@types/nock": "^10.0.3",
44-
"@types/node": "^12.6.9",
45-
"@types/request-promise-native": "^1.0.16",
46-
"@types/semver": "^6.0.1",
43+
"@types/nock": "^11.1.0",
44+
"@types/node": "^12.7.9",
45+
"@types/request-promise-native": "^1.0.17",
46+
"@types/semver": "^6.0.2",
4747
"@types/shimmer": "^1.0.1",
4848
"@types/sinon": "^7.0.13",
4949
"@types/superagent": "^4.1.3",
@@ -55,17 +55,17 @@
5555
"request": "^2.88.0",
5656
"request-promise-native": "^1.0.7",
5757
"superagent": "5.1.0",
58-
"codecov": "^3.5.0",
59-
"gts": "^1.0.0",
60-
"mocha": "^6.2.0",
61-
"nock": "^11.0.0",
58+
"codecov": "^3.6.1",
59+
"gts": "^1.1.0",
60+
"mocha": "^6.2.1",
61+
"nock": "^11.3.5",
6262
"nyc": "^14.1.1",
6363
"rimraf": "^3.0.0",
64-
"sinon": "^7.3.2",
64+
"sinon": "^7.5.0",
6565
"tslint-microsoft-contrib": "^6.2.0",
66-
"tslint-consistent-codestyle": "^1.15.1",
66+
"tslint-consistent-codestyle": "^1.16.0",
6767
"ts-mocha": "^6.0.0",
68-
"ts-node": "^8.3.0",
68+
"ts-node": "^8.4.1",
6969
"typescript": "^3.6.3"
7070
},
7171
"dependencies": {

packages/opentelemetry-plugin-http/src/http.ts

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
SpanOptions,
2222
Attributes,
2323
CanonicalCode,
24+
Status,
2425
} from '@opentelemetry/types';
2526
import {
2627
ClientRequest,
@@ -39,6 +40,7 @@ import {
3940
ResponseEndArgs,
4041
ParsedRequestOptions,
4142
HttpRequestArgs,
43+
Err,
4244
} from './types';
4345
import { Format } from './enums/Format';
4446
import { AttributeNames } from './enums/AttributeNames';
@@ -50,11 +52,14 @@ import * as utils from './utils';
5052
export class HttpPlugin extends BasePlugin<Http> {
5153
readonly component: string;
5254
protected _config!: HttpPluginConfig;
55+
/** keep track on spans not ended */
56+
private readonly _spanNotEnded: WeakSet<Span>;
5357

5458
constructor(readonly moduleName: string, readonly version: string) {
5559
super();
5660
// For now component is equal to moduleName but it can change in the future.
5761
this.component = this.moduleName;
62+
this._spanNotEnded = new WeakSet<Span>();
5863
this._config = {};
5964
}
6065

@@ -192,29 +197,38 @@ export class HttpPlugin extends BasePlugin<Http> {
192197
[AttributeNames.HTTP_HOSTNAME]: host,
193198
[AttributeNames.HTTP_METHOD]: method,
194199
[AttributeNames.HTTP_PATH]: options.path || '/',
195-
[AttributeNames.HTTP_USER_AGENT]: userAgent || '',
196200
});
197201

202+
if (userAgent !== undefined) {
203+
span.setAttribute(AttributeNames.HTTP_USER_AGENT, userAgent);
204+
}
205+
198206
request.on(
199207
'response',
200-
(response: IncomingMessage & { aborted?: boolean }) => {
208+
(
209+
response: IncomingMessage & { aborted?: boolean; req: ClientRequest }
210+
) => {
211+
if (response.statusCode) {
212+
span.setAttributes({
213+
[AttributeNames.HTTP_STATUS_CODE]: response.statusCode,
214+
[AttributeNames.HTTP_STATUS_TEXT]: response.statusMessage,
215+
});
216+
}
217+
201218
this._tracer.bind(response);
202219
this._logger.debug('outgoingRequest on response()');
203220
response.on('end', () => {
204221
this._logger.debug('outgoingRequest on end()');
205-
if (response.statusCode) {
206-
span.setAttributes({
207-
[AttributeNames.HTTP_STATUS_CODE]: response.statusCode,
208-
[AttributeNames.HTTP_STATUS_TEXT]: response.statusMessage,
209-
});
210-
}
222+
let status: Status;
211223

212224
if (response.aborted && !response.complete) {
213-
span.setStatus({ code: CanonicalCode.ABORTED });
225+
status = { code: CanonicalCode.ABORTED };
214226
} else {
215-
span.setStatus(utils.parseResponseStatus(response.statusCode!));
227+
status = utils.parseResponseStatus(response.statusCode!);
216228
}
217229

230+
span.setStatus(status);
231+
218232
if (this._config.applyCustomAttributesOnSpan) {
219233
this._safeExecute(
220234
span,
@@ -228,13 +242,23 @@ export class HttpPlugin extends BasePlugin<Http> {
228242
);
229243
}
230244

231-
span.end();
245+
this._closeHttpSpan(span);
246+
});
247+
response.on('error', (error: Err) => {
248+
utils.setSpanWithError(span, error, response);
249+
this._closeHttpSpan(span);
232250
});
233-
utils.setSpanOnError(span, response);
234251
}
235252
);
236-
237-
utils.setSpanOnError(span, request);
253+
request.on('close', () => {
254+
if (!request.aborted) {
255+
this._closeHttpSpan(span);
256+
}
257+
});
258+
request.on('error', (error: Err) => {
259+
utils.setSpanWithError(span, error, request);
260+
this._closeHttpSpan(span);
261+
});
238262

239263
this._logger.debug('makeRequestTrace return request');
240264
return request;
@@ -331,7 +355,7 @@ export class HttpPlugin extends BasePlugin<Http> {
331355
attributes[AttributeNames.HTTP_ROUTE] = requestUrl.pathname || '/';
332356
}
333357

334-
if (userAgent) {
358+
if (userAgent !== undefined) {
335359
attributes[AttributeNames.HTTP_USER_AGENT] = userAgent;
336360
}
337361

@@ -352,7 +376,7 @@ export class HttpPlugin extends BasePlugin<Http> {
352376
);
353377
}
354378

355-
span.end();
379+
plugin._closeHttpSpan(span);
356380
return returned;
357381
};
358382

@@ -438,10 +462,22 @@ export class HttpPlugin extends BasePlugin<Http> {
438462
}
439463

440464
private _startHttpSpan(name: string, options: SpanOptions) {
441-
return this._tracer
465+
const span = this._tracer
442466
.startSpan(name, options)
443467
.setAttribute(AttributeNames.COMPONENT, this.component);
468+
this._spanNotEnded.add(span);
469+
return span;
470+
}
471+
472+
private _closeHttpSpan(span: Span) {
473+
if (!this._spanNotEnded.has(span)) {
474+
return;
475+
}
476+
477+
span.end();
478+
this._spanNotEnded.delete(span);
444479
}
480+
445481
private _safeExecute<
446482
T extends (...args: unknown[]) => ReturnType<T>,
447483
K extends boolean
@@ -460,7 +496,7 @@ export class HttpPlugin extends BasePlugin<Http> {
460496
} catch (error) {
461497
if (rethrow) {
462498
utils.setSpanWithError(span, error);
463-
span.end();
499+
this._closeHttpSpan(span);
464500
throw error;
465501
}
466502
this._logger.error('caught error ', error);

packages/opentelemetry-plugin-http/src/utils.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -154,21 +154,6 @@ export const isIgnored = (
154154
return false;
155155
};
156156

157-
/**
158-
* Will subscribe obj on error event and will set attributes when emitting event
159-
* @param span to set
160-
* @param obj to subscribe on 'error' event (must extend `EventEmitter`)
161-
*/
162-
export const setSpanOnError = (
163-
span: Span,
164-
obj: IncomingMessage | ClientRequest
165-
) => {
166-
obj.on('error', (error: Err) => {
167-
setSpanWithError(span, error, obj);
168-
span.end();
169-
});
170-
};
171-
172157
/**
173158
* Sets the span with the error passed in params
174159
* @param {Span} span the span that need to be set
@@ -189,7 +174,6 @@ export const setSpanWithError = (
189174

190175
if (!obj) {
191176
span.setStatus({ code: CanonicalCode.UNKNOWN, message });
192-
span.end();
193177
return;
194178
}
195179

packages/opentelemetry-plugin-http/test/functionals/http-enable.test.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { DummyPropagation } from '../utils/DummyPropagation';
3030
import { httpRequest } from '../utils/httpRequest';
3131
import * as utils from '../../src/utils';
3232
import { HttpPluginConfig, Http } from '../../src/types';
33+
import { AttributeNames } from '../../src/enums/AttributeNames';
3334

3435
const applyCustomAttributesOnSpanErrorMessage =
3536
'bad applyCustomAttributesOnSpan function';
@@ -541,12 +542,12 @@ describe('HttpPlugin', () => {
541542
const [span] = spans;
542543
assert.strictEqual(spans.length, 1);
543544
assert.strictEqual(span.status.code, CanonicalCode.ABORTED);
544-
assert.ok(Object.keys(span.attributes).length > 7);
545+
assert.ok(Object.keys(span.attributes).length > 6);
545546
}
546547
});
547548

548549
it('should have 1 ended span when request is aborted after receiving response', async () => {
549-
nock('http://my.server.com')
550+
nock(`${protocol}://my.server.com`)
550551
.get('/')
551552
.delay({
552553
body: 50,
@@ -555,7 +556,7 @@ describe('HttpPlugin', () => {
555556

556557
const promiseRequest = new Promise((resolve, reject) => {
557558
const req = http.request(
558-
'http://my.server.com',
559+
`${protocol}://my.server.com`,
559560
(resp: http.IncomingMessage) => {
560561
let data = '';
561562
resp.on('data', chunk => {
@@ -582,6 +583,44 @@ describe('HttpPlugin', () => {
582583
assert.ok(Object.keys(span.attributes).length > 7);
583584
}
584585
});
586+
587+
it("should have 1 ended span when request doesn't listening response", done => {
588+
nock.cleanAll();
589+
nock.enableNetConnect();
590+
const req = http.request(`${protocol}://${hostname}/`);
591+
req.on('close', () => {
592+
const spans = memoryExporter.getFinishedSpans();
593+
const [span] = spans;
594+
assert.strictEqual(spans.length, 1);
595+
assert.ok(Object.keys(span.attributes).length > 6);
596+
done();
597+
});
598+
req.end();
599+
});
600+
601+
it("should have 1 ended span when response is listened by using req.on('response')", done => {
602+
const host = `${protocol}://${hostname}`;
603+
nock(host)
604+
.get('/')
605+
.reply(404);
606+
const req = http.request(`${host}/`);
607+
req.on('response', response => {
608+
response.on('data', () => {});
609+
response.on('end', () => {
610+
const spans = memoryExporter.getFinishedSpans();
611+
const [span] = spans;
612+
assert.strictEqual(spans.length, 1);
613+
assert.ok(Object.keys(span.attributes).length > 6);
614+
assert.strictEqual(
615+
span.attributes[AttributeNames.HTTP_STATUS_CODE],
616+
404
617+
);
618+
assert.strictEqual(span.status.code, CanonicalCode.NOT_FOUND);
619+
done();
620+
});
621+
});
622+
req.end();
623+
});
585624
});
586625
});
587626
});

packages/opentelemetry-plugin-http/test/functionals/http-package.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { NoopLogger } from '@opentelemetry/core';
18-
import { SpanKind, Span } from '@opentelemetry/types';
18+
import { SpanKind } from '@opentelemetry/types';
1919
import * as assert from 'assert';
2020
import * as http from 'http';
2121
import * as nock from 'nock';
@@ -34,11 +34,10 @@ import {
3434
SimpleSpanProcessor,
3535
} from '@opentelemetry/tracing';
3636

37-
const memoryExporter = new InMemorySpanExporter();
37+
import { HttpPluginConfig } from '../../src/types';
38+
import { customAttributeFunction } from './http-enable.test';
3839

39-
export const customAttributeFunction = (span: Span): void => {
40-
span.setAttribute('span kind', SpanKind.CLIENT);
41-
};
40+
const memoryExporter = new InMemorySpanExporter();
4241

4342
describe('Packages', () => {
4443
describe('get', () => {
@@ -55,7 +54,10 @@ describe('Packages', () => {
5554
});
5655

5756
before(() => {
58-
plugin.enable(http, tracer, tracer.logger);
57+
const config: HttpPluginConfig = {
58+
applyCustomAttributesOnSpan: customAttributeFunction,
59+
};
60+
plugin.enable(http, tracer, tracer.logger, config);
5961
});
6062

6163
after(() => {

packages/opentelemetry-plugin-http/test/functionals/utils.test.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import * as assert from 'assert';
1818
import * as sinon from 'sinon';
1919
import * as url from 'url';
20-
import { CanonicalCode, Attributes, SpanKind } from '@opentelemetry/types';
20+
import { CanonicalCode, SpanKind } from '@opentelemetry/types';
2121
import { NoopScopeManager } from '@opentelemetry/scope-base';
2222
import { IgnoreMatcher } from '../../src/types';
2323
import * as utils from '../../src/utils';
@@ -235,27 +235,6 @@ describe('Utility', () => {
235235
assert.strictEqual(result, 'http://localhost:8080/helloworld');
236236
});
237237
});
238-
describe('setSpanOnError()', () => {
239-
it('should call span methods when we get an error event', done => {
240-
/* tslint:disable-next-line:no-any */
241-
const span: any = {
242-
setAttributes: (obj: Attributes) => {},
243-
setStatus: (status: unknown) => {},
244-
end: () => {},
245-
};
246-
sinon.spy(span, 'setAttributes');
247-
sinon.spy(span, 'setStatus');
248-
sinon.spy(span, 'end');
249-
const req = http.get('http://noop');
250-
utils.setSpanOnError(span, req);
251-
req.on('error', () => {
252-
assert.strictEqual(span.setAttributes.callCount, 1);
253-
assert.strictEqual(span.setStatus.callCount, 1);
254-
assert.strictEqual(span.end.callCount, 1);
255-
done();
256-
});
257-
});
258-
});
259238

260239
describe('setSpanWithError()', () => {
261240
it('should have error attributes', () => {

packages/opentelemetry-plugin-https/test/functionals/https-enable.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ describe('HttpsPlugin', () => {
436436
const [span] = spans;
437437
assert.strictEqual(spans.length, 1);
438438
assert.strictEqual(span.status.code, CanonicalCode.ABORTED);
439-
assert.ok(Object.keys(span.attributes).length > 7);
439+
assert.ok(Object.keys(span.attributes).length > 6);
440440
}
441441
});
442442

0 commit comments

Comments
 (0)