apminsight
Version:
monitor nodejs applications
674 lines (615 loc) • 22.4 kB
JavaScript
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
};