Skip to content
This repository was archived by the owner on Oct 3, 2023. It is now read-only.

Commit 20c44f8

Browse files
eduardoemerykjin
authored andcommitted
feat: add http/https automatic tracing
1 parent 497261a commit 20c44f8

File tree

21 files changed

+10057
-546
lines changed

21 files changed

+10057
-546
lines changed

packages/opencensus-instrumentation-http/package-lock.json

Lines changed: 3006 additions & 114 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/opencensus-instrumentation-http/package.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
"version": "0.0.1",
44
"description": "OpenCensus is a toolkit for collecting application performance and behavior data.",
55
"main": "build/src/index.js",
6-
"types": "build/src/index.d.js",
6+
"types": "build/src/index.d.ts",
77
"repository": "census-instrumentation/opencensus-node",
88
"scripts": {
9-
"build": "node_modules/.bin/tsc --declaration",
10-
"test": "npm run script run-unit-tests",
11-
"clean": "rimraf build/*"
9+
"test": "nyc -x '**/test/**' --reporter=html --reporter=text mocha 'build/test/**/*.js'",
10+
"clean": "rimraf build/*",
11+
"check": "gts check",
12+
"compile": "tsc -p .",
13+
"fix": "gts fix",
14+
"prepare": "npm run compile",
15+
"pretest": "npm run compile",
16+
"posttest": "npm run check"
1217
},
1318
"keywords": [
1419
"opencensus",
@@ -42,6 +47,7 @@
4247
"gts": "^0.5.1",
4348
"mocha": "^5.0.4",
4449
"ncp": "^2.0.0",
50+
"nyc": "^11.7.1",
4551
"ts-node": "^4.0.0",
4652
"typescript": "^2.7.2"
4753
},
Lines changed: 239 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2018 Google Inc. All Rights Reserved.
2+
* Copyright 2018, OpenCensus Authors
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,32 +14,252 @@
1414
* limitations under the License.
1515
*/
1616

17-
import * as semver from 'semver'
18-
import * as url from 'url'
19-
import * as eos from 'end-of-stream'
17+
import {types} from '@opencensus/opencensus-core';
18+
import {classes} from '@opencensus/opencensus-core';
19+
import {logger} from '@opencensus/opencensus-core';
20+
import {B3Format} from '@opencensus/opencensus-propagation-b3';
21+
import * as semver from 'semver';
22+
import * as shimmer from 'shimmer';
23+
import * as url from 'url';
24+
import * as uuid from 'uuid';
2025

21-
import { Tracer } from '@opencensus/opencensus-core'
22-
import { debug } from '@opencensus/opencensus-core'
23-
import { Plugin, BasePlugin } from '@opencensus/opencensus-core'
24-
import { TraceOptions, TraceContext } from '@opencensus/opencensus-core';
25-
import { B3Format } from '@opencensus/opencensus-propagation-b3'
26+
// import * as Debug from 'debug';
27+
// const debug = Debug('opencensus');
2628

