Skip to content

Commit 5e72dc8

Browse files
sjanuarytobespc
authored andcommitted
Add HTTPS probes and tests (#455)
* Add https probes and tests * fix error in https test * style and readability changes * implement review comments * Only parse URLs once in http and https outbound probes
1 parent 2ce0f90 commit 5e72dc8

12 files changed

+678
-47
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
"fix-prettier": "prettier --single-quote --trailing-comma es5 --print-width 120 --write {bin,lib,probes,tests}/**/*.js *.js",
3030
"fix-eslint": "eslint --fix {bin,lib,probes,tests}/**/*.js *.js",
3131
"pretest": "eslint .",
32-
"test": "tap --reporter tap --timeout=120 tests/*tests.js tests/probes/http-outbound-probe-test.js tests/probes/http-probe-test.js tests/headless_test.js",
33-
"travis": "tap --reporter tap --timeout=120 tests/*tests.js tests/probes/http-outbound-probe-test.js tests/probes/http-probe-test.js tests/headless_test.js --coverage",
32+
"test": "tap --reporter tap --timeout=120 tests/*tests.js tests/probes/http*test.js tests/headless_test.js",
33+
"travis": "tap --reporter tap --timeout=120 tests/*tests.js tests/probes/http*test.js tests/headless_test.js --coverage",
3434
"posttravis": "./get_code_cov.sh && tap --coverage-report=lcov && codecov --disable=gcov",
3535
"install": "node extract_all_binaries.js || node-gyp rebuild"
3636
},

probes/http-outbound-probe.js

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var am = require('../');
2323
var semver = require('semver');
2424

