appdynamics
Version:
Performance Profiler and Monitor
424 lines (358 loc) • 14.7 kB
JavaScript
/*
Copyright (c) AppDynamics, Inc., and its affiliates
2015
All Rights Reserved
*/
;
var HttpCommon = require('./http-common');
var HttpOTUtils = require('./http-ot-utils.js');
var url = require('url');
function HttpExitProbe(agent) {
this.agent = agent;
}
exports.HttpExitProbe = HttpExitProbe;
HttpExitProbe.prototype.init = function () { };
HttpExitProbe.prototype.attach = function (obj, moduleName) {
var self = this;
if (self.agent.tracer) {
self.ot_api = self.agent.TracerProvider.ot_api;
self.tracer = self.agent.tracer;
}
var profiler = self.agent.profiler;
var proxy = self.agent.proxy;
// we need to replicate Node core's http.get() implementation
// here so the call to http.request() uses our proxy wrapper;
// otherwise, Node's internal implementation uses the http
// module's internal binding, bypassing our instrumentation.
obj.get = function (url, options, cb) {
var req = obj.request(url, options, cb);
req.end();
return req;
};
// support 0.11.x and further
if (obj.globalAgent && obj.globalAgent.request) {
obj = obj.globalAgent;
}
if (typeof global.fetch === 'function' && !global.fetch.__appdynamicsProxyInfo__) {
self.instrumentFetch();
}
function clientCallback(locals) {
if (!locals.time.done()) return;
var exitCall = locals.exitCall;
var error = locals.error;
if (exitCall) {
if (locals.res) {
exitCall.responseHeaders = locals.res.headers;
exitCall.statusCode = ~~locals.res.statusCode;
if ((!error) && ((exitCall.statusCode < 200) || (exitCall.statusCode >= 400))) {
error = HttpCommon.getHttpExitCallError(exitCall.statusCode, exitCall.stack, locals);
}
}
if (locals.span) {
locals.span.setAttributes(HttpOTUtils.getOutgoingRequestAttributesOnResponse(locals.res, locals.opts.hostname));
locals.span.setStatus(HttpOTUtils.parseResponseStatus(locals.res));
locals.span.end();
}
profiler.addExitCall(locals.time, exitCall, error);
}
}
proxy.around(obj, 'request', function (obj, args, locals) {
var isRepeatForHttps = false;
var [input, options] = args;
if (typeof (input) != 'string' && !(input instanceof url.URL)) {
options = input;
input = null;
} else if (!options) {
options = {};
args[1] = options;
}
if (typeof (options) == 'function') {
args[2] = options;
options = {};
args[1] = options;
}
if (moduleName === 'https') {
options.__appdIsHttps = true;
}
self.setHttpDefaults(locals, args[0], args[1], moduleName);
if (locals.opts.method === 'CONNECT') {
// ignore proxy tunnel setup; requests sent via the
// proxy tunnel will get instrumented as they are made
options.appdIgnore = true;
return;
}
locals.time = profiler.time();
self.agent.logger.debug('HTTP exit call is initiated by the transaction for endpoint: ' +
locals.opts.method + ' ' +
locals.opts.hostname + ' ' +
locals.opts.port + ' ' +
locals.opts.path);
if (moduleName === 'http' && options.__appdIsHttps) {
isRepeatForHttps = true;
}
var isOtPath = self.agent.TracerProvider && locals.opts.hostname == self.agent.TracerProvider.host
&& locals.opts.port == self.agent.TracerProvider.port;
var isDualMode = self.agent.opts.dualModeConfig && self.agent.opts.dualModeConfig.collectorHost == locals.opts.hostname;
var isDynamoDBReq = options.headers && options.headers['X-Amz-Target'] && options.headers['X-Amz-Target'].indexOf('DynamoDB') > -1;
if (options.appdIgnore || isDynamoDBReq || isRepeatForHttps || isOtPath || isDualMode) {
// (ignore internal HTTP calls, e.g. to Analytics Agent, DynamoDB calls)
self.agent.logger.debug('Skipping HTTP exit call for the transaction.' +
'AppdIgnore is: ' + options.appdIgnore + ' ' +
'DynamoDB call is:' + isDynamoDBReq + ' ' +
'HTTPS call is:' + isRepeatForHttps + ' ' +
'isOtPath is :' + isOtPath + ' ' +
'isDualMode is :' + isDualMode);
} else {
self.agent.logger.debug('Gatheing HTTP exit call information.');
var threadId = self.agent.thread.current();
self.agent.metricsManager.addMetric(self.agent.metricsManager.HTTP_OUTGOING_COUNT, 1);
var host = locals.opts.hostname;
var port = locals.opts.port;
var path = locals.opts.path;
var supportedProperties = {
'HOST': host,
'PORT': port
};
var parsedUrl = url.parse(path);
supportedProperties.URL = parsedUrl.pathname;
if (parsedUrl.query) {
supportedProperties['QUERY STRING'] = parsedUrl.query;
}
var category = ((locals.opts.method === 'POST' || locals.opts.method === 'PUT') ? "write" : "read");
var span = {};
if (self.tracer) {
const currentSpan = self.ot_api.trace.getSpan(self.ot_api.context.active());
if (currentSpan) {
var method = locals.opts.method ? locals.opts.method : 'GET';
span = self.tracer.startSpan(method + ' ' + path, {
kind: self.ot_api.SpanKind.CLIENT,
attributes: HttpOTUtils.getOutgoingRequestAttributes(locals.opts, args[0].headers)
});
locals.span = span;
locals.parentSpan = currentSpan;
}
}
locals.exitCall = profiler.createExitCall(locals.time, {
exitType: 'EXIT_HTTP',
supportedProperties: supportedProperties,
stackTrace: profiler.stackTrace(),
group: (locals.opts.method || 'GET'),
method: locals.opts.method,
command: host + ':' + port + path,
category: category,
protocol: moduleName
});
if (!locals.exitCall) return;
Error.captureStackTrace(locals.exitCall);
var dataConsumption = false, res;
proxy.callback(args, -1, function (obj, args) {
res = args[0];
if (locals.parentSpan) {
self.ot_api.context.bind(res, locals.parentSpan);
}
// If there is no way to consume the data here, then close up the exit call loop and
// release the httpParser's object httpParserMethod.
// There are 3 ways to consume data from a readable stream according to the doc:
// https://nodejs.org/dist/latest-v4.x/docs/api/stream.html#stream_class_stream_readable
proxy.before(res, ['on', 'addListener'], function (obj, args) {
// workaround for end event
if (!dataConsumption && args[0] === 'data') {
dataConsumption = true;
}
proxy.callback(args, -1, null, null, threadId);
}, false, false, threadId);
proxy.before(res, 'pipe', function () {
dataConsumption = true;
}, false, false, threadId);
proxy.before(res, 'resume', function () {
dataConsumption = true;
}, false, false, threadId);
}, function () {
if (dataConsumption) return;
var httpParser = res.socket.parser,
httpParserMethod = httpParser.__proto__.constructor.kOnHeadersComplete;
proxy.release(httpParser[httpParserMethod]);
locals.res = res;
clientCallback(locals);
res.socket.__appdynamicsCleanup = true;
}, threadId);
}
},
function (obj, args, ret, locals) {
var [input, options] = args;
if (typeof (input) != 'string' && !(input instanceof url.URL)) {
options = input;
input = null;
}
options = options || {};
if (!options.appdIgnore && (moduleName != 'http' || (moduleName === 'http' && !options.__appdIsHttps))) {
if (locals.parentSpan) {
self.ot_api.context.bind(ret, locals.parentSpan);
}
var writeOnce = false, httpParser, httpParserMethod;
var threadId = locals.time.threadId;
proxy.before(ret, ['on', 'addListener'], function (obj, args) {
proxy.callback(args, -1, null, null, threadId);
}, false, false, threadId);
proxy.before(ret, ['write', 'end'], function (obj) {
if (!writeOnce) {
writeOnce = true;
proxy.callback(ret, -1, null, null, threadId);
} else {
return;
}
var correlationHeaderValue = undefined;
if (locals.exitCall) {
correlationHeaderValue = self.agent.backendConnector.getCorrelationHeader(locals.exitCall);
}
if (locals.span) {
var ot_context = self.ot_api.context.active();
if (correlationHeaderValue) {
var baggage = self.ot_api.propagation.getBaggage(ot_context) || {};
baggage[self.agent.correlation.HEADER_NAME] = { value: correlationHeaderValue };
ot_context = self.ot_api.propagation.setBaggage(ot_context, self.ot_api.propagation.createBaggage(baggage));
}
var headers = {};
self.ot_api.propagation.inject(self.ot_api.trace.setSpan(ot_context, locals.span), headers);
for (const [key, value] of Object.entries(headers)) {
obj.setHeader(key, value);
}
}
if (correlationHeaderValue) {
obj.setHeader(self.agent.correlation.HEADER_NAME, correlationHeaderValue);
}
}, false, false, threadId);
ret.on('socket', function (socket) {
httpParser = socket.parser;
httpParserMethod = httpParser.__proto__.constructor.kOnHeadersComplete;
var socketCloseHandler = function () {
socket.removeListener('close', socketCloseHandler);
if (socket.__appdynamicsCleanup) {
return;
}
proxy.release(httpParser[httpParserMethod]);
clientCallback(locals);
};
socket.on('close', socketCloseHandler);
proxy.after(httpParser, httpParserMethod, function (obj, args, ret) {
var resp = httpParser.incoming;
resp.on('end', function () {
proxy.release(httpParser[httpParserMethod]);
locals.res = resp;
clientCallback(locals);
socket.__appdynamicsCleanup = true;
socket.removeListener('close', socketCloseHandler);
});
return ret;
}, false, threadId);
});
ret.on('error', function (error) {
var currentCtxt = self.agent.thread.current();
self.agent.thread.resume(threadId);
if (httpParser && httpParserMethod) {
proxy.release(httpParser[httpParserMethod]);
}
locals.error = error;
clientCallback(locals);
if (ret.socket) {
ret.socket.__appdynamicsCleanup = true;
}
self.agent.thread.resume(currentCtxt);
});
}
});
};
HttpExitProbe.prototype.setHttpDefaults = function (locals, spec, opts, protocol) {
if (typeof (spec) === 'string') {
locals.opts = url.parse(spec);
} else if (spec instanceof url.URL) {
locals.opts = url.parse(spec.toString());
} else {
locals.opts = Object.assign({}, spec);
}
if (typeof (opts) === 'object') {
Object.assign(locals.opts, opts);
}
locals.opts.hostname = locals.opts.hostname || locals.opts.host || 'localhost';
locals.opts.port = locals.opts.port
|| locals.opts.defaultPort
|| locals.opts.agent && locals.opts.agent.defaultPort
|| ((protocol === 'https') ? 443 : 80);
locals.opts.path = locals.opts.path || '/';
};
HttpExitProbe.prototype.instrumentFetch = function () {
const self = this;
const profiler = self.agent.profiler;
const proxy = self.agent.proxy;
proxy.around(
global,
['fetch'],
function beforeFetch(_, args, locals) {
let [input, options = {}] = args;
const parsedUrl = typeof input === 'string' ? new url.URL(input) : input instanceof url.URL ? input : new url.URL(input.url);
const method = (options.method || 'GET').toUpperCase();
self.setHttpDefaults(locals, parsedUrl, args[1], parsedUrl.protocol === 'https:' ? 'https' : 'http');
locals.time = profiler.time();
self.agent.logger.debug('Instrumenting fetch request for endpoint: ' +
`${method} ${parsedUrl.hostname}:${parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80)}${parsedUrl.pathname}`);
self.agent.metricsManager.addMetric(self.agent.metricsManager.HTTP_OUTGOING_COUNT, 1);
locals.exitCall = profiler.createExitCall(locals.time, {
exitType: 'EXIT_HTTP',
supportedProperties: {
'HOST': locals.opts.hostname,
'PORT': locals.opts.port,
'URL': locals.opts.path,
},
stackTrace: profiler.stackTrace(),
group: method,
method,
command: `${locals.opts.hostname}:${locals.opts.port}${locals.opts.path}`,
category: ['POST', 'PUT'].includes(method) ? 'write' : 'read',
protocol: parsedUrl.protocol,
});
if (!locals.exitCall) return;
Error.captureStackTrace(locals.exitCall);
let correlationHeaderValue = undefined;
if (locals.exitCall) {
correlationHeaderValue = self.agent.backendConnector.getCorrelationHeader(locals.exitCall);
}
if ((typeof options === 'object' && options.headers == undefined)) {
options.headers = {};
}
if (correlationHeaderValue) {
args[1].headers = Object.assign(args[1].headers, {
[self.agent.correlation.HEADER_NAME]: correlationHeaderValue
});
}
return args;
},
function afterFetch(_, args, response, locals) {
if (!response || !response.data) {
return;
}
const res = response.data;
locals.res = res;
if (res.status < 200 || res.status >= 400) {
locals.error = new Error(`HTTP error! Status: ${res.status} - ${res.statusText}`);
}
self.finalizeClientCallback(profiler, locals);
return;
}
);
};
//replace the original function with this in the next commit after 24.12 release
HttpExitProbe.prototype.finalizeClientCallback = function (profiler, locals) {
if (!locals.time.done()) return;
var exitCall = locals.exitCall;
var error = locals.error;
if (exitCall) {
if (locals.res) {
exitCall.responseHeaders = locals.res.headers;
exitCall.statusCode = ~~locals.res.status || ~~locals.res.statusCode;
if ((!error) && ((exitCall.statusCode < 200) || (exitCall.statusCode >= 400))) {
error = HttpCommon.getHttpExitCallError(exitCall.statusCode, exitCall.stack, locals);
}
}
profiler.addExitCall(locals.time, exitCall, error);
}
};