UNPKG

@instana/core

Version:
1,186 lines (1,052 loc) 44.3 kB
/* * (c) Copyright IBM Corp. 2021 * (c) Copyright Instana Inc. and contributors 2019 */ 'use strict'; const configNormalizers = require('./configNormalizers'); const configValidators = require('./configValidators'); const deepMerge = require('../util/deepMerge'); const { DEFAULT_STACK_TRACE_LENGTH, DEFAULT_STACK_TRACE_MODE, CONFIG_SOURCES } = require('../util/constants'); const { validateStackTraceMode, validateStackTraceLength } = require('./configValidators/stackTraceValidation'); const util = require('./util'); const validate = require('./validator'); // @typedef {{ [x: string]: any }} configMeta /** @type {configMeta} */ const configMeta = {}; const configStore = { /** * @param {string} configPath * @param {{ source: number }} obj */ set(configPath, obj) { configMeta[configPath] = obj; }, /** * @param {string} configPath - The config path * @returns {{ source: number } | undefined} */ get(configPath) { return configMeta[configPath]; }, clear() { Object.keys(configMeta).forEach(key => delete configMeta[key]); } }; /** * @type {InstanaConfig} * * NOTE: currentConfig is a reference to the config object returned by normalize(). * This variable exists to allow dynamic config updates via the update() function without * requiring the config object to be passed as a parameter. * * TODO: This can be removed in the future when we implement config.get()/config.set() * methods. The values will be kept in the configStore instance. */ let currentConfig; /** * @typedef {Object} InstanaTracingOption * @property {boolean} [enabled] * @property {boolean} [useOpentelemetry] * @property {boolean} [automaticTracingEnabled] * @property {boolean} [activateImmediately] * @property {number} [forceTransmissionStartingAt] * @property {number} [initialTransmissionDelay] * @property {number} [maxBufferedSpans] * @property {number} [transmissionDelay] * @property {string} [stackTrace] * @property {number} [stackTraceLength] * @property {HTTPTracingOptions} [http] * @property {import('../config/types').Disable} [disable] * @property {boolean} [spanBatchingEnabled] * @property {boolean} [disableW3cTraceCorrelation] * @property {KafkaTracingOptions} [kafka] * @property {boolean} [allowRootExitSpan] * @property {import('../config/types').IgnoreEndpoints} [ignoreEndpoints] * @property {boolean} [ignoreEndpointsDisableSuppression] * @property {boolean} [disableEOLEvents] * @property {globalStackTraceConfig} [global] * @property {otlpExporterOptions} [otlp] */ /** * @typedef {Object} HTTPTracingOptions * @property {Array<string>} [extraHttpHeadersToCapture] */ /** * @typedef {Object} KafkaTracingOptions * @property {boolean} [traceCorrelation] */ /** * @typedef {Object} otlpExporterOptions * @property {boolean} [enabled] * @property {number} [port] * @property {string} [semConvVersion] */ /** * @typedef {Object} globalStackTraceConfig * @property {string} [stackTrace] * @property {number} [stackTraceLength] */ /** * @typedef {Object} InstanaMetricsOption * @property {number} [transmissionDelay] * @property {number} [timeBetweenHealthcheckCalls] */ /** * @typedef {'contains' | 'equals-ignore-case' | 'equals' | 'contains-ignore-case' | 'regex' | 'none'} MatchingOption */ /** * @typedef {Object} InstanaSecretsOption * @property {MatchingOption} [matcherMode] * @property {Array<string>} [keywords] */ /** @type {String[]} */ const allowedSecretMatchers = ['equals', 'equals-ignore-case', 'contains', 'contains-ignore-case', 'regex', 'none']; const transmissionDelayMaxValue = 5000; /** * @typedef {Object} InstanaConfig * @property {string} [serviceName] * @property {import('../core').GenericLogger} [logger] * @property {string} [packageJsonPath] * @property {InstanaMetricsOption} [metrics] * @property {InstanaTracingOption} [tracing] * @property {InstanaSecretsOption} [secrets] * @property {number} [timeBetweenHealthcheckCalls] * @property {boolean} [preloadOpentelemetry] */ /** @type {import('../core').GenericLogger} */ let logger; /** @type {InstanaConfig} */ let defaults = { serviceName: null, packageJsonPath: null, metrics: { transmissionDelay: 1000, timeBetweenHealthcheckCalls: 3000 }, tracing: { enabled: true, useOpentelemetry: true, allowRootExitSpan: false, automaticTracingEnabled: true, activateImmediately: false, forceTransmissionStartingAt: 500, maxBufferedSpans: 1000, transmissionDelay: 1000, initialTransmissionDelay: 1000, http: { extraHttpHeadersToCapture: [] }, stackTrace: DEFAULT_STACK_TRACE_MODE, stackTraceLength: DEFAULT_STACK_TRACE_LENGTH, disable: {}, spanBatchingEnabled: false, disableW3cTraceCorrelation: false, kafka: { traceCorrelation: true }, ignoreEndpoints: {}, ignoreEndpointsDisableSuppression: false, disableEOLEvents: false, otlp: { enabled: false, // Currently, we only have http protocol support and default to 4318 // This option is internal and not exposed port: 4318, semConvVersion: '1.23' } }, preloadOpentelemetry: false, secrets: { matcherMode: 'contains-ignore-case', keywords: ['key', 'pass', 'secret'] } }; const validSecretsMatcherModes = ['equals-ignore-case', 'equals', 'contains-ignore-case', 'contains', 'regex', 'none']; module.exports.configNormalizers = configNormalizers; module.exports.configValidators = configValidators; /** * @param {import('../core').GenericLogger} [_logger] */ module.exports.init = _logger => { logger = _logger; configNormalizers.init({ logger }); util.init(logger); }; /** * Merges the config that was passed to the init function with environment variables and default values. * @param {{ userConfig?: InstanaConfig, finalConfigBase?: Object, defaultsOverride?: InstanaConfig }} [options] * @returns {InstanaConfig} */ module.exports.normalize = ({ userConfig = {}, finalConfigBase = {}, defaultsOverride = {} } = {}) => { if (defaultsOverride && typeof defaultsOverride === 'object' && Object.keys(defaultsOverride).length > 0) { defaults = deepMerge(defaults, defaultsOverride); } let normalizedUserConfig; // NOTE: Do not modify the original user input object if (userConfig !== null && userConfig !== undefined) { normalizedUserConfig = Object.assign({}, userConfig); } else { normalizedUserConfig = {}; } // TODO: This call needs to be reconsidered when we add the full config instance (`config.get(...)`). configStore.clear(); // Preserve finalConfigBase in the finalConfig to allow additional config values // that are not part of the core config schema. Eg: collector config needs to be preserved. /** @type InstanaConfig */ const finalConfig = finalConfigBase ? Object.assign({}, finalConfigBase) : {}; // TODO: remove this and forward the logger via init fn. finalConfig.logger = logger; normalizeServiceName({ userConfig: normalizedUserConfig, defaultConfig: defaults, finalConfig }); normalizePackageJsonPath({ userConfig: normalizedUserConfig, defaultConfig: defaults, finalConfig }); normalizeMetricsConfig({ userConfig: normalizedUserConfig, defaultConfig: defaults, finalConfig }); normalizeTracingConfig({ userConfig: normalizedUserConfig, defaultConfig: defaults, finalConfig }); normalizeSecrets({ userConfig: normalizedUserConfig, defaultConfig: defaults, finalConfig }); normalizePreloadOpentelemetry({ userConfig: normalizedUserConfig, defaultConfig: defaults, finalConfig }); currentConfig = finalConfig; return finalConfig; }; /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeServiceName({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const { value, source } = util.resolve( { envValue: 'INSTANA_SERVICE_NAME', inCodeValue: userConfig.serviceName, defaultValue: defaultConfig.serviceName }, [validate.stringValidator] ); configStore.set('config.serviceName', { source }); finalConfig.serviceName = value; util.log({ configPath: 'config.serviceName', source, value, envVarName: 'INSTANA_SERVICE_NAME' }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizePackageJsonPath({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const { value, source } = util.resolve( { envValue: 'INSTANA_PACKAGE_JSON_PATH', inCodeValue: userConfig.packageJsonPath, defaultValue: defaultConfig.packageJsonPath }, [validate.stringValidator] ); configStore.set('config.packageJsonPath', { source }); finalConfig.packageJsonPath = value; util.log({ configPath: 'config.packageJsonPath', source, value, envVarName: 'INSTANA_PACKAGE_JSON_PATH' }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeMetricsConfig({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const userMetrics = userConfig.metrics; finalConfig.metrics = {}; const { value: transmissionDelay, source: transmissionDelaySource } = util.resolve( { envValue: 'INSTANA_METRICS_TRANSMISSION_DELAY', inCodeValue: userMetrics?.transmissionDelay, defaultValue: defaultConfig.metrics.transmissionDelay }, [validate.numberValidator] ); finalConfig.metrics.transmissionDelay = transmissionDelay; // Validate max value for transmissionDelay if (finalConfig.metrics.transmissionDelay > transmissionDelayMaxValue) { logger.warn( // eslint-disable-next-line max-len `The value of config.metrics.transmissionDelay (or INSTANA_METRICS_TRANSMISSION_DELAY) (${finalConfig.metrics.transmissionDelay}) exceeds the maximum allowed value of ${transmissionDelayMaxValue}. Assuming the max value ${transmissionDelayMaxValue}.` ); finalConfig.metrics.transmissionDelay = transmissionDelayMaxValue; } configStore.set('config.metrics.transmissionDelay', { source: transmissionDelaySource }); util.log({ configPath: 'config.metrics.transmissionDelay', source: transmissionDelaySource, value: transmissionDelay, envVarName: 'INSTANA_METRICS_TRANSMISSION_DELAY' }); const { value: healthcheckInterval, source: healthcheckSource } = util.resolve( { inCodeValue: userMetrics?.timeBetweenHealthcheckCalls, defaultValue: defaultConfig.metrics.timeBetweenHealthcheckCalls }, [validate.numberValidator] ); finalConfig.metrics.timeBetweenHealthcheckCalls = healthcheckInterval; configStore.set('config.metrics.timeBetweenHealthcheckCalls', { source: healthcheckSource }); util.log({ configPath: 'config.metrics.timeBetweenHealthcheckCalls', source: healthcheckSource, value: healthcheckInterval }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeTracingConfig({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { finalConfig.tracing = finalConfig.tracing || {}; userConfig.tracing = userConfig.tracing || {}; normalizeTracingEnabled({ userConfig, defaultConfig, finalConfig }); normalizeUseOpentelemetry({ userConfig, defaultConfig, finalConfig }); normalizeDisableTracing({ userConfig, defaultConfig, finalConfig }); normalizeAutomaticTracingEnabled({ userConfig, defaultConfig, finalConfig }); normalizeActivateImmediately({ userConfig, defaultConfig, finalConfig }); normalizeTracingTransmission({ userConfig, defaultConfig, finalConfig }); normalizeTracingHttp({ userConfig, defaultConfig, finalConfig }); normalizeTracingStackTrace({ userConfig, defaultConfig, finalConfig }); normalizeSpanBatchingEnabled({ userConfig, defaultConfig, finalConfig }); normalizeDisableW3cTraceCorrelation({ userConfig, defaultConfig, finalConfig }); normalizeTracingKafka({ userConfig, defaultConfig, finalConfig }); normalizeAllowRootExitSpan({ userConfig, defaultConfig, finalConfig }); normalizeIgnoreEndpoints({ userConfig, defaultConfig, finalConfig }); normalizeIgnoreEndpointsDisableSuppression({ userConfig, defaultConfig, finalConfig }); normalizeDisableEOLEvents({ userConfig, defaultConfig, finalConfig }); normalizeOtlpExporter({ userConfig, defaultConfig, finalConfig }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeTracingEnabled({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { // INSTANA_TRACING_DISABLE can be either: // 1. A boolean ('true'/'false') to enable/disable all tracing // 2. A list of instrumentations/groups to selectively disable // We only use it for tracing.enabled if it's a boolean value const envValue = process.env.INSTANA_TRACING_DISABLE; const isBooleanValue = envValue === 'true' || envValue === 'false'; const { value, source } = util.resolve( { envValue: isBooleanValue ? 'INSTANA_TRACING_DISABLE' : undefined, inCodeValue: userConfig.tracing.enabled, defaultValue: defaultConfig.tracing.enabled }, [validate.booleanValidator] ); // The env var is TRACING_DISABLE, so we need to invert it when it comes from env // TODO: Consider adding this normalization support to util.resolver const finalValue = source === CONFIG_SOURCES.ENV ? !value : value; configStore.set('config.tracing.enabled', { source }); finalConfig.tracing.enabled = finalValue; util.log({ configPath: 'config.tracing.enabled', source, value: finalValue, envVarName: source === CONFIG_SOURCES.ENV ? 'INSTANA_TRACING_DISABLE' : undefined }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeAllowRootExitSpan({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const { value, source } = util.resolve( { envValue: 'INSTANA_ALLOW_ROOT_EXIT_SPAN', inCodeValue: userConfig.tracing.allowRootExitSpan, defaultValue: defaultConfig.tracing.allowRootExitSpan }, [validate.booleanValidator] ); configStore.set('config.tracing.allowRootExitSpan', { source }); finalConfig.tracing.allowRootExitSpan = value; util.log({ configPath: 'config.tracing.allowRootExitSpan', source, value, envVarName: 'INSTANA_ALLOW_ROOT_EXIT_SPAN' }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeUseOpentelemetry({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const { value, source } = util.resolve( { envValue: 'INSTANA_DISABLE_USE_OPENTELEMETRY', inCodeValue: userConfig.tracing.useOpentelemetry, defaultValue: defaultConfig.tracing.useOpentelemetry }, [validate.booleanValidator] ); // The env var is DISABLE_USE_OPENTELEMETRY, so we need to invert it when it comes from env // TODO: add normalization helpers to util.resolve(...) const finalValue = source === CONFIG_SOURCES.ENV ? !value : value; configStore.set('config.tracing.useOpentelemetry', { source }); finalConfig.tracing.useOpentelemetry = finalValue; util.log({ configPath: 'config.tracing.useOpentelemetry', source, value: finalValue, envVarName: source === CONFIG_SOURCES.ENV ? 'INSTANA_DISABLE_USE_OPENTELEMETRY' : undefined }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeAutomaticTracingEnabled({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { if (!finalConfig.tracing.enabled) { finalConfig.tracing.automaticTracingEnabled = false; return; } const { value, source } = util.resolve( { envValue: 'INSTANA_DISABLE_AUTO_INSTR', inCodeValue: userConfig.tracing.automaticTracingEnabled, defaultValue: defaultConfig.tracing.automaticTracingEnabled }, [validate.booleanValidator] ); // The env var is DISABLE_AUTO_INSTR, so we need to invert it when it comes from env // TODO: add normalization helpers to util.resolve(...) const finalValue = source === CONFIG_SOURCES.ENV ? !value : value; configStore.set('config.tracing.automaticTracingEnabled', { source }); finalConfig.tracing.automaticTracingEnabled = finalValue; util.log({ configPath: 'config.tracing.automaticTracingEnabled', source, value: finalValue, envVarName: source === CONFIG_SOURCES.ENV ? 'INSTANA_DISABLE_AUTO_INSTR' : undefined }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeActivateImmediately({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { if (!finalConfig.tracing.enabled) { finalConfig.tracing.activateImmediately = false; return; } const { value, source } = util.resolve( { envValue: 'INSTANA_TRACE_IMMEDIATELY', inCodeValue: userConfig.tracing.activateImmediately, defaultValue: defaultConfig.tracing.activateImmediately }, [validate.booleanValidator] ); configStore.set('config.tracing.activateImmediately', { source }); finalConfig.tracing.activateImmediately = value; util.log({ configPath: 'config.tracing.activateImmediately', source, value, envVarName: 'INSTANA_TRACE_IMMEDIATELY' }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeTracingTransmission({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { finalConfig.tracing.maxBufferedSpans = userConfig.tracing.maxBufferedSpans ?? defaultConfig.tracing.maxBufferedSpans; const maxBufferedSpansSource = userConfig.tracing.maxBufferedSpans !== undefined ? CONFIG_SOURCES.INCODE : CONFIG_SOURCES.DEFAULT; configStore.set('config.tracing.maxBufferedSpans', { source: maxBufferedSpansSource }); util.log({ configPath: 'config.tracing.maxBufferedSpans', source: maxBufferedSpansSource, value: finalConfig.tracing.maxBufferedSpans }); const { value: tracingTransmissionDelay, source: tracingTransmissionDelaySource } = util.resolve( { envValue: 'INSTANA_TRACING_TRANSMISSION_DELAY', inCodeValue: userConfig.tracing.transmissionDelay, defaultValue: defaultConfig.tracing.transmissionDelay }, [validate.numberValidator] ); configStore.set('config.tracing.transmissionDelay', { source: tracingTransmissionDelaySource }); finalConfig.tracing.transmissionDelay = tracingTransmissionDelay; util.log({ configPath: 'config.tracing.transmissionDelay', source: tracingTransmissionDelaySource, value: tracingTransmissionDelay, envVarName: 'INSTANA_TRACING_TRANSMISSION_DELAY' }); const { value: forceTransmissionStartingAt, source: forceTransmissionStartingAtSource } = util.resolve( { envValue: 'INSTANA_FORCE_TRANSMISSION_STARTING_AT', inCodeValue: userConfig.tracing.forceTransmissionStartingAt, defaultValue: defaultConfig.tracing.forceTransmissionStartingAt }, [validate.numberValidator] ); configStore.set('config.tracing.forceTransmissionStartingAt', { source: forceTransmissionStartingAtSource }); finalConfig.tracing.forceTransmissionStartingAt = forceTransmissionStartingAt; util.log({ configPath: 'config.tracing.forceTransmissionStartingAt', source: forceTransmissionStartingAtSource, value: forceTransmissionStartingAt, envVarName: 'INSTANA_FORCE_TRANSMISSION_STARTING_AT' }); const { value: initialTransmissionDelay, source: initialTransmissionDelaySource } = util.resolve( { envValue: 'INSTANA_TRACING_INITIAL_TRANSMISSION_DELAY', inCodeValue: userConfig.tracing.initialTransmissionDelay, defaultValue: defaultConfig.tracing.initialTransmissionDelay }, [validate.numberValidator] ); configStore.set('config.tracing.initialTransmissionDelay', { source: initialTransmissionDelaySource }); finalConfig.tracing.initialTransmissionDelay = initialTransmissionDelay; util.log({ configPath: 'config.tracing.initialTransmissionDelay', source: initialTransmissionDelaySource, value: initialTransmissionDelay, envVarName: 'INSTANA_TRACING_INITIAL_TRANSMISSION_DELAY' }); } /** * NOTE: This normalization logic is not handled in the resolver. * because it involves complex multi-step processing: * Future improvement: Consider refactoring to use a more generic resolver pattern. * * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeTracingHttp({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const userHttp = userConfig.tracing.http; finalConfig.tracing.http = {}; const userHeaders = userHttp?.extraHttpHeadersToCapture; // 1. Check environment variable if (process.env.INSTANA_EXTRA_HTTP_HEADERS) { const fromEnvVar = parseHeadersEnvVar(process.env.INSTANA_EXTRA_HTTP_HEADERS); finalConfig.tracing.http.extraHttpHeadersToCapture = fromEnvVar; configStore.set('config.tracing.http.extraHttpHeadersToCapture', { source: CONFIG_SOURCES.ENV }); util.log({ configPath: 'config.tracing.http.extraHttpHeadersToCapture', source: CONFIG_SOURCES.ENV, value: fromEnvVar, envVarName: 'INSTANA_EXTRA_HTTP_HEADERS' }); return; } // 2. Check in-code configuration if (userHeaders !== undefined) { if (!Array.isArray(userHeaders)) { logger.warn( // eslint-disable-next-line max-len `Invalid configuration: config.tracing.http.extraHttpHeadersToCapture is not an array, the value will be ignored: ${JSON.stringify( userHeaders )}` ); } else { finalConfig.tracing.http.extraHttpHeadersToCapture = userHeaders.map(s => s.toLowerCase()); configStore.set('config.tracing.http.extraHttpHeadersToCapture', { source: CONFIG_SOURCES.INCODE }); util.log({ configPath: 'config.tracing.http.extraHttpHeadersToCapture', source: CONFIG_SOURCES.INCODE, value: finalConfig.tracing.http.extraHttpHeadersToCapture }); return; } } // 3. Use default configuration finalConfig.tracing.http.extraHttpHeadersToCapture = defaultConfig.tracing.http.extraHttpHeadersToCapture; configStore.set('config.tracing.http.extraHttpHeadersToCapture', { source: CONFIG_SOURCES.DEFAULT }); } /** * @param {string} envVarValue * @returns {Array<string>} */ function parseHeadersEnvVar(envVarValue) { return envVarValue .split(/[;,]/) .map(header => header.trim().toLowerCase()) .filter(header => header !== ''); } /** * Handles both stackTrace and stackTraceLength configuration * * NOTE: This normalization logic is not handled in the resolver. * because it involves complex multi-step processing: * Future improvement: Consider refactoring to use a more generic resolver pattern. * * * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeTracingStackTrace({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const userTracingConfig = userConfig.tracing; const userGlobal = userTracingConfig.global; const envStackTrace = process.env.INSTANA_STACK_TRACE; const envStackTraceLength = process.env.INSTANA_STACK_TRACE_LENGTH; // Priority 1: Environment variable if (envStackTrace !== undefined) { const result = validateStackTraceMode(envStackTrace); if (result.isValid) { const normalized = configNormalizers.stackTrace.normalizeStackTraceModeFromEnv(envStackTrace); if (normalized !== null) { finalConfig.tracing.stackTrace = normalized; configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.ENV }); } else { finalConfig.tracing.stackTrace = defaultConfig.tracing.stackTrace; configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.DEFAULT }); } } else { logger.warn(`Invalid env INSTANA_STACK_TRACE: ${result.error}`); finalConfig.tracing.stackTrace = defaultConfig.tracing.stackTrace; configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.DEFAULT }); } } else if (userGlobal?.stackTrace !== undefined) { // Priority 2: In-code configuration const result = validateStackTraceMode(userGlobal.stackTrace); if (result.isValid) { const normalized = configNormalizers.stackTrace.normalizeStackTraceMode(userConfig); if (normalized !== null) { finalConfig.tracing.stackTrace = normalized; configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.INCODE }); } else { finalConfig.tracing.stackTrace = defaultConfig.tracing.stackTrace; configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.DEFAULT }); } } else { logger.warn(`Invalid config.tracing.global.stackTrace: ${result.error}`); finalConfig.tracing.stackTrace = defaultConfig.tracing.stackTrace; configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.DEFAULT }); } } else { finalConfig.tracing.stackTrace = defaultConfig.tracing.stackTrace; configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.DEFAULT }); } const isLegacyLengthDefined = userTracingConfig?.stackTraceLength !== undefined; const stackTraceConfigValue = userGlobal?.stackTraceLength || userTracingConfig?.stackTraceLength; // Priority 1: Environment variable if (envStackTraceLength !== undefined) { const result = validateStackTraceLength(envStackTraceLength); if (result.isValid) { const normalized = configNormalizers.stackTrace.normalizeStackTraceLengthFromEnv(envStackTraceLength); if (normalized !== null) { finalConfig.tracing.stackTraceLength = normalized; configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.ENV }); } else { finalConfig.tracing.stackTraceLength = defaultConfig.tracing.stackTraceLength; configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.DEFAULT }); } } else { logger.warn(`Invalid env INSTANA_STACK_TRACE_LENGTH: ${result.error}`); finalConfig.tracing.stackTraceLength = defaultConfig.tracing.stackTraceLength; configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.DEFAULT }); } } else if (stackTraceConfigValue !== undefined) { // Priority 2: In-code configuration if (isLegacyLengthDefined) { logger.warn( // eslint-disable-next-line max-len '[Deprecation Warning] The configuration option config.tracing.stackTraceLength is deprecated and will be removed in a future release. ' + 'Please use config.tracing.global.stackTraceLength instead.' ); } const result = validateStackTraceLength(stackTraceConfigValue); if (result.isValid) { const normalized = configNormalizers.stackTrace.normalizeStackTraceLength(userConfig); if (normalized !== null) { finalConfig.tracing.stackTraceLength = normalized; configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.INCODE }); } else { finalConfig.tracing.stackTraceLength = defaultConfig.tracing.stackTraceLength; configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.DEFAULT }); } } else { logger.warn(`Invalid stackTraceLength value: ${result.error}`); finalConfig.tracing.stackTraceLength = defaultConfig.tracing.stackTraceLength; configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.DEFAULT }); } } else { finalConfig.tracing.stackTraceLength = defaultConfig.tracing.stackTraceLength; configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.DEFAULT }); } } /** * NOTE: This normalization logic is not handled in the resolver. * because it involves complex multi-step processing: * Future improvement: Consider refactoring to use a more generic resolver pattern. * * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeDisableTracing({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const disableRes = configNormalizers.disable.normalize(userConfig); const disableConfig = disableRes?.value; // If tracing is globally disabled (via `disable: true` or INSTANA_TRACING_DISABLE=true ), // mark `tracing.enabled` as false and clear any specific disable rules. if (disableConfig === true) { finalConfig.tracing.enabled = false; finalConfig.tracing.disable = {}; configStore.set('config.tracing.disable', { source: disableRes.source }); return; } if (typeof disableConfig === 'object' && disableRes.source !== CONFIG_SOURCES.DEFAULT) { finalConfig.tracing.disable = disableConfig; configStore.set('config.tracing.disable', { source: disableRes.source }); return; } finalConfig.tracing.disable = defaultConfig.tracing.disable; configStore.set('config.tracing.disable', { source: CONFIG_SOURCES.DEFAULT }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeSpanBatchingEnabled({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const { value, source } = util.resolve( { envValue: 'INSTANA_SPANBATCHING_ENABLED', inCodeValue: userConfig.tracing.spanBatchingEnabled, defaultValue: defaultConfig.tracing.spanBatchingEnabled }, [validate.booleanValidator] ); configStore.set('config.tracing.spanBatchingEnabled', { source }); finalConfig.tracing.spanBatchingEnabled = value; util.log({ configPath: 'config.tracing.spanBatchingEnabled', source, value, envVarName: 'INSTANA_SPANBATCHING_ENABLED' }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeDisableW3cTraceCorrelation({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const { value, source } = util.resolve( { envValue: 'INSTANA_DISABLE_W3C_TRACE_CORRELATION', inCodeValue: userConfig.tracing.disableW3cTraceCorrelation, defaultValue: defaultConfig.tracing.disableW3cTraceCorrelation }, [validate.validateTruthyBoolean] ); configStore.set('config.tracing.disableW3cTraceCorrelation', { source }); finalConfig.tracing.disableW3cTraceCorrelation = value; util.log({ configPath: 'config.tracing.disableW3cTraceCorrelation', source, value, envVarName: 'INSTANA_DISABLE_W3C_TRACE_CORRELATION' }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeTracingKafka({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const userKafka = userConfig.tracing.kafka || {}; finalConfig.tracing.kafka = finalConfig.tracing.kafka || {}; const { value, source } = util.resolve( { envValue: 'INSTANA_KAFKA_TRACE_CORRELATION', inCodeValue: userKafka.traceCorrelation, defaultValue: defaultConfig.tracing.kafka.traceCorrelation }, [validate.booleanValidator] ); configStore.set('config.tracing.kafka.traceCorrelation', { source }); finalConfig.tracing.kafka.traceCorrelation = value; util.log({ configPath: 'config.tracing.kafka.traceCorrelation', source, value, envVarName: 'INSTANA_KAFKA_TRACE_CORRELATION' }); } /** * NOTE: This normalization logic is not handled in the resolver. * because it involves complex multi-step processing: * Future improvement: Consider refactoring to use a more generic resolver pattern. * * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeSecrets({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const userSecrets = userConfig.secrets; finalConfig.secrets = {}; /** @type {InstanaSecretsOption} */ let fromEnvVar = {}; if (process.env.INSTANA_SECRETS) { fromEnvVar = parseSecretsEnvVar(process.env.INSTANA_SECRETS); } let matcherModeSource; let matcherMode; if (fromEnvVar.matcherMode) { matcherMode = fromEnvVar.matcherMode; matcherModeSource = CONFIG_SOURCES.ENV; logger.debug(`[config] env:INSTANA_SECRETS (matcherMode) = ${matcherMode}`); } else if (userSecrets?.matcherMode) { matcherMode = userSecrets.matcherMode; matcherModeSource = CONFIG_SOURCES.INCODE; logger.debug(`[config] incode:config.secrets.matcherMode = ${matcherMode}`); } else { matcherMode = defaultConfig.secrets.matcherMode; matcherModeSource = CONFIG_SOURCES.DEFAULT; } let keywordsSource; let keywords; if (fromEnvVar.keywords) { keywords = fromEnvVar.keywords; keywordsSource = CONFIG_SOURCES.ENV; logger.debug('[config] env:INSTANA_SECRETS (keywords)'); } else if (userSecrets?.keywords) { keywords = userSecrets.keywords; keywordsSource = CONFIG_SOURCES.INCODE; logger.debug('[config] incode:config.secrets.keywords'); } else { keywords = defaultConfig.secrets.keywords; keywordsSource = CONFIG_SOURCES.DEFAULT; } if (typeof matcherMode !== 'string') { logger.warn( // eslint-disable-next-line max-len `The value of config.secrets.matcherMode ("${matcherMode}") is not a string. Assuming the default value ${defaults.secrets.matcherMode}.` ); finalConfig.secrets.matcherMode = defaultConfig.secrets.matcherMode; configStore.set('config.secrets.matcherMode', { source: CONFIG_SOURCES.DEFAULT }); } else if (validSecretsMatcherModes.indexOf(matcherMode) < 0) { logger.warn( // eslint-disable-next-line max-len `The value of config.secrets.matcherMode (or the matcher mode parsed from INSTANA_SECRETS) (${matcherMode}) is not a supported matcher mode. Assuming the default value ${defaults.secrets.matcherMode}.` ); finalConfig.secrets.matcherMode = defaultConfig.secrets.matcherMode; configStore.set('config.secrets.matcherMode', { source: CONFIG_SOURCES.DEFAULT }); } else { finalConfig.secrets.matcherMode = matcherMode; configStore.set('config.secrets.matcherMode', { source: matcherModeSource }); } if (!Array.isArray(keywords)) { logger.warn( // eslint-disable-next-line max-len `The value of config.secrets.keywords (${keywords}) is not an array. Assuming the default value ${defaults.secrets.keywords}.` ); finalConfig.secrets.keywords = defaultConfig.secrets.keywords; configStore.set('config.secrets.keywords', { source: CONFIG_SOURCES.DEFAULT }); } else { finalConfig.secrets.keywords = keywords; configStore.set('config.secrets.keywords', { source: keywordsSource }); } if (finalConfig.secrets.matcherMode === 'none') { finalConfig.secrets.keywords = []; configStore.set('config.secrets.keywords', { source: matcherModeSource }); } } /** * @param {string} matcherMode * @returns {MatchingOption} */ function parseMatcherMode(matcherMode) { const trimmed = matcherMode.trim().toLowerCase(); const isSecretMatcher = allowedSecretMatchers.includes(trimmed); if (isSecretMatcher) { return /** @type {MatchingOption} */ (trimmed); } else { return /** @type {MatchingOption} */ 'contains-ignore-case'; } } /** * @param {string} envVarValue * @returns {InstanaSecretsOption} */ function parseSecretsEnvVar(envVarValue) { const [matcherMode, keywords] = envVarValue.split(':', 2); const parsedMatcherMode = parseMatcherMode(matcherMode); if (parsedMatcherMode === 'none') { return { matcherMode: parsedMatcherMode, keywords: [] }; } if (!keywords) { // a list of keywords (with at least one element) is mandatory for all matcher modes except "none" logger.warn( // eslint-disable-next-line max-len `The value of INSTANA_SECRETS (${envVarValue}) cannot be parsed. Please use the following format: INSTANA_SECRETS=<matcher>:<secret>[,<secret>]. This setting will be ignored.` ); return {}; } const keywordsArray = keywords.split(',').map(word => word.trim()); return { matcherMode: parsedMatcherMode, keywords: keywordsArray }; } /** * NOTE: This normalization logic is not handled in the resolver. * because it involves complex multi-step processing: * Future improvement: Consider refactoring to use a more generic resolver pattern. * * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeIgnoreEndpoints({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const userIgnoreEndpoints = userConfig.tracing.ignoreEndpoints; // Priority 1: Load from a YAML file if `INSTANA_IGNORE_ENDPOINTS_PATH` is set // Introduced in Phase 2 for advanced filtering based on both methods and endpoints. // Also supports basic filtering for endpoints. if (process.env.INSTANA_IGNORE_ENDPOINTS_PATH) { finalConfig.tracing.ignoreEndpoints = configNormalizers.ignoreEndpoints.fromYaml( process.env.INSTANA_IGNORE_ENDPOINTS_PATH ); logger.debug('[config] env:INSTANA_IGNORE_ENDPOINTS_PATH'); configStore.set('config.tracing.ignoreEndpoints', { source: CONFIG_SOURCES.ENV }); return; } // Priority 2: Load from the `INSTANA_IGNORE_ENDPOINTS` environment variable // Introduced in Phase 1 for basic filtering based only on operations (e.g., `redis.get`, `kafka.consume`). // Provides a simple way to configure ignored operations via environment variables. if (process.env.INSTANA_IGNORE_ENDPOINTS) { finalConfig.tracing.ignoreEndpoints = configNormalizers.ignoreEndpoints.fromEnv( process.env.INSTANA_IGNORE_ENDPOINTS ); logger.debug('[config] env:INSTANA_IGNORE_ENDPOINTS'); configStore.set('config.tracing.ignoreEndpoints', { source: CONFIG_SOURCES.ENV }); return; } // Priority 3: Use in-code configuration if available if (userIgnoreEndpoints && (typeof userIgnoreEndpoints !== 'object' || Array.isArray(userIgnoreEndpoints))) { logger.warn( `Invalid tracing.ignoreEndpoints configuration. Expected an object, but received: ${JSON.stringify( userIgnoreEndpoints )}` ); finalConfig.tracing.ignoreEndpoints = defaultConfig.tracing.ignoreEndpoints; configStore.set('config.tracing.ignoreEndpoints', { source: CONFIG_SOURCES.DEFAULT }); return; } if (userIgnoreEndpoints && Object.keys(userIgnoreEndpoints).length) { finalConfig.tracing.ignoreEndpoints = configNormalizers.ignoreEndpoints.normalizeConfig(userIgnoreEndpoints); logger.debug('[config] incode:config.tracing.ignoreEndpoints'); configStore.set('config.tracing.ignoreEndpoints', { source: CONFIG_SOURCES.INCODE }); return; } finalConfig.tracing.ignoreEndpoints = defaultConfig.tracing.ignoreEndpoints; configStore.set('config.tracing.ignoreEndpoints', { source: CONFIG_SOURCES.DEFAULT }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeIgnoreEndpointsDisableSuppression({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const { value, source } = util.resolve( { envValue: 'INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION', inCodeValue: userConfig.tracing.ignoreEndpointsDisableSuppression, defaultValue: defaultConfig.tracing.ignoreEndpointsDisableSuppression }, [validate.booleanValidator] ); configStore.set('config.tracing.ignoreEndpointsDisableSuppression', { source }); finalConfig.tracing.ignoreEndpointsDisableSuppression = value; util.log({ configPath: 'config.tracing.ignoreEndpointsDisableSuppression', source, value, envVarName: 'INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION' }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeDisableEOLEvents({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const { value, source } = util.resolve( { envValue: 'INSTANA_TRACING_DISABLE_EOL_EVENTS', inCodeValue: userConfig.tracing.disableEOLEvents, defaultValue: defaultConfig.tracing.disableEOLEvents }, [validate.booleanValidator] ); configStore.set('config.tracing.disableEOLEvents', { source }); finalConfig.tracing.disableEOLEvents = value; util.log({ configPath: 'config.tracing.disableEOLEvents', source, value, envVarName: 'INSTANA_TRACING_DISABLE_EOL_EVENTS' }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizePreloadOpentelemetry({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { const { value, source } = util.resolve( { inCodeValue: userConfig.preloadOpentelemetry, defaultValue: defaultConfig.preloadOpentelemetry }, [validate.booleanValidator] ); finalConfig.preloadOpentelemetry = value; configStore.set('config.preloadOpentelemetry', { source }); util.log({ configPath: 'config.preloadOpentelemetry', source, value }); } /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ function normalizeOtlpExporter({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { // TODO: This needs to be extended for the rest of the otlp configurations const userOtlp = userConfig.tracing?.otlp || {}; finalConfig.tracing.otlp = Object.assign({}, defaultConfig.tracing?.otlp, finalConfig.tracing.otlp); const { value, source } = util.resolve( { envValue: 'INSTANA_TRACING_OTLP_ENABLED', inCodeValue: userOtlp.enabled, defaultValue: defaultConfig.tracing?.otlp?.enabled }, [validate.booleanValidator] ); configStore.set('config.tracing.otlp.enabled', { source }); finalConfig.tracing.otlp.enabled = value; util.log({ configPath: 'config.tracing.otlp.enabled', source, value, envVarName: 'INSTANA_TRACING_OTLP_ENABLED' }); } /** * Updates configuration values dynamically from external sources (e.g., agent) * * @param {Object} [params] * @param {Record<string, any>} [params.externalConfig] * @param {number} [params.source] * @param {Record<string, any>} [params.target=currentConfig] * @param {string} [params.basePath='config'] * @returns {Record<string, any>} */ exports.update = function update({ externalConfig = {}, source, target = currentConfig, basePath = 'config' } = {}) { if (!externalConfig || typeof externalConfig !== 'object' || Object.keys(externalConfig).length === 0) { return currentConfig; } Object.keys(externalConfig).forEach(key => { const path = `${basePath}.${key}`; const currentMeta = configStore.get(path); if (currentMeta && currentMeta.source < source) { logger.debug(`[config] Skipping ${path}: current source ${currentMeta.source} > incoming ${source}`); return; } const incomingValue = externalConfig[key]; if (incomingValue && typeof incomingValue === 'object' && !Array.isArray(incomingValue)) { if (!target[key] || typeof target[key] !== 'object') { target[key] = {}; } update({ externalConfig: incomingValue, source, target: target[key], basePath: path }); } else { target[key] = incomingValue; configStore.set(path, { source }); util.log({ configPath: path, source, value: incomingValue }); } }); return currentConfig; };