2525
var methods;
26+
// In Node.js < v8.0.0 'get' calls 'request' so we only instrument 'request'
2627
if (semver.lt(process.version, '8.0.0')) {
2728
methods = ['request'];
2829
} else {
@@ -38,7 +39,7 @@ util.inherits(HttpOutboundProbe, Probe);
3839

3940
HttpOutboundProbe.prototype.attach = function(name, target) {
4041
var that = this;
41-
if (name == 'http') {
42+
if (name === 'http') {
4243
if (target.__outboundProbeAttached__) return target;
4344
target.__outboundProbeAttached__ = true;
4445

@@ -47,40 +48,41 @@ HttpOutboundProbe.prototype.attach = function(name, target) {
4748
methods,
4849
// Before 'http.request' function
4950
function(obj, methodName, methodArgs, probeData) {
50-
// Get HTTP request method from options
51-
var options = methodArgs[0];
52-
var requestMethod = 'GET';
53-
var urlRequested = '';
54-
var headers = '';
55-
if (typeof options === 'object') {
56-
urlRequested = formatURL(options);
57-
if (options.method) {
58-
requestMethod = options.method;
59-
}
60-
if (options.headers) {
61-
headers = options.headers;
62-
}
63-
} else if (typeof options === 'string') {
64-
urlRequested = options;
65-
var parsedOptions = url.parse(options);
66-
if (parsedOptions.method) {
67-
requestMethod = parsedOptions.method;
68-
}
69-
if (parsedOptions.headers) {
70-
headers = parsedOptions.headers;
71-
}
72-
}
7351

7452
// Start metrics
75-
that.metricsProbeStart(probeData, requestMethod, urlRequested);
76-
that.requestProbeStart(probeData, requestMethod, urlRequested);
53+
that.metricsProbeStart(probeData);
54+
that.requestProbeStart(probeData);
7755

7856
// End metrics
7957
aspect.aroundCallback(
8058
methodArgs,
8159
probeData,
8260
function(target, args, probeData) {
83-
methodArgs.statusCode = args[0].statusCode;
61+
62+
// Get HTTP request method from options
63+
var options = methodArgs[0];
64+
var requestMethod = 'GET';
65+
var urlRequested = '';
66+
var headers = '';
67+
if (options !== null && typeof options === 'object') {
68+
urlRequested = formatURL(options);
69+
if (options.method) {
70+
requestMethod = options.method;
71+
}
72+
if (options.headers) {
73+
headers = options.headers;
74+
}
75+
} else if (typeof options === 'string') {
76+
urlRequested = options;
77+
var parsedOptions = url.parse(options);
78+
if (parsedOptions.method) {
79+
requestMethod = parsedOptions.method;
80+
}
81+
if (parsedOptions.headers) {
82+
headers = parsedOptions.headers;
83+
}
84+
}
85+
8486
that.metricsProbeEnd(probeData, requestMethod, urlRequested, args[0], headers);
8587
that.requestProbeEnd(probeData, requestMethod, urlRequested, args[0], headers);
8688
},
@@ -93,13 +95,13 @@ HttpOutboundProbe.prototype.attach = function(name, target) {
9395
// After 'http.request' function returns
9496
function(target, methodName, methodArgs, probeData, rc) {
9597
// If no callback has been used then end the metrics after returning from the method instead
96-
if (aspect.findCallbackArg(methodArgs) == undefined) {
98+
if (aspect.findCallbackArg(methodArgs) === undefined) {
9799
// Need to get request method and URL again
98100
var options = methodArgs[0];
99101
var requestMethod = 'GET';
100102
var urlRequested = '';
101103
var headers = '';
102-
if (typeof options === 'object') {
104+
if (options !== null && typeof options === 'object') {
103105
urlRequested = formatURL(options);
104106
if (options.method) {
105107
requestMethod = options.method;
@@ -181,7 +183,7 @@ HttpOutboundProbe.prototype.metricsEnd = function(probeData, method, url, res, h
181183
url: url,
182184
duration: probeData.timer.timeDelta,
183185
statusCode: res.statusCode,
184-
contentType: res.headers ? res.headers['content-type'] : 'undefined',
186+
contentType: res.headers ? res.headers['content-type'] : undefined,
185187
requestHeaders: headers,
186188
});
187189
}
@@ -201,7 +203,7 @@ HttpOutboundProbe.prototype.requestEnd = function(probeData, method, url, res, h
201203
probeData.req.stop({
202204
url: url,
203205
statusCode: res.statusCode,
204-
contentType: res.headers ? res.headers['content-type'] : 'undefined',
206+
contentType: res.headers ? res.headers['content-type'] : undefined,
205207
requestHeaders: headers,
206208
});
207209
};

probes/http-probe.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ util.inherits(HttpProbe, Probe);
3030

3131
HttpProbe.prototype.attach = function(name, target) {
3232
var that = this;
33-
if (name == 'http') {
33+
if (name === 'http') {
3434
if (target.__probeAttached__) return target;
3535
target.__probeAttached__ = true;
3636
var methods = ['on', 'addListener'];
@@ -61,7 +61,7 @@ HttpProbe.prototype.attach = function(name, target) {
6161
/*
6262
* Custom req.url parser that strips out any trailing query
6363
*/
64-
var parse = function(url) {
64+
function parse(url) {
6565
['?', '#'].forEach(function(separator) {
6666
var index = url.indexOf(separator);
6767
if (index !== -1) url = url.substring(0, index);
@@ -75,7 +75,7 @@ var parse = function(url) {
7575
HttpProbe.prototype.filterUrl = function(req) {
7676
var resultUrl = parse(req.url);
7777
var filters = this.config.filters;
78-
if (filters.length == 0) return resultUrl;
78+
if (filters.length === 0) return resultUrl;
7979

8080
var identifier = req.method + ' ' + resultUrl;
8181
for (var i = 0; i < filters.length; ++i) {
@@ -124,7 +124,7 @@ HttpProbe.prototype.requestStart = function(probeData, method, url) {
124124
};
125125

126126
HttpProbe.prototype.requestEnd = function(probeData, method, url, res, httpReq) {
127-
if (probeData && probeData.req)
127+
if (probeData && probeData.req) {
128128
probeData.req.stop({
129129
url: url,
130130
method: method,
@@ -133,6 +133,7 @@ HttpProbe.prototype.requestEnd = function(probeData, method, url, res, httpReq)
133133
header: res._header,
134134
contentType: res.getHeader('content-type'),
135135
});
136+
}
136137
};
137138

138139
/*

probes/https-outbound-probe.js

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*******************************************************************************
2+
* Copyright 2017 IBM Corp.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*******************************************************************************/
16+
'use strict';
17+
18+
var am = require('../');
19+
var aspect = require('../lib/aspect.js');
20+
var Probe = require('../lib/probe.js');
21+
var request = require('../lib/request.js');
22+
23+
var url = require('url');
24+
var util = require('util');
25+
26+
// In https 'get' calls 'request' so we only instrument 'request'
27+
var methods = ['request'];
28+
29+
// Probe to instrument outbound https requests
30+
31+
function HttpsOutboundProbe() {
32+
Probe.call(this, 'https'); // match the name of the module we're instrumenting
33+
}
34+
util.inherits(HttpsOutboundProbe, Probe);
35+
36+
HttpsOutboundProbe.prototype.attach = function(name, target) {
37+
var that = this;
38+
if (name === 'https') {
39+
if (target.__outboundProbeAttached__) return target;
40+
target.__outboundProbeAttached__ = true;
41+
42+
aspect.around(
43+
target,
44+
methods,
45+
// Before 'https.request' function
46+
function(obj, methodName, methodArgs, probeData) {
47+
48+
// Start metrics
49+
that.metricsProbeStart(probeData);
50+
that.requestProbeStart(probeData);
51+
52+
// End metrics
53+
aspect.aroundCallback(
54+
methodArgs,
55+
probeData,
56+
function(target, args, probeData) {
57+
58+
// Get HTTPS request method from options
59+
var options = methodArgs[0];
60+
var requestMethod = 'GET';
61+
var urlRequested = '';
62+
var headers = '';
63+
if (options !== null && typeof options === 'object') {
64+
urlRequested = formatURL(options);
65+
if (options.method) {
66+
requestMethod = options.method;
67+
}
68+
if (options.headers) {
69+
headers = options.headers;
70+
}
71+
} else if (typeof options === 'string') {
72+
urlRequested = options;
73+
var parsedOptions = url.parse(options);
74+
if (parsedOptions.method) {
75+
requestMethod = parsedOptions.method;
76+
}
77+
if (parsedOptions.headers) {
78+
headers = parsedOptions.headers;
79+
}
80+
}
81+
82+
that.metricsProbeEnd(probeData, requestMethod, urlRequested, args[0], headers);
83+
that.requestProbeEnd(probeData, requestMethod, urlRequested, args[0], headers);
84+
},
85+
function(target, args, probeData, ret) {
86+
// Don't need to do anything after the callback
87+
return ret;
88+
}
89+
);
90+
},
91+
// After 'https.request' function returns
92+
function(target, methodName, methodArgs, probeData, rc) {
93+
// If no callback has been used then end the metrics after returning from the method instead
94+
if (aspect.findCallbackArg(methodArgs) === undefined) {
95+
// Need to get request method and URL again
96+
var options = methodArgs[0];
97+
var requestMethod = 'GET';
98+
var urlRequested = '';
99+
var headers = '';
100+
if (options !== null && typeof options === 'object') {
101+
urlRequested = formatURL(options);
102+
if (options.method) {
103+
requestMethod = options.method;
104+
}
105+
if (options.headers) {
106+
headers = options.headers;
107+
}
108+
} else if (typeof options === 'string') {
109+
urlRequested = options;
110+
var parsedOptions = url.parse(options);
111+
if (parsedOptions.method) {
112+
requestMethod = parsedOptions.method;
113+
}
114+
if (parsedOptions.headers) {
115+
headers = parsedOptions.headers;
116+
}
117+
}
118+
119+
// End metrics (no response available so pass empty object)
120+
that.metricsProbeEnd(probeData, requestMethod, urlRequested, {}, headers);
121+
that.requestProbeEnd(probeData, requestMethod, urlRequested, {}, headers);
122+
}
123+
return rc;
124+
}
125+
);
126+
}
127+
return target;
128+
};
129+
130+
// Get a URL as a string from the options object passed to https.get or https.request
131+
// See https://nodejs.org/api/http.html#http_http_request_options_callback
132+
function formatURL(httpsOptions) {
133+
var url;
134+
if (httpsOptions.protocol) {
135+
url = httpsOptions.protocol;
136+
} else {
137+
url = 'https:';
138+
}
139+
url += '//';
140+
if (httpsOptions.auth) {
141+
url += httpsOptions.auth + '@';
142+
}
143+
if (httpsOptions.host) {
144+
url += httpsOptions.host;
145+
} else if (httpsOptions.hostname) {
146+
url += httpsOptions.host;
147+
} else {
148+
url += 'localhost';
149+
}
150+
if (httpsOptions.port) {
151+
url += ':' + httpsOptions.port;
152+
}
153+
if (httpsOptions.path) {
154+
url += httpsOptions.path;
155+
} else {
156+
url += '/';
157+
}
158+
return url;
159+
}
160+
161+
/*
162+
* Lightweight metrics probe for HTTPS requests
163+
*
164+
* These provide:
165+
* time: time event started
166+
* method: HTTPS method, eg. GET, POST, etc
167+
* url: The url requested
168+
* requestHeaders: The HTTPS headers for the request
169+
* duration: The time for the request to respond
170+
* contentType: HTTPS content-type
171+
* statusCode: HTTPS status code
172+
*/
173+
HttpsOutboundProbe.prototype.metricsEnd = function(probeData, method, url, res, headers) {
174+
if (probeData && probeData.timer) {
175+
probeData.timer.stop();
176+
am.emit('https-outbound', {
177+
time: probeData.timer.startTimeMillis,
178+
method: method,
179+
url: url,
180+
duration: probeData.timer.timeDelta,
181+
statusCode: res.statusCode,
182+
contentType: res.headers ? res.headers['content-type'] : undefined,
183+
requestHeaders: headers,
184+
});
185+
}
186+
};
187+
188+
/*
189+
* Heavyweight request probes for HTTPS outbound requests
190+
*/
191+
HttpsOutboundProbe.prototype.requestStart = function(probeData, method, url) {
192+
var reqType = 'https-outbound';
193+
// Do not mark as a root request
194+
probeData.req = request.startRequest(reqType, url, false, probeData.timer);
195+
};
196+
197+
HttpsOutboundProbe.prototype.requestEnd = function(probeData, method, url, res, headers) {
198+
if (probeData && probeData.req)
199+
probeData.req.stop({
200+
url: url,
201+
statusCode: res.statusCode,
202+
contentType: res.headers ? res.headers['content-type'] : undefined,
203+
requestHeaders: headers,
204+
});
205+
};
206+
207+
module.exports = HttpsOutboundProbe;

0 commit comments

Comments
 (0)