appdynamics
Version:
Performance Profiler and Monitor
744 lines (631 loc) • 23.9 kB
JavaScript
/*
Copyright (c) AppDynamics, Inc., and its affiliates
2015
All Rights Reserved
*/
'use strict';
var os = require('os');
var fs = require('fs');
var util = require('util');
var path = require('path');
var EventEmitter = require('events').EventEmitter;
var crypto = require('crypto');
var Context = require('./context').Context;
var Logger = require('./logger').Logger;
var Timers = require('./timers').Timers;
var System = require('./system').System;
var AppDProxy = require('./appDProxy').AppDProxy;
var Thread = require('./thread').Thread;
var MetricsManager = require('../metrics/metrics-manager').MetricsManager;
var ProcessInfo = require('../process/process-info').ProcessInfo;
var ProcessScanner = require('../process/process-scanner').ProcessScanner;
var ProcessStats = require('../process/process-stats').ProcessStats;
var InstanceTracker = require('../process/instance-tracker').InstanceTracker;
var StringMatcher = require('../proxy/string-matcher').StringMatcher;
var ExpressionEvaluator = require('../proxy/expression-evaluator').ExpressionEvaluator;
var Correlation = require('../transactions/correlation').Correlation;
var Eum = require('../transactions/eum').Eum;
var Profiler = require('../profiler/profiler').Profiler;
var CustomTransaction = require('../profiler/custom-transaction').CustomTransaction;
var GCStats = require('../v8/gc-stats').GCStats;
var CpuProfiler = require('../v8/cpu-profiler').CpuProfiler;
var HeapProfiler = require('../v8/heap-profiler').HeapProfiler;
var agentVersion = require('../../appdynamics_version.json');
var appDNativeLoader = require('appdynamics-native');
var SecureApp = require('../secure_app/secure_app').SecureApp;
var LibAgent = require('../libagent/libagent').LibAgent;
var LibagentConnector = require('../libagent/libagent-connector').LibagentConnector;
var TransactionSender = require('../libagent/transaction-sender').TransactionSender;
var ProcessSnapshotSender = require('../libagent/process-snapshot-sender').ProcessSnapshotSender;
var MetricSender = require('../libagent/metric-sender').MetricSender;
var InstanceInfoSender = require('../libagent/instance-info-sender').InstanceInfoSender;
function Agent() {
this.isWindows = os.platform() == 'win32';
this.rootTmpDir = '/tmp/appd';
if (this.isWindows) {
this.rootTmpDir = os.tmpdir();
} else if (!fs.existsSync('/tmp')) {
this.rootTmpDir = './tmp/appd'; // Heroku case
}
this.initialized = false;
this.version = agentVersion.version;
this.compatibilityVersion = agentVersion.compatibilityVersion;
this.nextId = parseInt(crypto.randomBytes(4).toString('hex'), 16) % 1e6;
this.appdNative = undefined;
this.meta = [];
// predefine options
this.precompiled = undefined;
this.proxyCtrlDir = undefined;
this.proxyLogsDir = undefined;
this.proxyRuntimeDir = undefined;
EventEmitter.call(this);
// create modules
this.context = new Context(this);
this.logger = new Logger(this);
this.timers = new Timers(this);
this.system = new System(this);
this.proxy = new AppDProxy(this);
this.thread = new Thread(this);
this.metricsManager = new MetricsManager(this);
this.processInfo = new ProcessInfo(this);
this.processScanner = new ProcessScanner(this);
this.processStats = new ProcessStats(this);
this.instanceTracker = new InstanceTracker(this);
this.stringMatcher = new StringMatcher(this);
this.expressionEvaluator = new ExpressionEvaluator(this);
this.correlation = new Correlation(this);
this.eum = new Eum(this);
this.profiler = new Profiler(this);
this.customTransaction = new CustomTransaction(this);
this.gcStats = new GCStats(this);
this.cpuProfiler = new CpuProfiler(this);
this.heapProfiler = new HeapProfiler(this);
this.secureApp = new SecureApp(this);
this.libagent = null;
// nsolid support
if (typeof (nsolid) == 'undefined') {
try {
this.nsolid = require('nsolid');
} catch (e) {
// ignored; not available
}
} else {
this.nsolid = nsolid; // eslint-disable-line no-undef
}
this.nsolidEnabled = typeof (this.nsolid) !== 'undefined' &&
typeof (this.nsolid.info) === 'function';
this.nsolidMetaCollected = false;
// libagent
this.libagentConnector = new LibagentConnector(this);
this.transactionSender = new TransactionSender(this);
this.processSnapshotSender = new ProcessSnapshotSender(this);
this.metricSender = new MetricSender(this);
this.instanceInfoSender = new InstanceInfoSender(this);
}
util.inherits(Agent, EventEmitter);
/* istanbul ignore next */
Agent.prototype.recursiveMkDir = function (dir) {
var dirsToMake = [];
var currDir = path.normalize(dir);
var parentDir = path.dirname(currDir);
while (currDir !== parentDir) {
if (fs.existsSync(currDir))
break;
dirsToMake.push(currDir);
currDir = parentDir;
parentDir = path.dirname(currDir);
if (currDir == parentDir)
break;
}
while (dirsToMake.length) {
currDir = dirsToMake.pop();
try {
fs.mkdirSync(currDir);
} catch (e) {
if (e.code != 'EEXIST') throw e;
}
}
};
Agent.computeTmpDir = function (rootTmpDir,
controllerHost,
controllerPort,
appName,
tierName,
nodeName) {
var stringToHash = controllerHost.toString() + ',' +
controllerPort.toString() + ',' +
appName.toString() + ',' +
tierName.toString() + ',' +
nodeName.toString();
// We don't need cryptographic security here,
// just a unique, predictable fixed length directory name.
var sha256 = crypto.createHash('sha256');
sha256.update(stringToHash);
return path.join(rootTmpDir, sha256.digest('hex').slice(0, 32));
};
Agent.prototype.setKubernetesConfig = function() {
var self = this;
try {
const NSPACE_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
let clusterService = (process.env.APPDYNAMICS_CONTAINERINFO_FETCH_SERVICE).split(":");
if(clusterService.length != 2) {
return;
}
self.opts.kbHost = clusterService[0];
self.opts.kbPort = parseInt(clusterService[1]);
self.opts.kbPodName = process.env.HOSTNAME;
self.opts.kbContainerName = process.env.APPDYNAMICS_CONTAINER_NAME;
self.opts.kbNspace = fs.readFileSync(NSPACE_PATH, 'utf-8').trim();
self.opts.kbConfigEnabled = true;
} catch(err) {
self.logger.warn('Kubernetes containerid disabled due to' + err + '\n' + err.stack);
}
};
Agent.prototype.isMainThread = function () {
const { isMainThread } = require('worker_threads');
return isMainThread;
};
function setMetadataFile(agent, instrumentedStatus) {
const filePath = process.env.APPDYNAMICS_AGENT_DIR || '/tmp/appd';
const fileName = path.join(filePath, 'app-metadata.json');
try {
fs.accessSync(filePath, fs.constants.W_OK);
const content = JSON.stringify({ instrumented: instrumentedStatus });
fs.writeFileSync(fileName, content);
} catch (err) {
agent.logger.error(
`Unable to write the file app-metadata.json to path ${filePath} \n`,
err
);
}
}
/* istanbul ignore next -- requires too much stubbing and mocking to unit test */
Agent.prototype.init = function (opts) {
var self = this;
if(!self.isMainThread()) {
return;
}
try {
if (self.initialized)
return;
self.initialized = true;
self.initializeOpts(opts);
self.opts.clsDisabled = !!self.opts.clsDisabled;
if(process.env.APPDYNAMICS_CONTAINERINFO_FETCH_SERVICE) {
self.setKubernetesConfig();
}
self.backendConnector = new LibAgent(this);
// agent_deployment_mode = 'appd' || 'dual' || 'hybrid'
let agent_deployment_mode = opts.agent_deployment_mode || process.env.agent_deployment_mode;
if(agent_deployment_mode == 'dual' || opts.openTelemetry && opts.openTelemetry.enabled) {
const url = require('url');
let collectorEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
let collectorEndpointUrl = url.parse(collectorEndpoint);
self.opts.dualModeConfig = {
collectorHost: collectorEndpointUrl.hostname,
collectorPort: collectorEndpointUrl.port,
};
try {
const { start } = require('@splunk/otel');
start();
}
catch (err) {
self.logger.error('AppDynamics dual mode could not be started ' + err);
}
}
self.libagentConnector.subscribeToIsEnabled((isEnabled) => setMetadataFile(self, isEnabled));
self.precompiled = opts.precompiled === undefined || opts.precompiled;
if (self.opts.excludeAgentFromCallGraph === undefined) {
self.opts.excludeAgentFromCallGraph = true;
}
if (self.opts.rootTmpDir) {
self.rootTmpDir = self.opts.rootTmpDir;
}
// Temp directory
if (self.opts.tmpDir) {
self.tmpDir = self.opts.tmpDir;
}
else {
if (!self.opts.controllerHostName || self.opts.controllerHostName.length <= 0) {
self.logger.error('AppDynamics agent cannot be started: controller host name is either missing or empty');
return;
}
if (!self.opts.controllerPort) {
self.logger.error('AppDynamics agent cannot be started: controller port is missing');
return;
}
if (!self.opts.applicationName || self.opts.applicationName.length <= 0) {
self.logger.error('AppDynamics agent cannot be started: application name is either missing or empty');
return;
}
if (!self.opts.tierName || self.opts.tierName.length <= 0) {
self.logger.error('AppDynamics agent cannot be started: application tier name is either missing or empty');
return;
}
if ((!self.opts.nodeName || self.opts.nodeName.length <= 0) &&
(self.reuseNode && (!self.opts.reuseNodePrefix || self.opts.reuseNodePrefix.length <= 0))) {
self.logger.error('AppDynamics agent cannot be started: node name/node name prefix is either missing or empty');
return;
}
var nodeName;
if(self.opts.nodeName && self.opts.nodeName.length) {
nodeName = self.opts.nodeName;
} else {
nodeName = self.opts.reuseNodePrefix;
}
const filePath = path.join(require.resolve('appdynamics'), '..');
process.env.APPDYNAMICS_AGENT_DIR = filePath;
self.tmpDir = Agent.computeTmpDir(self.rootTmpDir,
self.opts.controllerHostName,
self.opts.controllerPort,
self.opts.applicationName,
self.opts.tierName,
nodeName);
}
self.backendConnector.addNodeIndexToNodeName();
// Initialize logger first.
self.backendConnector.initializeLogger();
self.dumpRuntimeProperties();
self.backendConnector.createCLRDirectories();
self.setMaxListeners(15);
// Load native extention
self.loadNativeExtention();
// Initialize core modules first.
self.context.init();
self.timers.init();
self.system.init();
self.proxy.init();
self.thread.init();
// Initialize other modules.
self.backendConnector.init();
self.stringMatcher.init();
self.expressionEvaluator.init();
self.correlation.init();
self.eum.init();
// Metrics aggregator should be initialize before
// metric senders.
self.metricsManager.init();
// Initialize the rest.
self.processInfo.init();
self.processScanner.init();
self.processStats.init();
self.instanceTracker.init();
self.profiler.init();
self.customTransaction.init();
self.gcStats.init();
self.cpuProfiler.init();
self.heapProfiler.init();
if(self.opts.secureAppEnabled || process.env.APPDYNAMICS_SECURE_APP_ENABLED) {
self.secureApp.init(this);
}
// Initialize libagent
self.backendConnector.intializeAgentHelpers();
// Prepare probes.
self.loadProbes();
self.fetchMetadata(function (err, meta) {
if (err) {
self.logger.error(err);
}
try {
self.emit('session');
}
catch (err) {
self.logger.error(err);
}
var filters = {
dataFilters: opts.dataFilters || [],
urlFilters: opts.urlFilters || [],
messageFilters: opts.messageFilters || []
};
self.backendConnector.startAgent(meta, filters);
});
} catch (err) {
self.logger.error('Appdynamics agent cannot be initialized due to ' + err + '\n' + err.stack);
}
};
Agent.prototype.registerOtelProviders = function(containerId) {
var self = this;
if (self.opts.openTelemetryLogger && self.opts.openTelemetryLogger.enabled) {
var OtelLoggerProvider = require('./opentelemetry-logger');
self.OtelLoggerProvider = new OtelLoggerProvider(self.logger);
if (!self.OtelLoggerProvider.register(self.opts, containerId)) {
self.logger.error('OtelLoggerProvider registration has failed, feature disabled');
return;
}
self.OtelLogger = self.OtelLoggerProvider;
}
};
Agent.prototype.initializeOpts = function (opts) {
var self = this;
opts = opts || {};
self.opts = opts;
if (self.opts.controllerHostName === undefined) {
self.opts.controllerHostName = process.env.APPDYNAMICS_CONTROLLER_HOST_NAME;
}
if (self.opts.controllerPort === undefined) {
self.opts.controllerPort = process.env.APPDYNAMICS_CONTROLLER_PORT;
}
if (self.opts.controllerSslEnabled === undefined) {
self.opts.controllerSslEnabled = process.env.APPDYNAMICS_CONTROLLER_SSL_ENABLED;
}
if (self.opts.accountName === undefined) {
self.opts.accountName = process.env.APPDYNAMICS_AGENT_ACCOUNT_NAME;
}
if (self.opts.accountAccessKey === undefined) {
self.opts.accountAccessKey = process.env.APPDYNAMICS_AGENT_ACCOUNT_ACCESS_KEY;
}
if (self.opts.applicationName === undefined) {
self.opts.applicationName = process.env.APPDYNAMICS_AGENT_APPLICATION_NAME;
}
if (self.opts.tierName === undefined) {
self.opts.tierName = process.env.APPDYNAMICS_AGENT_TIER_NAME;
}
if (self.opts.nodeName === undefined) {
self.opts.nodeName = process.env.APPDYNAMICS_AGENT_NODE_NAME;
}
if (self.opts.reuseNode === undefined) {
self.opts.reuseNode = process.env.APPDYNAMICS_AGENT_REUSE_NODE_NAME;
}
if (self.opts.reuseNodePrefix === undefined) {
self.opts.reuseNodePrefix = process.env.APPDYNAMICS_AGENT_REUSE_NODE_NAME_PREFIX;
}
if (self.opts.analyticsMaxSegmentSizeInBytes === undefined && process.env.APPDYNAMICS_ANALYTICS_MAX_SEGMENT_SIZE) {
self.opts.analyticsMaxSegmentSizeInBytes = parseInt(process.env.APPDYNAMICS_ANALYTICS_MAX_SEGMENT_SIZE, 10);
}
if (self.opts.analyticsMaxSegmentsPerRequest === undefined && process.env.APPDYNAMICS_ANALYTICS_MAX_SEGMENTS_PER_REQUEST) {
self.opts.analyticsMaxSegmentsPerRequest = parseInt(process.env.APPDYNAMICS_ANALYTICS_MAX_SEGMENTS_PER_REQUEST, 10);
}
if (self.opts.analyticsMaxMessageSizeInBytes === undefined && process.env.APPDYNAMICS_ANALYTICS_MAX_MESSAGE_SIZE) {
self.opts.analyticsMaxMessageSizeInBytes = parseInt(process.env.APPDYNAMICS_ANALYTICS_MAX_MESSAGE_SIZE, 10);
}
};
Agent.prototype.fetchMetadata = function (cb) {
var self = this, key;
function add(key, value) {
self.meta.push({ name: key, value: value });
}
var processInfo = self.processInfo.fetchInfo();
for (key in processInfo) {
add(key, processInfo[key]);
}
if (!this.nsolidEnabled) {
return cb(null, self.meta);
}
this.nsolid.info(function (err, results) {
var label;
self.nsolidMetaCollected = true;
if (err) return cb(err);
var labels = {
app: 'App Name',
appVersion: 'App Version',
processStart: 'Process Start'
};
for (key in labels) if (labels.hasOwnProperty(key)) {
add('N|Solid ' + labels[key], results[key]);
}
for (key in results.versions.nsolid_lib) if (results.versions.nsolid_lib.hasOwnProperty(key)) {
label = 'N|Solid Module Versions: ' + key;
add(label, results.versions.nsolid_lib[key]);
}
add('N|Solid Tags', results.tags.join(', '));
cb(null, self.meta);
});
};
Agent.prototype.profile = Agent.prototype.init;
/* istanbul ignore next -- not unit testable */
Agent.prototype.loadProbes = function () {
var self = this;
// Dynamic probes.
var probeCons = [];
probeCons.push(require('../probes/cluster-probe').ClusterProbe);
probeCons.push(require('../probes/disk-probe').DiskProbe);
probeCons.push(require('../probes/http-probe').HttpProbe);
probeCons.push(require('../probes/http2-probe').Http2Probe);
probeCons.push(require('../probes/grpc-probe').GrpcProbe);
probeCons.push(require('../probes/memcached-probe').MemcachedProbe);
probeCons.push(require('../probes/mongodb-probe').MongodbProbe);
probeCons.push(require('../probes/mssql-probe').MssqlProbe);
probeCons.push(require('../probes/mysql-probe').MysqlProbe);
probeCons.push(require('../probes/net-probe').NetProbe);
probeCons.push(require('../probes/nsolid-probe').NSolidProbe);
probeCons.push(require('../probes/pg-probe').PgProbe);
probeCons.push(require('../probes/redis-probe').RedisProbe);
probeCons.push(require('../probes/ioredis-probe').IoredisProbe);
probeCons.push(require('../probes/socket.io-probe').SocketioProbe);
probeCons.push(require('../probes/dynamodb-probe').DynamoDbProbe);
probeCons.push(require('../probes/couchbase-probe').CouchBaseProbe);
probeCons.push(require('../probes/cassandra-probe').CassandraProbe);
probeCons.push(require('../probes/express-probe').ExpressProbe);
probeCons.push(require('../probes/rabbitmq-probe').RabbitMQProbe);
probeCons.push(require('../probes/winston-probe').WinstonProbe);
probeCons.push(require('../probes/tedious-probe').TediousProbe);
var packageProbes = {};
var relativePackageProbes = {};
probeCons.forEach(function (ProbeCon) {
var probe = new ProbeCon(self);
probe.packages.forEach(function (pkg) {
packageProbes[pkg] = probe;
});
if(probe.relativePackages) {
probe.relativePackages.forEach(function (pkg) {
relativePackageProbes[pkg] = probe;
});
}
});
// on demand probe attaching
self.proxy.after(module.__proto__, 'require', function (obj, args, ret) {
var probe = packageProbes[args[0]];
if (probe) {
self.logger.debug('loaded ' + args[0] + ' module on demand');
return probe.attach(ret, args[0]);
} else {
// Uncomment to diagnose certain issues with instrumentation
// self.logger.trace('missing probe for ' + args[0] + ' module');
}
var rProbe = relativePackageProbes[args[0]];
if (rProbe) {
self.logger.debug('loaded ' + args[0] + ' module on demand');
return rProbe.attachRelative(obj, args, ret);
} else {
// Uncomment to diagnose certain issues with instrumentation
// self.logger.trace('missing probe for ' + args[0] + ' module');
}
});
// Trying to preattaching probes.
for (var name in packageProbes) {
try {
require(name);
self.logger.debug('found ' + name + ' module');
} catch (err) {
self.logger.trace('unable to load ' + name + ' module');
}
}
// Explicit probes.
var ProcessProbe = require('../probes/process-probe').ProcessProbe;
new ProcessProbe(self).attach(process);
var GlobalProbe = require('../probes/global-probe').GlobalProbe;
new GlobalProbe(self).attach(global);
};
/* istanbul ignore next */
Agent.prototype.loadNativeExtention = function () {
var self = this;
if (!self.appdNative)
self.appdNative = appDNativeLoader.load(this);
};
Agent.prototype.getNextId = function () {
return this.nextId++;
};
Agent.prototype.destroy = function () {
try {
this.emit('destroy');
}
catch (err) {
this.logger.error(err);
}
this.removeAllListeners();
};
Agent.prototype.getTransaction = function (req) {
var threadId, txn;
if (!this.initialized) return;
threadId = req && req.__appdThreadId;
txn = threadId && this.profiler.getTransaction(threadId);
return txn && !txn.ignore && this.customTransaction.join(req, txn);
};
Agent.prototype.startTransaction = function (transactionInfo, cb) {
if (!this.initialized) return;
return this.customTransaction.start(transactionInfo, cb);
};
Agent.prototype.parseCorrelationInfo = function (source) {
var self = this;
if (typeof (source) === 'object') {
source = source.headers && source.headers[self.correlation.HEADER_NAME];
}
return {
businessTransactionName: 'NodeJS API Business Transaction',
headers: {
'singularityheader': source
}
};
};
Agent.prototype.createCustomMetric = function (name, unit, op, customRollup, holeHandling) {
if (!this.initialized) {
this.logger.warn('Can\'t create a metric before agent initialization');
return;
}
name = name ? 'Custom Metrics|' + name : 'Custom Metrics';
return this.metricsManager.createMetric({
path: name,
unit: unit,
op: op,
clusterRollup: customRollup,
holeHandling: holeHandling
}, true);
};
Agent.prototype.addAnalyticsData = function (key, value) {
var currentTxnTp = this.getCurrentTransactionTimePromise();
if (!currentTxnTp) {
this.logger.warn('Business transaction associated with the request cannot be found');
return;
}
currentTxnTp.addAnalyticsData(key, value);
};
Agent.prototype.addSnapshotData = function (key, value) {
var currentTxnTp = this.getCurrentTransactionTimePromise();
if (!currentTxnTp) {
this.logger.warn('Business transaction associated with the request cannot be found');
return;
}
currentTxnTp.addSnapshotData(key, value);
};
Agent.prototype.markError = function (err, statusCode) {
var currentTxnTp = this.getCurrentTransactionTimePromise();
if (!currentTxnTp) {
this.logger.warn('Business transaction associated with the request cannot be found');
return;
}
currentTxnTp.markError(err, statusCode);
};
Agent.prototype.getCurrentTransactionTimePromise = function () {
var threadId = this.thread.current();
var txn = threadId && this.profiler.getTransaction(threadId);
return txn && !txn.ignore && this.customTransaction.join(null, txn);
};
Agent.prototype.dumpRuntimeProperties = function () {
var self = this;
self.logger.env('NodeJS ' + process.arch + ' runtime properties for PID ' + process.pid);
self.logger.env('Process command line [' + process.argv + ']');
self.logger.env('Full node executable path: ' + process.execPath);
var key;
for (key in process.versions) {
self.logger.env('version: ' + key + ' = ' + process.versions[key]);
}
for (key in process.features) {
self.logger.env('feature: ' + key + ' = ' + process.features[key]);
}
for (key in process.release) {
self.logger.env('release information: ' + key + ' = ' + process.release[key]);
}
for (key in process.config.target_defaults) {
self.logger.env('configuration target defaults: ' + key + ' = ' + process.config.target_defaults[key]);
}
for (key in process.config.variables) {
self.logger.env('configuration variables: ' + key + ' = ' + process.config.variables[key]);
}
};
var AppDynamics = function () {
var self = this;
var agent = new Agent();
['profile',
'destroy',
'getTransaction',
'startTransaction',
'parseCorrelationInfo',
'createCustomMetric',
'addAnalyticsData',
'addSnapshotData',
'markError'
].forEach(function (meth) {
self[meth] = function () {
return agent[meth].apply(agent, arguments);
};
});
['on',
'addListener',
'pause',
'resume'
].forEach(function (meth) {
self[meth] = function () {
// deprecated
};
});
// Here so jasmine tests can access
// agent functions.
self.__AppDynamics = AppDynamics;
self.__agent = agent;
};
// Here so jasmine tests can access
// agent functions.
AppDynamics.Agent = Agent;
exports = module.exports = new AppDynamics();
// export for testing
exports._setMetadataFile = setMetadataFile;