diff --git a/packages/core/src/config/index.js b/packages/core/src/config/index.js index dde9e8e041..ba65c195a2 100644 --- a/packages/core/src/config/index.js +++ b/packages/core/src/config/index.js @@ -13,6 +13,7 @@ const configValidators = require('./configValidators'); const deepMerge = require('../util/deepMerge'); const { DEFAULT_STACK_TRACE_LENGTH, DEFAULT_STACK_TRACE_MODE } = require('../util/constants'); const { validateStackTraceMode, validateStackTraceLength } = require('./configValidators/stackTraceValidation'); +const util = require('./util'); /** * @typedef {Object} InstanaTracingOption @@ -141,6 +142,7 @@ module.exports.configValidators = configValidators; module.exports.init = _logger => { logger = _logger; configNormalizers.init({ logger }); + util.init(logger); }; /** @@ -216,12 +218,12 @@ function normalizeMetricsConfig(config) { config.metrics = {}; } - config.metrics.transmissionDelay = normalizeSingleValue( - config.metrics.transmissionDelay, - defaults.metrics.transmissionDelay, - 'config.metrics.transmissionDelay', - 'INSTANA_METRICS_TRANSMISSION_DELAY' - ); + config.metrics.transmissionDelay = util.resolveNumericConfig({ + envVar: 'INSTANA_METRICS_TRANSMISSION_DELAY', + configValue: config.metrics.transmissionDelay, + defaultValue: defaults.metrics.transmissionDelay, + configPath: 'config.metrics.transmissionDelay' + }); config.metrics.timeBetweenHealthcheckCalls = config.metrics.timeBetweenHealthcheckCalls || defaults.metrics.timeBetweenHealthcheckCalls; @@ -372,12 +374,12 @@ function normalizeActivateImmediately(config) { function normalizeTracingTransmission(config) { config.tracing.maxBufferedSpans = config.tracing.maxBufferedSpans || defaults.tracing.maxBufferedSpans; - config.tracing.transmissionDelay = normalizeSingleValue( - config.tracing.transmissionDelay, - defaults.tracing.transmissionDelay, - 'config.tracing.transmissionDelay', - 'INSTANA_TRACING_TRANSMISSION_DELAY' - ); + config.tracing.transmissionDelay = util.resolveNumericConfig({ + envVar: 'INSTANA_TRACING_TRANSMISSION_DELAY', + configValue: config.tracing.transmissionDelay, + defaultValue: defaults.tracing.transmissionDelay, + configPath: 'config.tracing.transmissionDelay' + }); // DEPRECATED! This was never documented, but we shared it with a customer. if (process.env['INSTANA_DEV_MIN_DELAY_BEFORE_SENDING_SPANS']) { @@ -397,19 +399,19 @@ function normalizeTracingTransmission(config) { } } - config.tracing.forceTransmissionStartingAt = normalizeSingleValue( - config.tracing.forceTransmissionStartingAt, - defaults.tracing.forceTransmissionStartingAt, - 'config.tracing.forceTransmissionStartingAt', - 'INSTANA_FORCE_TRANSMISSION_STARTING_AT' - ); + config.tracing.forceTransmissionStartingAt = util.resolveNumericConfig({ + envVar: 'INSTANA_FORCE_TRANSMISSION_STARTING_AT', + configValue: config.tracing.forceTransmissionStartingAt, + defaultValue: defaults.tracing.forceTransmissionStartingAt, + configPath: 'config.tracing.forceTransmissionStartingAt' + }); - config.tracing.initialTransmissionDelay = normalizeSingleValue( - config.tracing.initialTransmissionDelay, - defaults.tracing.initialTransmissionDelay, - 'config.tracing.initialTransmissionDelay', - 'INSTANA_TRACING_INITIAL_TRANSMISSION_DELAY' - ); + config.tracing.initialTransmissionDelay = util.resolveNumericConfig({ + envVar: 'INSTANA_TRACING_INITIAL_TRANSMISSION_DELAY', + configValue: config.tracing.initialTransmissionDelay, + defaultValue: defaults.tracing.initialTransmissionDelay, + configPath: 'config.tracing.initialTransmissionDelay' + }); } /** @@ -713,32 +715,6 @@ function parseSecretsEnvVar(envVarValue) { }; } -/** - * @param {*} configValue - * @param {*} defaultValue - * @param {string} configPath - * @param {string} envVarKey - * @returns {*} - */ -function normalizeSingleValue(configValue, defaultValue, configPath, envVarKey) { - const envVarVal = process.env[envVarKey]; - let originalValue = configValue; - if (configValue == null && envVarVal == null) { - return defaultValue; - } else if (configValue == null && envVarVal != null) { - originalValue = envVarVal; - configValue = parseInt(originalValue, 10); - } - - if (typeof configValue !== 'number' || isNaN(configValue)) { - logger.warn( - `The value of ${configPath} (or ${envVarKey}) ("${originalValue}") is ' + - 'not numerical or cannot be parsed to a numerical value. Assuming the default value ${defaultValue}.` - ); - return defaultValue; - } - return configValue; -} /** * @param {InstanaConfig} config */ diff --git a/packages/core/src/config/util.js b/packages/core/src/config/util.js new file mode 100644 index 0000000000..fac00095d8 --- /dev/null +++ b/packages/core/src/config/util.js @@ -0,0 +1,55 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +/** @type {import('../core').GenericLogger} */ +let logger; + +/** + * @param {import('../core').GenericLogger} [_logger] + */ +exports.init = _logger => { + logger = _logger; +}; + +/** + * @param {Object} params + * @param {string} params.envVar + * @param {number|string|undefined|null} params.configValue + * @param {number} params.defaultValue + * @param {string} params.configPath + * @returns {number} + */ +exports.resolveNumericConfig = function resolveNumericConfig({ envVar, configValue, defaultValue, configPath }) { + const envRaw = process.env[envVar]; + + /** @param {number|string|null|undefined} val */ + const toValidNumber = val => { + const num = typeof val === 'number' ? val : Number(val); + return Number.isNaN(num) ? undefined : num; + }; + + if (envRaw != null) { + const envParsed = toValidNumber(envRaw); + if (envParsed !== undefined) { + return envParsed; + } + + logger.warn(`Invalid numeric value from env:${envVar}: "${envRaw}". Ignoring and checking config value.`); + } + + if (configValue != null) { + const configParsed = toValidNumber(configValue); + if (configParsed !== undefined) { + return configParsed; + } + + logger.warn( + `Invalid numeric value for ${configPath} from config: "${configValue}". Falling back to default: ${defaultValue}.` + ); + } + + return defaultValue; +}; diff --git a/packages/core/test/config/util_test.js b/packages/core/test/config/util_test.js new file mode 100644 index 0000000000..dd92ff2357 --- /dev/null +++ b/packages/core/test/config/util_test.js @@ -0,0 +1,241 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +const expect = require('chai').expect; +const { createFakeLogger } = require('../test_util'); +const util = require('../../src/config/util'); + +describe('config.util', () => { + let logger; + + before(() => { + logger = createFakeLogger(); + util.init(logger); + }); + + beforeEach(resetEnv); + afterEach(resetEnv); + + function resetEnv() { + delete process.env.TEST_ENV_VAR; + } + + describe('resolveNumericConfig', () => { + it('should return the default value when no env var or config value is provided', () => { + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: undefined, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(1000); + }); + + it('should prioritize env var over config value', () => { + process.env.TEST_ENV_VAR = '2000'; + + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: 3000, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(2000); + }); + + it('should use config value when env var is not set', () => { + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: 3000, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(3000); + }); + + it('should handle numeric config value', () => { + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: 5000, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(5000); + }); + + it('should handle string config value that can be parsed as number', () => { + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: '5000', + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(5000); + }); + + it('should handle string env var that can be parsed as number', () => { + process.env.TEST_ENV_VAR = '7500'; + + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: undefined, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(7500); + }); + + it('should fall back to default when env var is invalid', () => { + process.env.TEST_ENV_VAR = 'not-a-number'; + + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: undefined, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(1000); + }); + + it('should fall back to default when config value is invalid', () => { + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: 'invalid', + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(1000); + }); + + it('should use config value when env var is invalid', () => { + process.env.TEST_ENV_VAR = 'not-a-number'; + + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: 3000, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(3000); + }); + + it('should handle zero as a valid value from env var', () => { + process.env.TEST_ENV_VAR = '0'; + + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: undefined, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(0); + }); + + it('should handle zero as a valid value from config', () => { + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: 0, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(0); + }); + + it('should handle negative numbers from env var', () => { + process.env.TEST_ENV_VAR = '-500'; + + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: undefined, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(-500); + }); + + it('should handle negative numbers from config', () => { + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: -500, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(-500); + }); + + it('should handle floating point numbers from env var', () => { + process.env.TEST_ENV_VAR = '123.45'; + + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: undefined, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(123.45); + }); + + it('should handle floating point numbers from config', () => { + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: 123.45, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(123.45); + }); + + it('should handle null config value', () => { + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: null, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(1000); + }); + + it('should handle empty string env var as 0', () => { + process.env.TEST_ENV_VAR = ''; + + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: undefined, + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(0); + }); + + it('should handle empty string config value as 0', () => { + const result = util.resolveNumericConfig({ + envVar: 'TEST_ENV_VAR', + configValue: '', + defaultValue: 1000, + configPath: 'config.test.value' + }); + + expect(result).to.equal(0); + }); + }); +});