UNPKG

appdynamics

Version:

Performance Profiler and Monitor

424 lines (358 loc) 14.7 kB
/* Copyright (c) AppDynamics, Inc., and its affiliates 2015 All Rights Reserved */ 'use strict'; 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); } };