Skip to content

Commit ce2bc95

Browse files
fix(http-plugin): ensure exceptions are handled (open-telemetry#273)
* fix(http-plugin): ensure exceptions are handled refactor(test): remove unnecessary code add tests closes open-telemetry#222 Signed-off-by: Olivier Albertini <[email protected]>
1 parent 0881e64 commit ce2bc95

File tree

7 files changed

+321
-158
lines changed

7 files changed

+321
-158
lines changed

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,9 @@ export class HttpPlugin extends BasePlugin<Http> {
276276
// Cannot pass args of type ResponseEndArgs,
277277
// tslint complains "Expected 1-2 arguments, but got 1 or more.", it does not make sense to me
278278
// tslint:disable-next-line:no-any
279-
const returned = response.end.apply(this, arguments as any);
279+
const returned = plugin._safeExecute(span, () =>
280+
response.end.apply(this, arguments as any)
281+
);
280282
const requestUrl = request.url ? url.parse(request.url) : null;
281283
const hostname = headers.host
282284
? headers.host.replace(/^(.*)(\:[0-9]{1,5})/, '$1')
@@ -314,7 +316,10 @@ export class HttpPlugin extends BasePlugin<Http> {
314316
span.end();
315317
return returned;
316318
};
317-
return original.apply(this, [event, ...args]);
319+
320+
return plugin._safeExecute(span, () =>
321+
original.apply(this, [event, ...args])
322+
);
318323
});
319324
};
320325
}
@@ -328,7 +333,7 @@ export class HttpPlugin extends BasePlugin<Http> {
328333
options: RequestOptions | string,
329334
...args: unknown[]
330335
): ClientRequest {
331-
if (!options) {
336+
if (!Utils.isValidOptionsType(options)) {
332337
return original.apply(this, [options, ...args]);
333338
}
334339

@@ -358,7 +363,9 @@ export class HttpPlugin extends BasePlugin<Http> {
358363
.getHttpTextFormat()
359364
.inject(span.context(), Format.HTTP, options.headers);
360365

361-
const request: ClientRequest = original.apply(this, [options, ...args]);
366+
const request: ClientRequest = plugin._safeExecute(span, () =>
367+
original.apply(this, [options, ...args])
368+
);
362369

363370
plugin._logger.debug('%s plugin outgoingRequest', plugin.moduleName);
364371
plugin._tracer.bind(request);
@@ -385,6 +392,18 @@ export class HttpPlugin extends BasePlugin<Http> {
385392
.startSpan(name, options)
386393
.setAttribute(AttributeNames.COMPONENT, HttpPlugin.component);
387394
}
395+
396+
private _safeExecute<T extends (...args: unknown[]) => ReturnType<T>>(
397+
span: Span,
398+
execute: T
399+
): ReturnType<T> {
400+
try {
401+
return execute();
402+
} catch (error) {
403+
Utils.setSpanWithError(span, error);
404+
throw error;
405+
}
406+
}
388407
}
389408

390409
export const plugin = new HttpPlugin(

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,11 @@ export interface HttpPluginConfig {
6262
ignoreOutgoingUrls?: IgnoreMatcher[];
6363
applyCustomAttributesOnSpan?: HttpCustomAttributeFunction;
6464
}
65+
66+
export interface Err extends Error {
67+
errno?: number;
68+
code?: string;
69+
path?: string;
70+
syscall?: string;
71+
stack?: string;
72+
}

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

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
ClientRequest,
2222
IncomingHttpHeaders,
2323
} from 'http';
24-
import { IgnoreMatcher } from './types';
24+
import { IgnoreMatcher, Err } from './types';
2525
import { AttributeNames } from './enums/AttributeNames';
2626
import * as url from 'url';
2727

@@ -138,26 +138,40 @@ export class Utils {
138138
* @param obj to subscribe on error
139139
*/
140140
static setSpanOnError(span: Span, obj: IncomingMessage | ClientRequest) {
141-
obj.on('error', error => {
142-
span.setAttributes({
143-
[AttributeNames.HTTP_ERROR_NAME]: error.name,
144-
[AttributeNames.HTTP_ERROR_MESSAGE]: error.message,
145-
});
146-
147-
let status: Status;
148-
if ((obj as IncomingMessage).statusCode) {
149-
status = Utils.parseResponseStatus(
150-
(obj as IncomingMessage).statusCode!
151-
);
152-
} else {
153-
status = { code: CanonicalCode.UNKNOWN };
154-
}
141+
obj.on('error', (error: Err) => {
142+
Utils.setSpanWithError(span, error, obj);
143+
});
144+
}
155145

156-
status.message = error.message;
146+
/**
147+
* Sets the span with the error passed in params
148+
* @param {Span} span the span that need to be set
149+
* @param {Error} error error that will be set to span
150+
* @param {(IncomingMessage | ClientRequest)} [obj] used for enriching the status by checking the statusCode.
151+
*/
152+
static setSpanWithError(
153+
span: Span,
154+
error: Err,
155+
obj?: IncomingMessage | ClientRequest
156+
) {
157+
const message = error.message;
157158

158-
span.setStatus(status);
159-
span.end();
159+
span.setAttributes({
160+
[AttributeNames.HTTP_ERROR_NAME]: error.name,
161+
[AttributeNames.HTTP_ERROR_MESSAGE]: message,
160162
});
163+
164+
let status: Status;
165+
if (obj && (obj as IncomingMessage).statusCode) {
166+
status = Utils.parseResponseStatus((obj as IncomingMessage).statusCode!);
167+
} else {
168+
status = { code: CanonicalCode.UNKNOWN };
169+
}
170+
171+
status.message = message;
172+
173+
span.setStatus(status);
174+
span.end();
161175
}
162176

163177
/**
@@ -172,7 +186,6 @@ export class Utils {
172186
let pathname = '/';
173187
let origin = '';
174188
let optionsParsed: url.URL | url.UrlWithStringQuery | RequestOptions;
175-
176189
if (typeof options === 'string') {
177190
optionsParsed = url.parse(options);
178191
pathname = (optionsParsed as url.UrlWithStringQuery).pathname || '/';
@@ -181,15 +194,13 @@ export class Utils {
181194
Object.assign(optionsParsed, extraOptions);
182195
}
183196
} else {
184-
optionsParsed = options;
185-
try {
186-
pathname = (options as url.URL).pathname;
187-
if (!pathname && options.path) {
188-
pathname = url.parse(options.path).pathname || '/';
189-
}
190-
origin = `${options.protocol || 'http:'}//${options.host ||
191-
`${options.hostname}:${options.port}`}`;
192-
} catch (ignore) {}
197+
optionsParsed = options as RequestOptions;
198+
pathname = (options as url.URL).pathname;
199+
if (!pathname && optionsParsed.path) {
200+
pathname = url.parse(optionsParsed.path).pathname || '/';
201+
}
202+
origin = `${optionsParsed.protocol || 'http:'}//${optionsParsed.host ||
203+
`${optionsParsed.hostname}:${optionsParsed.port}`}`;
193204
}
194205

195206
if (Utils.hasExpectHeader(optionsParsed)) {
@@ -207,4 +218,17 @@ export class Utils {
207218

208219
return { origin, pathname, method, optionsParsed };
209220
}
221+
222+
/**
223+
* Makes sure options is of type string or object
224+
* @param options for the request
225+
*/
226+
static isValidOptionsType(options: unknown): boolean {
227+
if (!options) {
228+
return false;
229+
}
230+
231+
const type = typeof options;
232+
return type === 'string' || (type === 'object' && !Array.isArray(options));
233+
}
210234
}

0 commit comments

Comments
 (0)