27-
export class HttpPlugin extends BasePlugin implements Plugin {
29+
export class HttpPlugin extends classes.BasePlugin {
30+
logger: types.Logger;
2831

29-
constructor() {
30-
super('http');
31-
}
32+
/** Constructs a new HttpPlugin instance. */
33+
constructor(moduleName: string) {
34+
super(moduleName);
35+
}
3236

33-
applyPatch(exporter: any, tracer: Tracer, version: string) {
34-
this.setPluginContext(exporter, tracer, version);
35-
return exporter
36-
}
37+
/**
38+
* Patches HTTP incoming and outcoming request functions.
39+
* @param moduleExporters The HTTP package.
40+
* @param tracer A tracer instance to create spans on.
41+
* @param version The package version.
42+
*/
43+
// tslint:disable:no-any
44+
applyPatch(moduleExporters: any, tracer: types.Tracer, version: string) {
45+
this.setPluginContext(moduleExporters, tracer, version);
46+
this.logger = tracer.logger || logger.logger('debug');
3747

38-
applyUnpatch(): void {
48+
shimmer.wrap(moduleExporters, 'request', this.patchOutgoingRequest());
3949

50+
// In Node 8, http.get calls a private request method, therefore we patch it
51+
// here too.
52+
if (semver.satisfies(version, '>=8.0.0')) {
53+
shimmer.wrap(moduleExporters, 'get', this.patchOutgoingRequest());
4054
}
4155

56+
shimmer.wrap(
57+
moduleExporters && moduleExporters.Server &&
58+
moduleExporters.Server.prototype,
59+
'emit', this.patchIncomingRequest());
60+
61+
return moduleExporters;
62+
}
63+
64+
/** Unpatches all HTTP patched function. */
65+
applyUnpatch(): void {
66+
shimmer.unwrap(this.moduleExporters, 'request');
67+
shimmer.unwrap(this.moduleExporters, 'get');
68+
shimmer.unwrap(
69+
this.moduleExporters && this.moduleExporters.Server &&
70+
this.moduleExporters.Server.prototype,
71+
'emit');
72+
}
73+
74+
/**
75+
* Creates spans for incoming requests, restoring spans' context if applied.
76+
* @param plugin The HTTP plugin itself.
77+
*/
78+
patchIncomingRequest() {
79+
const plugin: HttpPlugin = this;
80+
return (original: Function): Function => {
81+
// tslint:disable:no-any
82+
return function incomingRequest(
83+
event: string, request: any, response: any) {
84+
// Only traces request events
85+
if (event !== 'request') {
86+
return original.apply(this, arguments);
87+
}
88+
89+
const traceOptions = {
90+
name: arguments[1].url,
91+
type: 'SERVER',
92+
spanContext: B3Format.extractFromHeader(arguments[1].headers)
93+
};
94+
95+
return plugin.tracer.startRootSpan(traceOptions, rootSpan => {
96+
if (!rootSpan) return original.apply(this, arguments);
97+
98+
plugin.tracer.wrapEmitter(request);
99+
plugin.tracer.wrapEmitter(response);
100+
101+
// Wraps end (inspired by:
102+
// https://github.com/GoogleCloudPlatform/cloud-trace-nodejs/blob/master/src/plugins/plugin-connect.ts#L75)
103+
const originalEnd = response.end;
104+
// tslint:disable:no-any
105+
response.end = function(this: any) {
106+
response.end = originalEnd;
107+
const returned = response.end.apply(this, arguments);
108+
109+
rootSpan.addAttribute('http.host', request.url.host);
110+
rootSpan.addAttribute('http.method', request.method);
111+
rootSpan.addAttribute('http.path', request.url.pathname);
112+
rootSpan.addAttribute('http.route', request.url.path);
113+
rootSpan.addAttribute(
114+
'http.user_agent', 'HTTPClient/' + request.httpVersion);
115+
rootSpan.addAttribute('http.status_code', request.statusCode);
116+
117+
rootSpan.status = plugin.traceStatus(request.statusCode);
118+
119+
// Message Event ID is not defined
120+
rootSpan.addMessageEvent(
121+
'MessageEventTypeRecv', uuid.v4().split('-').join(''));
122+
123+
// debug('Ended root span: ', rootSpan.name, rootSpan.id,
124+
// rootSpan.traceId);
125+
rootSpan.end();
126+
return returned;
127+
};
128+
129+
return original.apply(this, arguments);
130+
});
131+
};
132+
};
133+
}
134+
135+
/**
136+
* Creates spans for outgoing requests, sending spans' context for distributed
137+
* tracing.
138+
* @param plugin The HTTP plugin itself.
139+
*/
140+
patchOutgoingRequest() {
141+
const plugin: HttpPlugin = this;
142+
// tslint:disable:no-any
143+
return (original: any): any => {
144+
return function outgoingRequest() {
145+
// Makes sure the url is an url object
146+
if (typeof (arguments[0]) === 'string') {
147+
arguments[0] = url.parse(arguments[0]);
148+
}
149+
150+
// Do not trace ourselves
151+
if (arguments[0].headers &&
152+
arguments[0].headers['x-opencensus-outgoing-request']) {
153+
// tslint:disable:no-any
154+
return original.apply(this as any, arguments);
155+
}
156+
157+
const traceOptions = {
158+
name: arguments[0].pathname,
159+
type: 'CLIENT',
160+
};
161+
162+
// Checks if this outgoing request is part of an operation by checking
163+
// if there is a current root span, if so, we create a child span. In
164+
// case there is no root span, this means that the outgoing request is
165+
// the first operation, therefore we create a root span.
166+
if (!plugin.tracer.currentRootSpan) {
167+
return plugin.tracer.startRootSpan(
168+
traceOptions,
169+
plugin.makeRequestTrace(original, arguments, plugin));
170+
} else {
171+
const span: types.Span = plugin.tracer.startChildSpan(
172+
traceOptions.name, traceOptions.type);
173+
return (span: types.Span) =>
174+
plugin.makeRequestTrace(original, arguments, plugin);
175+
}
176+
};
177+
};
178+
}
179+
180+
/**
181+
* Injects span's context to header for distributed tracing and finshes the
182+
* span when the response is finished.
183+
* @param original The original patched function.
184+
* @param args The arguments to the original function.
185+
*/
186+
// tslint:disable:no-any
187+
makeRequestTrace(original: Function, args: any, plugin: HttpPlugin): any {
188+
return (span: types.Span) => {
189+
args[0].headers = B3Format.injectToHeader(args[0].headers, span);
190+
191+
const request = original.apply(this, args);
192+
193+
if (!span) {
194+
return request;
195+
}
196+
197+
// tslint:disable:no-any
198+
request.on('response', (response: any) => {
199+
response.on('end', () => {
200+
span.addAttribute('http.host', args[0].host);
201+
span.addAttribute('http.method', request.method);
202+
span.addAttribute('http.path', args[0].pathname);
203+
span.addAttribute('http.route', args[0].path);
204+
span.addAttribute(
205+
'http.user_agent', 'HTTPClient/' + request.httpVersion);
206+
span.addAttribute('http.status_code', request.statusCode);
207+
208+
span.status = plugin.traceStatus(request.statusCode);
209+
210+
// Message Event ID is not defined
211+
span.addMessageEvent(
212+
'MessageEventTypeSent', uuid.v4().split('-').join(''));
213+
214+
// debug('Ended span: ', span.name, span.id, span.traceId);
215+
span.end();
216+
});
217+
});
218+
219+
return request;
220+
};
221+
}
222+
223+
traceStatus(statusCode: number): number {
224+
if (statusCode < 200 || statusCode >= 400) {
225+
return traceStatusCodes.UNKNOWN;
226+
} else {
227+
switch (statusCode) {
228+
case (400):
229+
return traceStatusCodes.INVALID_ARGUMENT;
230+
case (504):
231+
return traceStatusCodes.DEADLINE_EXCEEDED;
232+
case (404):
233+
return traceStatusCodes.NOT_FOUND;
234+
case (403):
235+
return traceStatusCodes.PERMISSION_DENIED;
236+
case (401):
237+
return traceStatusCodes.UNAUTHENTICATED;
238+
case (429):
239+
return traceStatusCodes.RESOURCE_EXHAUSTED;
240+
case (501):
241+
return traceStatusCodes.UNIMPLEMENTED;
242+
case (503):
243+
return traceStatusCodes.UNAVAILABLE;
244+
default:
245+
return traceStatusCodes.OK;
246+
}
247+
}
248+
}
249+
}
42250

251+
enum traceStatusCodes {
252+
UNKNOWN = 2,
253+
OK = 0,
254+
INVALID_ARGUMENT = 3,
255+
DEADLINE_EXCEEDED = 4,
256+
NOT_FOUND = 5,
257+
PERMISSION_DENIED = 7,
258+
UNAUTHENTICATED = 16,
259+
RESOURCE_EXHAUSTED = 8,
260+
UNIMPLEMENTED = 12,
261+
UNAVAILABLE = 14
43262
}
44263

45-
module.exports = new HttpPlugin();
264+
const plugin = new HttpPlugin('http');
265+
export {plugin};

packages/opencensus-instrumentation-http/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2018 Google Inc. All Rights Reserved.
2+
* Copyright 2018, OpenCensus Authors
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)