UNPKG

elastic-apm-node

Version:

The official Elastic APM agent for Node.js

456 lines (391 loc) 13.1 kB
'use strict' var fs = require('fs') var path = require('path') var consoleLogLevel = require('console-log-level') var ElasticAPMHttpClient = require('elastic-apm-http-client') var readPkgUp = require('read-pkg-up') var truncate = require('unicode-byte-truncate') var entries = require('object.entries') // TODO: Remove when Node.js 6 is no longer supported var version = require('../package').version var userAgent = 'elastic-apm-node/' + version config.INTAKE_STRING_MAX_SIZE = 1024 config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER = 'never' config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES = 'messages' config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = 'always' module.exports = config var confPath = path.resolve(process.env.ELASTIC_APM_CONFIG_FILE || 'elastic-apm-node.js') if (fs.existsSync(confPath)) { try { var confFile = require(confPath) } catch (err) { console.error('Elastic APM initialization error: Can\'t read config file %s', confPath) console.error(err.stack) } } let serviceName try { serviceName = readPkgUp.sync().pkg.name } catch (err) {} var DEFAULTS = { abortedErrorThreshold: '25s', active: true, addPatch: undefined, apiRequestSize: '768kb', apiRequestTime: '10s', asyncHooks: true, captureBody: 'off', captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES, captureExceptions: true, captureHeaders: true, captureSpanStackTraces: true, containerId: undefined, disableInstrumentations: [], environment: process.env.NODE_ENV || 'development', errorMessageMaxLength: '2kb', errorOnAbortedRequests: false, filterHttpHeaders: true, globalLabels: undefined, instrument: true, kubernetesNamespace: undefined, kubernetesNodeName: undefined, kubernetesPodName: undefined, kubernetesPodUID: undefined, logLevel: 'info', metricsInterval: '30s', serverTimeout: '30s', sourceLinesErrorAppFrames: 5, sourceLinesErrorLibraryFrames: 5, sourceLinesSpanAppFrames: 0, sourceLinesSpanLibraryFrames: 0, stackTraceLimit: 50, transactionMaxSpans: 500, transactionSampleRate: 1.0, usePathAsTransactionName: false, verifyServerCert: true } var ENV_TABLE = { abortedErrorThreshold: 'ELASTIC_APM_ABORTED_ERROR_THRESHOLD', active: 'ELASTIC_APM_ACTIVE', addPatch: 'ELASTIC_APM_ADD_PATCH', apiRequestSize: 'ELASTIC_APM_API_REQUEST_SIZE', apiRequestTime: 'ELASTIC_APM_API_REQUEST_TIME', asyncHooks: 'ELASTIC_APM_ASYNC_HOOKS', captureBody: 'ELASTIC_APM_CAPTURE_BODY', captureErrorLogStackTraces: 'ELASTIC_APM_CAPTURE_ERROR_LOG_STACK_TRACES', captureExceptions: 'ELASTIC_APM_CAPTURE_EXCEPTIONS', captureHeaders: 'ELASTIC_APM_CAPTURE_HEADERS', captureSpanStackTraces: 'ELASTIC_APM_CAPTURE_SPAN_STACK_TRACES', containerId: 'ELASTIC_APM_CONTAINER_ID', disableInstrumentations: 'ELASTIC_APM_DISABLE_INSTRUMENTATIONS', environment: 'ELASTIC_APM_ENVIRONMENT', errorMessageMaxLength: 'ELASTIC_APM_ERROR_MESSAGE_MAX_LENGTH', errorOnAbortedRequests: 'ELASTIC_APM_ERROR_ON_ABORTED_REQUESTS', filterHttpHeaders: 'ELASTIC_APM_FILTER_HTTP_HEADERS', frameworkName: 'ELASTIC_APM_FRAMEWORK_NAME', frameworkVersion: 'ELASTIC_APM_FRAMEWORK_VERSION', globalLabels: 'ELASTIC_APM_GLOBAL_LABELS', hostname: 'ELASTIC_APM_HOSTNAME', instrument: 'ELASTIC_APM_INSTRUMENT', kubernetesNamespace: ['ELASTIC_APM_KUBERNETES_NAMESPACE', 'KUBERNETES_NAMESPACE'], kubernetesNodeName: ['ELASTIC_APM_KUBERNETES_NODE_NAME', 'KUBERNETES_NODE_NAME'], kubernetesPodName: ['ELASTIC_APM_KUBERNETES_POD_NAME', 'KUBERNETES_POD_NAME'], kubernetesPodUID: ['ELASTIC_APM_KUBERNETES_POD_UID', 'KUBERNETES_POD_UID'], logLevel: 'ELASTIC_APM_LOG_LEVEL', metricsInterval: 'ELASTIC_APM_METRICS_INTERVAL', payloadLogFile: 'ELASTIC_APM_PAYLOAD_LOG_FILE', secretToken: 'ELASTIC_APM_SECRET_TOKEN', serverTimeout: 'ELASTIC_APM_SERVER_TIMEOUT', serverUrl: 'ELASTIC_APM_SERVER_URL', serviceName: 'ELASTIC_APM_SERVICE_NAME', serviceVersion: 'ELASTIC_APM_SERVICE_VERSION', sourceLinesErrorAppFrames: 'ELASTIC_APM_SOURCE_LINES_ERROR_APP_FRAMES', sourceLinesErrorLibraryFrames: 'ELASTIC_APM_SOURCE_LINES_ERROR_LIBRARY_FRAMES', sourceLinesSpanAppFrames: 'ELASTIC_APM_SOURCE_LINES_SPAN_APP_FRAMES', sourceLinesSpanLibraryFrames: 'ELASTIC_APM_SOURCE_LINES_SPAN_LIBRARY_FRAMES', stackTraceLimit: 'ELASTIC_APM_STACK_TRACE_LIMIT', transactionMaxSpans: 'ELASTIC_APM_TRANSACTION_MAX_SPANS', transactionSampleRate: 'ELASTIC_APM_TRANSACTION_SAMPLE_RATE', usePathAsTransactionName: 'ELASTIC_APM_USE_PATH_AS_TRANSACTION_NAME', verifyServerCert: 'ELASTIC_APM_VERIFY_SERVER_CERT' } var BOOL_OPTS = [ 'active', 'asyncHooks', 'captureExceptions', 'captureHeaders', 'captureSpanStackTraces', 'errorOnAbortedRequests', 'filterHttpHeaders', 'instrument', 'usePathAsTransactionName', 'verifyServerCert' ] var NUM_OPTS = [ 'sourceLinesErrorAppFrames', 'sourceLinesErrorLibraryFrames', 'sourceLinesSpanAppFrames', 'sourceLinesSpanLibraryFrames', 'stackTraceLimit', 'transactionMaxSpans', 'transactionSampleRate' ] var TIME_OPTS = [ 'abortedErrorThreshold', 'apiRequestTime', 'metricsInterval', 'serverTimeout' ] var BYTES_OPTS = [ 'apiRequestSize', 'errorMessageMaxLength' ] var MINUS_ONE_EQUAL_INFINITY = [ 'transactionMaxSpans' ] var ARRAY_OPTS = [ 'disableInstrumentations' ] var KEY_VALUE_OPTS = [ 'addPatch', 'globalLabels' ] function config (opts) { opts = Object.assign( {}, DEFAULTS, // default options confFile, // options read from elastic-apm-node.js config file opts, // options passed in to agent.start() readEnv() // options read from environment variables ) // Custom logic for setting serviceName so that an empty string in the config // doesn't overwrite the serviceName read from package.json if (!opts.serviceName) opts.serviceName = serviceName // NOTE: A logger will already exists if a custom logger was given to start() if (typeof opts.logger === 'undefined') { opts.logger = consoleLogLevel({ level: opts.logLevel }) } normalizeIgnoreOptions(opts) normalizeKeyValuePairs(opts) normalizeNumbers(opts) normalizeBytes(opts) normalizeArrays(opts) normalizeTime(opts) normalizeBools(opts) truncateOptions(opts) if (typeof opts.transport !== 'function') { opts.transport = function httpTransport (conf, agent) { var transport = new ElasticAPMHttpClient({ // metadata agentName: 'nodejs', agentVersion: version, serviceName: conf.serviceName, serviceVersion: conf.serviceVersion, frameworkName: conf.frameworkName, frameworkVersion: conf.frameworkVersion, globalLabels: maybePairsToObject(conf.globalLabels), hostname: conf.hostname, environment: conf.environment, // Sanitize conf truncateKeywordsAt: config.INTAKE_STRING_MAX_SIZE, truncateErrorMessagesAt: conf.errorMessageMaxLength, // HTTP conf secretToken: conf.secretToken, userAgent: userAgent, serverUrl: conf.serverUrl, rejectUnauthorized: conf.verifyServerCert, serverTimeout: conf.serverTimeout * 1000, // Streaming conf size: conf.apiRequestSize, time: conf.apiRequestTime * 1000, // Debugging payloadLogFile: conf.payloadLogFile, // Container conf containerId: conf.containerId, kubernetesNodeName: conf.kubernetesNodeName, kubernetesNamespace: conf.kubernetesNamespace, kubernetesPodName: conf.kubernetesPodName, kubernetesPodUID: conf.kubernetesPodUID }) transport.on('error', err => { agent.logger.error('APM Server transport error:', err.stack) }) transport.on('request-error', err => { const haveAccepted = Number.isFinite(err.accepted) const haveErrors = Array.isArray(err.errors) let msg if (err.code === 404) { msg = 'APM Server responded with "404 Not Found". ' + 'This might be because you\'re running an incompatible version of the APM Server. ' + 'This agent only supports APM Server v6.5 and above. ' + 'If you\'re using an older version of the APM Server, ' + 'please downgrade this agent to version 1.x or upgrade the APM Server' } else if (err.code) { msg = `APM Server transport error (${err.code}): ${err.message}` } else { msg = `APM Server transport error: ${err.message}` } if (haveAccepted || haveErrors) { if (haveAccepted) msg += `\nAPM Server accepted ${err.accepted} events in the last request` if (haveErrors) { err.errors.forEach(error => { msg += `\nError: ${error.message}` if (error.document) msg += `\n Document: ${error.document}` }) } } else if (err.response) { msg += `\n${err.response}` } agent.logger.error(msg) }) return transport } } return opts } function readEnv () { var opts = {} Object.keys(ENV_TABLE).forEach(function (key) { var env = ENV_TABLE[key] if (!Array.isArray(env)) env = [ env ] for (let envKey of env) { if (envKey in process.env) { opts[key] = process.env[envKey] } } }) return opts } function normalizeIgnoreOptions (opts) { opts.ignoreUrlStr = [] opts.ignoreUrlRegExp = [] opts.ignoreUserAgentStr = [] opts.ignoreUserAgentRegExp = [] if (opts.ignoreUrls) { opts.ignoreUrls.forEach(function (ptn) { if (typeof ptn === 'string') opts.ignoreUrlStr.push(ptn) else opts.ignoreUrlRegExp.push(ptn) }) delete opts.ignoreUrls } if (opts.ignoreUserAgents) { opts.ignoreUserAgents.forEach(function (ptn) { if (typeof ptn === 'string') opts.ignoreUserAgentStr.push(ptn) else opts.ignoreUserAgentRegExp.push(ptn) }) delete opts.ignoreUserAgents } } function normalizeNumbers (opts) { NUM_OPTS.forEach(function (key) { if (key in opts) opts[key] = Number(opts[key]) }) MINUS_ONE_EQUAL_INFINITY.forEach(function (key) { if (opts[key] === -1) opts[key] = Infinity }) } function normalizeBytes (opts) { BYTES_OPTS.forEach(function (key) { if (key in opts) opts[key] = bytes(String(opts[key])) }) } function normalizeTime (opts) { TIME_OPTS.forEach(function (key) { if (key in opts) opts[key] = toSeconds(String(opts[key])) }) } function maybeSplit (separator) { return (value) => { return typeof value === 'string' ? value.split(separator) : value } } const maybeSplitValues = maybeSplit(',') const maybeSplitPairs = maybeSplit('=') function normalizeArrays (opts) { ARRAY_OPTS.forEach(function (key) { if (key in opts) opts[key] = maybeSplitValues(opts[key]) }) } function normalizeKeyValuePairs (opts) { KEY_VALUE_OPTS.forEach(function (key) { if (key in opts) { if (typeof opts[key] === 'object' && !Array.isArray(opts[key])) { opts[key] = entries(opts[key]) return } if (!Array.isArray(opts[key])) { opts[key] = maybeSplitValues(opts[key]) } if (Array.isArray(opts[key])) { opts[key] = opts[key].map(maybeSplitPairs) } } }) } function normalizeBools (opts) { BOOL_OPTS.forEach(function (key) { if (key in opts) opts[key] = strictBool(opts.logger, key, opts[key]) }) } function truncateOptions (opts) { if (opts.serviceVersion) opts.serviceVersion = truncate(String(opts.serviceVersion), config.INTAKE_STRING_MAX_SIZE) if (opts.hostname) opts.hostname = truncate(String(opts.hostname), config.INTAKE_STRING_MAX_SIZE) } function bytes (input) { const matches = input.match(/^(\d+)(b|kb|mb|gb)$/i) if (!matches) return Number(input) const suffix = matches[2].toLowerCase() let value = Number(matches[1]) if (!suffix || suffix === 'b') { return value } value *= 1024 if (suffix === 'kb') { return value } value *= 1024 if (suffix === 'mb') { return value } value *= 1024 if (suffix === 'gb') { return value } } function toSeconds (value) { var matches = /^(-)?(\d+)(m|ms|s)?$/.exec(value) if (!matches) return null var negate = matches[1] var amount = Number(matches[2]) if (negate) amount = -amount var scale = matches[3] if (scale === 'm') { amount *= 60 } else if (scale === 'ms') { amount /= 1000 } return amount } function strictBool (logger, key, value) { if (typeof value === 'boolean') { return value } // This will return undefined for unknown inputs, resulting in them being skipped. switch (value) { case 'false': return false case 'true': return true default: { logger.warn('unrecognized boolean value "%s" for "%s"', value, key) } } } function maybePairsToObject (pairs) { return pairs ? pairsToObject(pairs) : undefined } function pairsToObject (pairs) { return pairs.reduce((object, [key, value]) => { object[key] = value return object }, {}) }