UNPKG

apminsight

Version:

monitor nodejs applications

674 lines (615 loc) 22.4 kB
var webtxn = require("./metrics/webtxn"); var bgtxn = require("./metrics/bgtxn"); var tracker = require("./metrics/tracker"); var constants = require("./constants"); var config = require("./config/configuration"); var threshold = require("./config/threshold"); var utils = require("./util/utils"); var datastore = require("./metrics/metricstore"); var logger = require("./util/logger"); var instrument = require("./instrument"); var connadapter = require("./communication/connadapter"); var component = require("./metrics/component"); var insinfo = require("./communication/insinfo"); var wrapper = require("./instrument/wrapper"); var dataExporter = require("./communication/data-exporter"); function Agent(configInstance, stateInfo) { this._config = configInstance; this._threshold = new threshold(); this._keyTxnThreshold = {}; this._insinfo = new insinfo(stateInfo, configInstance); this._curTxn = null; this._curTracker = null; this._dependencyList = null; this._dataExporter = new dataExporter.DataExporter(configInstance); } function init(options) { options.agentBaseDir = utils.checkAndCreateBaseDir(options); var configInstance = new config.Configuration(options); if (configInstance.isProcessManagerEnabled() && options.agentBaseDir) { const pm2Id = configInstance.getPm2Id(); if (!utils.isEmpty(pm2Id)) { options.port = pm2Id; options.agentBaseDir = utils.checkpm2(pm2Id, options); } } logger.init(configInstance); // intializing the logger module before loading other modules if (!global.apmInsightAgentInstance) { var stateInfo = utils.loadInfoFileSync(configInstance); stateInfo.agentBaseDir = configInstance.getBaseDir(); global.apmInsightAgentInstance = new Agent(configInstance, stateInfo); // Initialize data exporter if enabled if (configInstance.isDataExporterEnabled()) { global.apmInsightAgentInstance.getDataExporter().init(); } instrument.start(); connadapter.init(); } else { logger.error( "apmInsightAgentInstance already injected in global scope" ); } } Agent.prototype.applyConfig = function (options) { if ( !this.getConfig().isConfiguredProperly() && typeof options === "object" ) { this._config = new config.Configuration(options); connadapter.checkAndSendConnect(); } }; Agent.prototype.createTxn = function (req, res, listnerName) { this.clearCurContext(); if (this._config.isDataExporterEnabled()) { var reqInfo = this.parseReq(req); reqInfo.rootListner = listnerName; // var self = this; var txn = new webtxn(reqInfo, req); this.setCurContext(txn, txn.getRootTracker()); this.checkAndInstrumentResponse(txn, res); this.handleCrossAppRequest(req, res, txn); this._dataExporter.shouldSample(reqInfo, function (result) { if (!result.shouldSample) { txn.setIgnore(); logger.debug("Transaction ignored as per data-exporter sampling decision for URL: " + reqInfo.uri); } // Update the transaction's tracker drop threshold if available if (result.trackerDropThreshold !== undefined) { txn.setTrackerDropThreshold(result.trackerDropThreshold); // logger.debug("Updated tracker drop threshold to: " + result.trackerDropThreshold + " for URL: " + reqInfo.uri); } // Store normalized transaction name if available if (result.normalizedTxnName !== undefined) { txn.setNormalizedTxnName(result.normalizedTxnName); } }); return txn; } else { // flow when data exporter is disabled if (!this.isDataCollectionAllowed()) { logger.info("data collection is not allowed in agent inactive state"); return; } var reqInfo = this.parseReq(req); reqInfo.rootListner = listnerName; if (!this._threshold.isTxnAllowed(reqInfo.uri)) { logger.info(req.url + " txn skipped because of config"); return; } try { var newGroupingName = this._threshold.isTxnGroupingConfigured( reqInfo.uri ); if (newGroupingName) { reqInfo.uri = newGroupingName; } } catch (err) { logger.error( "Error while assigning the new grouping name to the request uri." ); } var txn = new webtxn(reqInfo, req); this.setCurContext(txn, txn.getRootTracker()); this.checkAndInstrumentResponse(txn, res); this.handleCrossAppRequest(req, res, txn); return txn; } }; Agent.prototype.checkAndInstrumentResponse = function (txn, res) { if (!res) { return; } if (utils.isFunction(res.write) && utils.isFunction(res.end)) { var actWrite = res.write; var actEnd = res.end; var bytesOut = 0; res.write = function (data) { var res = actWrite.apply(this, arguments); bytesOut += utils.isNonEmptyString(data) ? Buffer.byteLength(data) : 0; return res; }; res.end = function (data) { var res = actEnd.apply(this, arguments); bytesOut += utils.isNonEmptyString(data) ? Buffer.byteLength(data) : 0; txn.setBytesOut(bytesOut); return res; }; } if (utils.isFunction(res.on)) { res.on("finish", function () { apmInsightAgentInstance.endTxn(txn, res); }); } }; Agent.prototype.handleCrossAppRequest = function (req, res, txn) { const isDistributedTracingEnabled = this._config.isDataExporterEnabled() || utils.getGenericThreshold(txn.getUrl()).isDistributedTracingEnabled(); if (isDistributedTracingEnabled) { if ( req.headers && this.getConfig().getLicenseKeySuffix() === req.headers[constants.crossAppReqHeaderLower] ) { res.setHeader( constants.crossAppResHeader, JSON.stringify({ s_time: txn.getStartTime(), t_name: constants.webTxnPrefix + txn.getUrl(), instance_id: this.getInsInfo().getInstanceId() }) ); } } if (req.headers) { if (req.headers[constants.rumIntegrationAppKeyLower]) { var rumIntegAppKey = req.headers[constants.rumIntegrationAppKeyLower]; if ( !this.getConfig().getRumIntegAppKey().includes(rumIntegAppKey) ) { this.getConfig().getRumIntegAppKey().push(rumIntegAppKey); this.getConfig()._newRumAppKeyAdded = true; } } if (req.headers[constants.rumIntegrationTraceIdLower]) { txn.setRumTraceId(req.headers[constants.rumIntegrationAppKeyLower]); } if (req.headers[constants.syntheticMonitoringKeyLower]) { txn.setSyntheticKey( req.headers[constants.syntheticMonitoringKeyLower] ); txn.setCustomParams( constants.syntheticMonitoringKey, req.headers[constants.syntheticMonitoringKeyLower] ); } } }; Agent.prototype.parseReq = function parseReq(req) { var reqInfo = { uri: "", queryParam: "", inputParam: {}, httpHeaders: {} }; if (req && req.url && !utils.isObject(req.url)) { let index = -1; try { index = req.url.indexOf("?"); } catch (err) { logger.error( "Error while checking whether the url has any query string." ); reqInfo.uri = req.url; // Fallback: treat the full URL as the URI. } if (index > 0) { reqInfo.uri = req.url.substr(0, index); if (req.url.length > index + 1) { try { if (this._config.isDataExporterEnabled()) { reqInfo.queryParam = req.url.substr(index + 1); } else { const genericTh = utils.getGenericThreshold(reqInfo.uri); if (genericTh && genericTh.isCaptureHttpParamsEnabled()) { reqInfo.queryParam = req.url.substr(index + 1); } } } catch (error) { logger.error("error while capturing HTTP parameters: ", error); } } } else { reqInfo.uri = req.url; } } if (req && req.headers) { let genericTh; try { let isCaptureHttpHeadersEnabled = this._config.isDataExporterEnabled; if (!isCaptureHttpHeadersEnabled) { genericTh = utils.getGenericThreshold(reqInfo.uri); isCaptureHttpHeadersEnabled = genericTh.isCaptureHttpHeadersEnabled(); } if (isCaptureHttpHeadersEnabled) { const httpHeadersFromReq = req.headers; Object.keys(httpHeadersFromReq).forEach(function (eachHeaderKey) { const headerValue = httpHeadersFromReq[eachHeaderKey]; if (utils.isNonEmptyString(headerValue)) { reqInfo.httpHeaders[eachHeaderKey] = headerValue.split(","); } else if (Array.isArray(headerValue)) { reqInfo.httpHeaders[eachHeaderKey] = headerValue; } }); if (!this._config.isDataExporterEnabled()) { var headersListToIgnore = genericTh && genericTh.getRequestHeadersIgnoreList(); if (!utils.isEmpty(headersListToIgnore)) { var ignoreAsArray = headersListToIgnore.split(","); ignoreAsArray.forEach(function (eachHeaderValue) { delete reqInfo.httpHeaders[eachHeaderValue]; }); } } } } catch (error) { logger.error("error while retrieving generic threshold for uri: " + reqInfo.uri + " : " + error); } } return reqInfo; }; Agent.prototype.endTxn = function (txn, res) { // Early return if transaction is invalid if (utils.isEmpty(txn)) { return; } // If data exporter is enabled, always end the transaction // If data exporter is disabled, only end if data collection is allowed if (this._config.isDataExporterEnabled() || this.isDataCollectionAllowed()) { txn.endTransaction(res); } }; Agent.prototype.startApiTransaction = function (txnName, func, isBgtxn) { if (!this.isOkayToCreateTxn(txnName, func, isBgtxn)) { return func(); } this.clearCurContext(); var rootTracker = utils.isEmpty(func.name) ? "Anonymous" : func.name; if (this._config.isDataExporterEnabled()) { var txnInfo = { uri: txnName, rootListner: rootTracker, api: true }; var txn = isBgtxn ? new bgtxn(txnInfo) : new webtxn(txnInfo); var track = txn.getRootTracker(); this.setCurContext(txn); // Create reqInfo for shouldSample call const reqInfo = { uri: txnName }; this._dataExporter.shouldSample(reqInfo, function (result) { if (!result.shouldSample) { txn.setIgnore(); logger.debug("Transaction ignored as per data-exporter sampling decision for URL: " + txnName); } // Update the transaction's tracker drop threshold if available if (result.trackerDropThreshold !== undefined) { txn.setTrackerDropThreshold(result.trackerDropThreshold); } // Store normalized transaction name if available if (result.normalizedTxnName !== undefined) { txn.setNormalizedTxnName(result.normalizedTxnName); } }); try { var res = func(); wrapper.checkAndWrapPromise(res, txn, track); return res; } catch (e) { this.handleErrorTracker(track, e); throw e; } finally { this.clearCurContext(); } } else { // flow when data exporter is disabled try { var newGroupingName = this._threshold.isTxnGroupingConfigured(txnName); if (newGroupingName) { txnName = newGroupingName; } } catch (err) { logger.error( "Error while assigning the new grouping name to the request uri. " ); } var txnInfo = { uri: txnName, rootListner: rootTracker, api: true }; var txn = isBgtxn ? new bgtxn(txnInfo) : new webtxn(txnInfo); var track = txn.getRootTracker(); this.setCurContext(txn); try { var res = func(); wrapper.checkAndWrapPromise(res, txn, track); return res; } catch (e) { this.handleErrorTracker(track, e); throw e; } finally { this.clearCurContext(); } } }; Agent.prototype.isOkayToCreateTxn = function (txnName, func, isBgtxn) { if (!this.isDataCollectionAllowed()) { logger.info("data collection is not allowed in agent inactive state"); return false; } if (utils.isEmpty(txnName)) { logger.critical("create txn invoked with empty txn name"); return false; } if (!utils.isFunction(func)) { logger.critical("create txn invoked without function param"); return false; } if (!this._config.isDataExporterEnabled()) { if (!this._threshold.isTxnAllowed(txnName)) { logger.info(txnName + " txn skipped because of config"); return; } if ( isBgtxn && !utils.getGenericThreshold(txnName).isBgTxnTrackingEnabled() ) { logger.info("bg txn tracking disabled"); return false; } } return true; }; Agent.prototype.endApiTxn = function (err) { this._curTxn && this._curTxn.endApiTxn(err); }; Agent.prototype.setTransactionName = function (name) { this._curTxn && this._curTxn.setTransactionName(name); }; Agent.prototype.addParameter = function (key, value) { this._curTxn && this._curTxn.setCustomParams(key, value); }; Agent.prototype.ignoreCurTxn = function () { this._curTxn && this._curTxn.setIgnore(); }; Agent.prototype.createTracker = function (trackerInfo) { if (!this.getCurTxn()) { return; } var tempTracker; trackerInfo.parent = trackerInfo.parent || this.getCurTracker() || this.getCurTxn().getRootTracker(); // Assign method order during tracker creation (irrespective of data exporter) trackerInfo.methodOrder = this.getCurTxn().getNextMethodOrder(); if (!trackerInfo.isDbTracker && !trackerInfo.isEsTracker) { tempTracker = new tracker.Tracker(trackerInfo); } else if (trackerInfo.isEsTracker) { tempTracker = new tracker.EsTracker(trackerInfo); } else if ( this._threshold.isSqlCaptureEnabled() && trackerInfo.isDbTracker ) { tempTracker = new tracker.DbTracker(trackerInfo); } else { return; } this.setCurTracker(tempTracker); var parentTracker = trackerInfo.parent ? trackerInfo.parent : this.getCurTxn().getRootTracker(); parentTracker.addChildTracker(tempTracker); return tempTracker; }; Agent.prototype.createCustomTracker = function ( trackerName, componentName, handler, cb ) { if (!utils.isFunction(handler)) { return; } if (!this.getCurTxn() || this.getCurTxn().isCompleted()) { return handler(cb); } var parentTracker = this.getCurTracker(); var handlerName = utils.isEmpty(handler.name) ? "Custom-Tracker" : handler.name; trackerName = utils.isNonEmptyString(trackerName) ? trackerName : handlerName; var trackerInfo = { trackerName: trackerName, component: componentName, api: true }; trackerInfo.parent = parentTracker; var customTracker; if (utils.isFunction(cb)) { try { customTracker = this.createTracker(trackerInfo); cb = wrapper.wrapCallBack(cb, customTracker, this.getCurTxn()); return handler(cb); } catch (e) { this.handleErrorTracker(customTracker, e); } finally { this.setCurContext(this.getCurTxn(), parentTracker); } } else { try { trackerInfo.sync = true; customTracker = this.createTracker(trackerInfo); var res = handler(cb); if (res && utils.isFunction(res.then)) { wrapper.checkAndWrapPromise( res, this.getCurTxn(), customTracker ); } else { this.endTracker(customTracker, this.getCurTxn()); } return res; } catch (e) { this.handleErrorTracker(customTracker, e); } finally { this.setCurContext(this.getCurTxn(), parentTracker); } } }; Agent.prototype.endTracker = function (tracker, txn) { if (tracker) { tracker.endTracker(txn ? txn : this._curTxn); if (this._curTracker === tracker) { this.clearCurTracker(); } } }; Agent.prototype.handleErrorTracker = function (anyTracker, err) { if (anyTracker) { anyTracker.endTracker(this._curTxn, err); this.clearCurTracker(); } }; Agent.prototype.trackError = function (err) { if (err instanceof Error) { var parentTracker = this._curTracker; var errorTracker = this.createTracker({ trackerName: "Error", sync: true, parent: parentTracker }); if (errorTracker) { errorTracker.setError(err, this._curTxn); errorTracker.endTracker(this._curTxn); } this.setCurContext(this._curTxn, parentTracker); } }; Agent.prototype.getRumScript = function () { var rumScript = ""; try { if (!utils.isEmpty(this._config._rumAppKey)) { rumScript = constants.rumBeacon .replace("{static_beacon}", this._config._rumBeaconHost) .replace("{rum_key}", this._config._rumAppKey); } else { logger.error("RUM api key is empty"); //No I18N } } catch (e) { logger.error("Error while getting rum script " + e); //No I18N } return rumScript; }; Agent.prototype.endRootTracker = function (err) { if (!utils.isEmpty(this._curTxn)) { this._curTxn.endRootTracker(err); } }; Agent.prototype.checkAndAddComponent = function (txn, tracker) { if (!txn) { return; } txn.incrTrackersCount(); if (!utils.isEmpty(tracker.getComponent())) { var comp = new component.Component(tracker); txn.aggregateComponent(comp); } }; Agent.prototype.incrementCustomMetric = function (metricName, incrValue) { if (!this.isDataCollectionAllowed()) { logger.info("data collection is not allowed in agent inactive state"); return; } datastore.incrAppMetric(metricName, incrValue); }; Agent.prototype.averageCustomMetric = function (metricName, metricValue) { if (!this.isDataCollectionAllowed()) { logger.info("data collection is not allowed in agent inactive state"); return; } datastore.avgAppMetric(metricName, metricValue); }; Agent.prototype.instrumentingWebFramework = function (moduleName, moduleInfo) { instrument.instrumentCustomModules(moduleName, moduleInfo); }; Agent.prototype.setCurTxn = function (txn) { this._curTxn = txn; }; Agent.prototype.getCurTxn = function () { return this._curTxn; }; Agent.prototype.clearCurTxn = function () { this._curTxn = null; }; Agent.prototype.setCurTracker = function (tracker) { this._curTracker = tracker; }; Agent.prototype.getCurTracker = function () { return this._curTracker; }; Agent.prototype.clearCurTracker = function () { this._curTracker = null; }; Agent.prototype.setDependencyList = function (dependencyList) { this._dependencyList = dependencyList; }; Agent.prototype.getDependencyList = function () { return this._dependencyList; }; Agent.prototype.setCurContext = function (txn, tracker) { this.setCurTxn(txn); this.setCurTracker(tracker); }; Agent.prototype.clearCurContext = function () { this._curTxn = null; this._curTracker = null; }; Agent.prototype.getThreshold = function () { return this._threshold; }; Agent.prototype.getKeyTxnThreshold = function () { return this._keyTxnThreshold; }; Agent.prototype.getConfig = function () { return this._config; }; Agent.prototype.getDataExporter = function () { return this._dataExporter; }; Agent.prototype.isDataCollectionAllowed = function () { if (this._config.isDataExporterEnabled()) { return true; } var curStatus = this._insinfo.getStatus(); if (!curStatus || curStatus === constants.manageAgent) { return true; } return false; }; Agent.prototype.getInsInfo = function () { return this._insinfo; }; Agent.prototype.getRumScript = function () { var rumScript = ""; try { if (!utils.isEmpty(this._config._rumApiKey)) { rumScript = constants.rumBeacon .replace("{static_beacon}", this._config._rumBeaconHost) .replace("{rum_key}", this._config._rumApiKey); } else { logger.error("RUM api key is empty"); //No I18N } } catch (e) { logger.error("Error while getting rum script " + e); //No I18N } return rumScript; }; module.exports = { Agent: Agent, init: init };