elastic-apm-node
Version:
The official Elastic APM agent for Node.js
1,006 lines (964 loc) • 27.5 kB
JavaScript
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
;
const fs = require('fs');
const os = require('os');
const path = require('path');
const { URL } = require('url');
const AGENT_VERSION = require('../../package.json').version;
const { REDACTED } = require('../constants');
/**
* @typedef {Object} OptionDefinition
* @property {string} name the name of the configuration option
* @property {keyof TypeNormalizers} configType the type of the configuration option
* @property {any} defaultValue the default value of the property or undefined
* @property {any} [environmentValue] defined if value is provided via environment var
* @property {any} [startValue] defined if value is provided via `Agent.start()` API
* @property {any} [fileValue] defined if value is provided via config file
* @property {'environment' | 'start' | 'file'} [source] defined value comes from any source keeping its priorities (env, star, file, default)
* @property {string} [envVar] the name of the environment varaiable associated with the option
* @property {string} [envDeprecatedVar] the name of the deprecated environment varaiable associated with the option
* @property {string} [centralConfigName] name of option in central configuration
* @property {string} [crossAgentName] name of the same option in other agents
* @property {(v: any) => any} [redactFn] if passed the value will be redacted with it on serialization (preamble...)
*/
// TODO: options with `crossAgentName` property
// check for more cross agent config option names in
// https://docs.google.com/spreadsheets/d/1JJjZotapacA3FkHc2sv_0wiChILi3uKnkwLTjtBmxwU/edit#gid=0
/**
* @type Array<OptionDefinition>
*/
const CONFIG_SCHEMA = [
{
name: 'abortedErrorThreshold',
configType: 'durationSeconds',
defaultValue: '25s',
envVar: 'ELASTIC_APM_ABORTED_ERROR_THRESHOLD',
},
{
name: 'active',
configType: 'boolean',
defaultValue: true,
envVar: 'ELASTIC_APM_ACTIVE',
},
{
name: 'addPatch',
configType: 'stringKeyValuePairs',
defaultValue: undefined,
envVar: 'ELASTIC_APM_ADD_PATCH',
},
{
name: 'apiRequestSize',
configType: 'byte',
defaultValue: '768kb',
envVar: 'ELASTIC_APM_API_REQUEST_SIZE',
},
{
name: 'apiRequestTime',
configType: 'durationSeconds',
defaultValue: '10s',
envVar: 'ELASTIC_APM_API_REQUEST_TIME',
},
{
name: 'breakdownMetrics',
configType: 'boolean',
defaultValue: true,
envVar: 'ELASTIC_APM_BREAKDOWN_METRICS',
},
{
name: 'captureBody',
configType: 'select(off,all,errors,transactions)',
defaultValue: 'off',
envVar: 'ELASTIC_APM_CAPTURE_BODY',
centralConfigName: 'capture_body',
crossAgentName: 'capture_body',
},
{
name: 'captureErrorLogStackTraces',
configType: 'select(messages,always)',
defaultValue: 'messages',
envVar: 'ELASTIC_APM_CAPTURE_ERROR_LOG_STACK_TRACES',
},
{
name: 'captureExceptions',
configType: 'boolean',
defaultValue: true,
envVar: 'ELASTIC_APM_CAPTURE_EXCEPTIONS',
},
{
name: 'captureHeaders',
configType: 'boolean',
defaultValue: true,
envVar: 'ELASTIC_APM_CAPTURE_HEADERS',
},
{
name: 'centralConfig',
configType: 'boolean',
defaultValue: true,
envVar: 'ELASTIC_APM_CENTRAL_CONFIG',
},
{
name: 'cloudProvider',
configType: 'select(auto,gcp,azure,aws,none)',
defaultValue: 'auto',
envVar: 'ELASTIC_APM_CLOUD_PROVIDER',
},
{
name: 'containerId',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_CONTAINER_ID',
},
{
name: 'contextPropagationOnly',
configType: 'boolean',
defaultValue: false,
envVar: 'ELASTIC_APM_CONTEXT_PROPAGATION_ONLY',
},
{
name: 'customMetricsHistogramBoundaries',
configType: 'sortedNumberArray',
// Exponential powers-of-2 bucket boundaries, rounded to 6 significant figures.
// 2**N for N in [-8, -7.5, -7, ..., 16, 16.5, 17]
// https://github.com/elastic/apm/blob/main/specs/agents/metrics-otel.md#histogram-aggregation
defaultValue: [
0.00390625, 0.00552427, 0.0078125, 0.0110485, 0.015625, 0.0220971,
0.03125, 0.0441942, 0.0625, 0.0883883, 0.125, 0.176777, 0.25, 0.353553,
0.5, 0.707107, 1, 1.41421, 2, 2.82843, 4, 5.65685, 8, 11.3137, 16,
22.6274, 32, 45.2548, 64, 90.5097, 128, 181.019, 256, 362.039, 512,
724.077, 1024, 1448.15, 2048, 2896.31, 4096, 5792.62, 8192, 11585.2,
16384, 23170.5, 32768, 46341, 65536, 92681.9, 131072,
],
envVar: 'ELASTIC_APM_CUSTOM_METRICS_HISTOGRAM_BOUNDARIES',
},
{
name: 'disableInstrumentations',
configType: 'stringArray',
defaultValue: [],
envVar: 'ELASTIC_APM_DISABLE_INSTRUMENTATIONS',
},
{
name: 'disableMetrics',
configType: 'stringArray',
defaultValue: [],
envVar: 'ELASTIC_APM_DISABLE_METRICS',
},
{
name: 'disableMetricsRegExp',
configType: 'wildcardArray',
defaultValue: [],
deps: ['disableMetrics'],
},
{
name: 'disableSend',
configType: 'boolean',
defaultValue: false,
envVar: 'ELASTIC_APM_DISABLE_SEND',
},
{
name: 'elasticsearchCaptureBodyUrls',
configType: 'stringArray',
defaultValue: [
'*/_search',
'*/_search/template',
'*/_msearch',
'*/_msearch/template',
'*/_async_search',
'*/_count',
'*/_sql',
'*/_eql/search',
],
envVar: 'ELASTIC_APM_ELASTICSEARCH_CAPTURE_BODY_URLS',
},
{
name: 'elasticsearchCaptureBodyUrlsRegExp',
configType: 'wildcardArray',
defaultValue: [],
deps: ['elasticsearchCaptureBodyUrls'],
},
{
name: 'environment',
configType: 'string',
defaultValue: process.env.NODE_ENV || 'development',
envVar: 'ELASTIC_APM_ENVIRONMENT',
},
{
name: 'errorOnAbortedRequests',
configType: 'boolean',
defaultValue: false,
envVar: 'ELASTIC_APM_ERROR_ON_ABORTED_REQUESTS',
},
{
name: 'exitSpanMinDuration',
configType: 'durationMilliseconds',
defaultValue: '0ms',
envVar: 'ELASTIC_APM_EXIT_SPAN_MIN_DURATION',
centralConfigName: 'exit_span_min_duration',
crossAgentName: 'exit_span_min_duration',
},
{
name: 'globalLabels',
configType: 'stringKeyValuePairs',
defaultValue: undefined,
envVar: 'ELASTIC_APM_GLOBAL_LABELS',
},
{
name: 'ignoreMessageQueues',
configType: 'stringArray',
defaultValue: [],
envVar: 'ELASTIC_APM_IGNORE_MESSAGE_QUEUES',
centralConfigName: 'ignore_message_queues',
crossAgentName: 'ignore_message_queues',
},
{
name: 'ignoreMessageQueuesRegExp',
configType: 'wildcardArray',
defaultValue: [],
deps: ['ignoreMessageQueues'],
},
{
name: 'instrument',
configType: 'boolean',
defaultValue: true,
envVar: 'ELASTIC_APM_INSTRUMENT',
},
{
name: 'instrumentIncomingHTTPRequests',
configType: 'boolean',
defaultValue: true,
envVar: 'ELASTIC_APM_INSTRUMENT_INCOMING_HTTP_REQUESTS',
},
{
name: 'kubernetesNamespace',
configType: 'string',
defaultValue: undefined,
envVar: 'KUBERNETES_NAMESPACE',
},
{
name: 'kubernetesNodeName',
configType: 'string',
defaultValue: undefined,
envVar: 'KUBERNETES_NODE_NAME',
},
{
name: 'kubernetesPodName',
configType: 'string',
defaultValue: undefined,
envVar: 'KUBERNETES_POD_NAME',
},
{
name: 'kubernetesPodUID',
configType: 'string',
defaultValue: undefined,
envVar: 'KUBERNETES_POD_UID',
},
{
name: 'logLevel',
configType: 'select(debug,info,warning,error,critical,off,trace)',
defaultValue: 'info',
envVar: 'ELASTIC_APM_LOG_LEVEL',
centralConfigName: 'log_level',
crossAgentName: 'log_level',
},
{
name: 'longFieldMaxLength',
configType: 'number',
defaultValue: 10000,
envVar: 'ELASTIC_APM_LONG_FIELD_MAX_LENGTH',
},
{
name: 'maxQueueSize',
configType: 'number',
// Rough equivalent of the Java Agent's max_queue_size:
// https://www.elastic.co/guide/en/apm/agent/java/current/config-reporter.html#config-max-queue-size
defaultValue: 1024,
envVar: 'ELASTIC_APM_MAX_QUEUE_SIZE',
},
{
name: 'metricsInterval',
configType: 'durationSeconds',
defaultValue: '30s',
envVar: 'ELASTIC_APM_METRICS_INTERVAL',
},
{
name: 'metricsLimit',
configType: 'number',
defaultValue: 1000,
envVar: 'ELASTIC_APM_METRICS_LIMIT',
},
{
name: 'opentelemetryBridgeEnabled',
configType: 'boolean',
defaultValue: false,
envVar: 'ELASTIC_APM_OPENTELEMETRY_BRIDGE_ENABLED',
},
{
name: 'sanitizeFieldNames',
configType: 'stringArray',
// These patterns are specified in the shared APM specs:
// https://github.com/elastic/apm/blob/main/specs/agents/sanitization.md
defaultValue: [
'password',
'passwd',
'pwd',
'secret',
'*key',
'*token*',
'*session*',
'*credit*',
'*card*',
'*auth*',
'set-cookie',
'*principal*',
// These are default patterns only in the Node.js APM agent, historically
// from when the "is-secret" dependency was used.
'pw',
'pass',
'connect.sid',
],
envVar: 'ELASTIC_APM_SANITIZE_FIELD_NAMES',
centralConfigName: 'sanitize_field_names',
crossAgentName: 'sanitize_field_names',
},
{
name: 'sanitizeFieldNamesRegExp',
configType: 'wildcardArray',
defaultValue: [],
deps: ['sanitizeFieldNames'],
},
{
name: 'serviceNodeName',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_SERVICE_NODE_NAME',
},
{
name: 'serverTimeout',
configType: 'durationSeconds',
defaultValue: '30s',
envVar: 'ELASTIC_APM_SERVER_TIMEOUT',
},
{
name: 'serverUrl',
configType: 'url',
defaultValue: 'http://127.0.0.1:8200',
envVar: 'ELASTIC_APM_SERVER_URL',
crossAgentName: 'server_url',
redactFn: sanitizeUrl,
},
{
name: 'sourceLinesErrorAppFrames',
configType: 'number',
defaultValue: 5,
envVar: 'ELASTIC_APM_SOURCE_LINES_ERROR_APP_FRAMES',
},
{
name: 'sourceLinesErrorLibraryFrames',
configType: 'number',
defaultValue: 5,
envVar: 'ELASTIC_APM_SOURCE_LINES_ERROR_LIBRARY_FRAMES',
},
{
name: 'sourceLinesSpanAppFrames',
configType: 'number',
defaultValue: 0,
envVar: 'ELASTIC_APM_SOURCE_LINES_SPAN_APP_FRAMES',
},
{
name: 'sourceLinesSpanLibraryFrames',
configType: 'number',
defaultValue: 0,
envVar: 'ELASTIC_APM_SOURCE_LINES_SPAN_LIBRARY_FRAMES',
},
{
name: 'spanCompressionEnabled',
configType: 'boolean',
defaultValue: true,
envVar: 'ELASTIC_APM_SPAN_COMPRESSION_ENABLED',
},
{
name: 'spanCompressionExactMatchMaxDuration',
configType: 'durationCompression',
defaultValue: '50ms',
envVar: 'ELASTIC_APM_SPAN_COMPRESSION_EXACT_MATCH_MAX_DURATION',
},
{
name: 'spanCompressionSameKindMaxDuration',
configType: 'durationCompression',
defaultValue: '0ms',
envVar: 'ELASTIC_APM_SPAN_COMPRESSION_SAME_KIND_MAX_DURATION',
},
{
name: 'stackTraceLimit',
configType: 'number',
defaultValue: 50,
envVar: 'ELASTIC_APM_STACK_TRACE_LIMIT',
},
{
name: 'traceContinuationStrategy',
configType: 'select(continue,restart,external)',
defaultValue: 'continue',
envVar: 'ELASTIC_APM_TRACE_CONTINUATION_STRATEGY',
centralConfigName: 'trace_continuation_strategy',
crossAgentName: 'trace_continuation_strategy',
},
{
name: 'transactionIgnoreUrls',
configType: 'stringArray',
defaultValue: [],
envVar: 'ELASTIC_APM_TRANSACTION_IGNORE_URLS',
centralConfigName: 'transaction_ignore_urls',
crossAgentName: 'transaction_ignore_urls',
},
{
name: 'transactionIgnoreUrlRegExp',
configType: 'wildcardArray',
defaultValue: [],
deps: ['transactionIgnoreUrls'],
},
{
name: 'transactionMaxSpans',
configType: 'numberInfinity',
defaultValue: 500,
envVar: 'ELASTIC_APM_TRANSACTION_MAX_SPANS',
centralConfigName: 'transaction_max_spans',
crossAgentName: 'transaction_max_spans',
},
{
name: 'transactionSampleRate',
configType: 'sampleRate',
defaultValue: 1,
envVar: 'ELASTIC_APM_TRANSACTION_SAMPLE_RATE',
centralConfigName: 'transaction_sample_rate',
crossAgentName: 'transaction_sample_rate',
},
{
name: 'useElasticTraceparentHeader',
configType: 'boolean',
defaultValue: false,
envVar: 'ELASTIC_APM_USE_ELASTIC_TRACEPARENT_HEADER',
},
{
name: 'usePathAsTransactionName',
configType: 'boolean',
defaultValue: false,
envVar: 'ELASTIC_APM_USE_PATH_AS_TRANSACTION_NAME',
},
{
name: 'verifyServerCert',
configType: 'boolean',
defaultValue: true,
envVar: 'ELASTIC_APM_VERIFY_SERVER_CERT',
},
{
name: 'errorMessageMaxLength',
configType: 'byte',
defaultValue: undefined,
envVar: 'ELASTIC_APM_ERROR_MESSAGE_MAX_LENGTH',
deprecated: 'Deprecated in: v3.21.0, use longFieldMaxLength',
},
{
name: 'spanStackTraceMinDuration',
configType: 'durationMillisecondsNegative',
// 'spanStackTraceMinDuration' is explicitly *not* defined in DEFAULTS
// because normalizeSpanStackTraceMinDuration() needs to know if a value
// was provided by the user.
defaultValue: undefined,
envVar: 'ELASTIC_APM_SPAN_STACK_TRACE_MIN_DURATION',
centralConfigName: 'span_stack_trace_min_duration',
crossAgentName: 'span_stack_trace_min_duration',
},
{
name: 'apiKey',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_API_KEY',
crossAgentName: 'api_key',
redactFn: () => REDACTED,
},
{
name: 'captureSpanStackTraces',
configType: 'boolean',
defaultValue: undefined,
envVar: 'ELASTIC_APM_CAPTURE_SPAN_STACK_TRACES',
deprecated: 'Deprecated in: v3.30.0, use spanStackTraceMinDuration',
},
{
name: 'contextManager',
configType: 'select(asynclocalstorage,asynchooks)',
// 'contextManager' is explicitly *not* defined in DEFAULTS because
// normalizeContextManager() needs to know if a value was provided by the
// user.
defaultValue: undefined,
envVar: 'ELASTIC_APM_CONTEXT_MANAGER',
},
{
name: 'frameworkName',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_FRAMEWORK_NAME',
},
{
name: 'frameworkVersion',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_FRAMEWORK_VERSION',
},
{
name: 'hostname',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_HOSTNAME',
},
{
name: 'payloadLogFile',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_PAYLOAD_LOG_FILE',
},
{
name: 'serverCaCertFile',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_SERVER_CA_CERT_FILE',
},
{
name: 'secretToken',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_SECRET_TOKEN',
crossAgentName: 'secret_token',
redactFn: () => REDACTED,
},
{
name: 'serviceName',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_SERVICE_NAME',
crossAgentName: 'service_name',
},
{
name: 'serviceVersion',
configType: 'string',
defaultValue: undefined,
envVar: 'ELASTIC_APM_SERVICE_VERSION',
crossAgentName: 'service_version',
},
{
name: 'spanFramesMinDuration',
configType: 'durationSecondsNegative',
defaultValue: undefined,
envVar: 'ELASTIC_APM_SPAN_FRAMES_MIN_DURATION',
deprecated: 'Deprecated in: v3.30.0, use spanStackTraceMinDuration',
},
{ name: 'ignoreUrls', configType: 'stringArray', defaultValue: undefined },
{
name: 'ignoreUrlRegExp',
configType: 'wildcardArray',
defaultValue: [],
deps: ['ignoreUrls'],
},
{
name: 'ignoreUrlStr',
configType: 'wildcardArray',
defaultValue: [],
deps: ['ignoreUrls'],
},
{
name: 'ignoreUserAgents',
configType: 'stringArray',
defaultValue: undefined,
},
{
name: 'ignoreUserAgentRegExp',
configType: 'wildcardArray',
defaultValue: [],
deps: ['ignoreUserAgents'],
},
{
name: 'ignoreUserAgentStr',
configType: 'wildcardArray',
defaultValue: [],
deps: ['ignoreUserAgents'],
},
{
name: 'apmClientHeaders',
configType: 'stringKeyValuePairs',
defaultValue: undefined,
envVar: 'ELASTIC_APM_APM_CLIENT_HEADERS',
},
// Special options that
// - may afect the whole config
// - change the behavior of thelogger
// - are for testing or internal use
{
name: 'configFile',
configType: 'string',
defaultValue: 'elastic-apm-node.js',
envVar: 'ELASTIC_APM_CONFIG_FILE',
},
{
name: 'logger',
configType: 'logger',
defaultValue: undefined,
envVar: 'ELASTIC_APM_LOGGER',
},
{
name: 'transport',
configType: 'function',
defaultValue: undefined,
internal: true,
},
];
/**
* Retuns an object with th default values for all config options
* @returns {Record<string,any>}
*/
function getDefaultOptions() {
return CONFIG_SCHEMA.reduce((acc, def) => {
if (typeof def.defaultValue !== 'undefined') {
acc[def.name] = def.defaultValue;
}
return acc;
}, {});
}
/**
* Retuns an object with the values of config options defined set
* in the environment
* @returns {Record<string,any>}
*/
function getEnvironmentOptions() {
return CONFIG_SCHEMA.reduce((acc, def) => {
if ('environmentValue' in def) {
acc[def.name] = def.environmentValue;
}
return acc;
}, {});
}
/**
* Retuns an object with the values of config options defined set
* in the environment
* @returns {Record<string,any>}
*/
function getFileOptions() {
return CONFIG_SCHEMA.reduce((acc, def) => {
if ('fileValue' in def) {
acc[def.name] = def.fileValue;
}
return acc;
}, {});
}
/**
* Support function to read environment after the module is loaded. This is needed to
* perform some tests that modify `process.env`
* - ./test/config.tes.js
* - ./test/logging-preamble.test.js
*
* TODO: remove this function when test are refactored using the good `runTestFixture` method
*/
function readEnvOptions() {
CONFIG_SCHEMA.forEach((def) => {
if (def.envVar && process.env[def.envVar]) {
def.environmentValue = process.env[def.envVar];
def.source = 'environment';
} else if (def.envVar) {
// TODO: this is necessary since config.test.js sets different vars during tests
delete def.environmentValue;
delete def.source;
}
});
}
/**
* Tries to load a configuration file for the given path or from the ENV vars, if both undefined
* it uses the default config filename `elastic-apm-node.js`. If the config file is not present or
* there is something wrong in the file it will return no options
* @param {string} [confPath] the path to the file
* @returns {Record<string,any> | null}
*/
function readFileOptions(confPath) {
const configFileDef = CONFIG_SCHEMA.find((def) => def.name === 'configFile');
const defaultValue = configFileDef.defaultValue;
const envValue = process.env[configFileDef.envVar];
const filePath = path.resolve(confPath || envValue || defaultValue);
if (fs.existsSync(filePath)) {
try {
return require(filePath);
} catch (err) {
console.error(
"Elastic APM initialization error: Can't read config file %s",
filePath,
);
console.error(err.stack);
}
}
return null;
}
/**
* Sets into the schema the value given at agent start
*
* @param {Record<string,any>} options
*/
function setStartOptions(options) {
if (!options) {
return;
}
const fileOpts = options.configFile && readFileOptions(options.configFile);
CONFIG_SCHEMA.forEach((def) => {
const source = def.source || 'default';
if (def.name in options) {
def.startValue = options[def.name];
if (source !== 'environment') {
def.source = 'start';
}
} else if (fileOpts && def.name in fileOpts) {
// console.log('setting from file', def.name, fileOpts[def.name], source);
def.fileValue = fileOpts[def.name];
if (source === 'default') {
def.source = 'file';
}
console.log(def);
}
});
}
/**
* Collects information about the agent, environment and configuration options that are
* sets from a source (env vars, start options or config file). For each config option it
* collects
* - source: where te value came from (environment, start, file)
* - sourceValue: the raw value provided im the source
* - normalizedValue: the value normalized by the configuration which is used in the agent
* - file: if source === 'file' this proprty is defined with the path of the config file
*
* @param {Object} config The configuration object with normalized options
* @returns {Object} Object with agent, environment, and configuration data.
*/
function getPreambleData(config) {
const configFileDef = CONFIG_SCHEMA.find((def) => def.name === 'configFile');
const configFilePath = path.resolve(
configFileDef.environmentValue ||
configFileDef.startValue ||
configFileDef.defaultValue,
);
const result = {
agentVersion: AGENT_VERSION,
env: {
pid: process.pid,
proctitle: process.title,
// For darwin: https://en.wikipedia.org/wiki/Darwin_%28operating_system%29#Release_history
os: `${os.platform()} ${os.release()}`,
arch: os.arch(),
host: os.hostname(),
timezone: getTimezoneUtc(),
runtime: `Node.js ${process.version}`,
},
config: {},
};
const excludedKeys = new Set(['logger', 'transport']);
const mandatoryKeys = new Set([
'serviceName',
'serviceVersion',
'serverUrl',
'logLevel',
]);
CONFIG_SCHEMA.forEach((def) => {
if (
excludedKeys.has(def.name) ||
(!def.source && !mandatoryKeys.has(def.name))
) {
return;
}
const details = {
source: def.source || 'default',
value: config[def.name],
};
const sourceVal = def[`${details.source}Value`];
if (def.crossAgentName) {
details.commonName = def.crossAgentName;
}
if (def.source === 'file') {
details.file = configFilePath;
}
if (typeof def.redactFn === 'function') {
details.value = def.redactFn(details.value);
} else if (sourceVal !== details.value) {
details.sourceValue = sourceVal;
}
result.config[def.name] = details;
});
return result;
}
/**
* Returns the current Timezone in UTC notation fomat
*
* @returns {String} ex. UTC-0600
*/
function getTimezoneUtc() {
const offsetHours = -(new Date().getTimezoneOffset() / 60);
const offsetSign = offsetHours >= 0 ? '+' : '-';
const offsetPad = Math.abs(offsetHours) < 10 ? '0' : '';
return `UTC${offsetSign}${offsetPad}${Math.abs(offsetHours * 100)}`;
}
/**
* Takes an URL string and redacts user & password info if present in the basic
* auth part
*
* @param {String} urlStr The URL to strip sensitive info
* @returns {String || null} url with basic auth REDACTED
*/
function sanitizeUrl(urlStr) {
if (!urlStr) {
return '';
}
const url = new URL(urlStr);
if (url.username) {
url.username = REDACTED;
}
if (url.password) {
url.password = REDACTED;
}
return decodeURIComponent(url.href);
}
// Fail fast if CONFIG_SCHEMA has issues
// - duplicated options
// - dependencies not defined or defined after the dependant option
// - type with no normalizer
const depSet = new Set();
const fileOpts = readFileOptions();
CONFIG_SCHEMA.forEach((def) => {
if (depSet.has(def.name)) {
throw Error(`config option ${def.name} duplicated.`);
}
if (def.deps && def.deps.some((d) => !depSet.has(d))) {
throw Error(
`config option ${def.name} dependencies (${def.deps}) need to be defined before.`,
);
}
depSet.add(def.name);
// Initalize values comming from environment and config file
if (def.envVar && process.env[def.envVar]) {
def.environmentValue = process.env[def.envVar];
def.source = 'environment';
} else if (fileOpts && typeof fileOpts[def.name] !== 'undefined') {
def.fileValue = fileOpts[def.name];
def.source = 'file';
}
});
// TODO: these constants below to be removed in the future (normalization)
function getConfigName(def) {
return def.name;
}
function optsOfTypes(types) {
if (!Array.isArray(types)) {
types = [types];
}
return CONFIG_SCHEMA.filter((def) => types.indexOf(def.configType) !== -1);
}
const BOOL_OPTS = optsOfTypes('boolean').map(getConfigName);
const NUM_OPTS = optsOfTypes(['number', 'numberInfinity']).map(getConfigName);
const BYTES_OPTS = optsOfTypes('byte').map(getConfigName);
const URL_OPTS = optsOfTypes('url').map(getConfigName);
const ARRAY_OPTS = optsOfTypes('stringArray').map(getConfigName);
const KEY_VALUE_OPTS = optsOfTypes('stringKeyValuePairs').map(getConfigName);
const MINUS_ONE_EQUAL_INFINITY = optsOfTypes(['numberInfinity']).map(
getConfigName,
);
const CENTRAL_CONFIG_OPTS = CONFIG_SCHEMA.filter(
(def) => def.centralConfigName,
).reduce((acc, def) => {
acc[def.centralConfigName] = def.name;
return acc;
}, {});
const DURATION_OPTS = [
{
name: 'abortedErrorThreshold',
defaultUnit: 's',
allowedUnits: ['ms', 's', 'm'],
allowNegative: false,
},
{
name: 'apiRequestTime',
defaultUnit: 's',
allowedUnits: ['ms', 's', 'm'],
allowNegative: false,
},
{
name: 'exitSpanMinDuration',
defaultUnit: 'ms',
allowedUnits: ['us', 'ms', 's', 'm'],
allowNegative: false,
},
{
name: 'metricsInterval',
defaultUnit: 's',
allowedUnits: ['ms', 's', 'm'],
allowNegative: false,
},
{
name: 'serverTimeout',
defaultUnit: 's',
allowedUnits: ['ms', 's', 'm'],
allowNegative: false,
},
{
name: 'spanCompressionExactMatchMaxDuration',
defaultUnit: 'ms',
allowedUnits: ['ms', 's', 'm'],
allowNegative: false,
},
{
name: 'spanCompressionSameKindMaxDuration',
defaultUnit: 'ms',
allowedUnits: ['ms', 's', 'm'],
allowNegative: false,
},
{
// Deprecated: use `spanStackTraceMinDuration`.
name: 'spanFramesMinDuration',
defaultUnit: 's',
allowedUnits: ['ms', 's', 'm'],
allowNegative: true,
},
{
name: 'spanStackTraceMinDuration',
defaultUnit: 'ms',
allowedUnits: ['ms', 's', 'm'],
allowNegative: true,
},
];
const CROSS_AGENT_CONFIG_VAR_NAME = CONFIG_SCHEMA.filter(
(def) => def.crossAgentName,
).reduce((acc, def) => {
acc[def.name] = def.crossAgentName;
return acc;
}, {});
const ENV_TABLE = CONFIG_SCHEMA.filter((def) => def.envVar).reduce(
(acc, def) => {
acc[def.name] = def.envVar;
return acc;
},
{},
);
// Exports
module.exports = {
BOOL_OPTS,
NUM_OPTS,
DURATION_OPTS,
BYTES_OPTS,
MINUS_ONE_EQUAL_INFINITY,
ARRAY_OPTS,
KEY_VALUE_OPTS,
URL_OPTS,
CROSS_AGENT_CONFIG_VAR_NAME,
CENTRAL_CONFIG_OPTS,
ENV_TABLE,
CONFIG_SCHEMA,
getDefaultOptions,
getEnvironmentOptions,
getFileOptions,
setStartOptions,
getPreambleData,
readEnvOptions,
};