diff --git a/packages/aws-lambda/src/wrapper.js b/packages/aws-lambda/src/wrapper.js index 76a9d1e7b6..b668c1de35 100644 --- a/packages/aws-lambda/src/wrapper.js +++ b/packages/aws-lambda/src/wrapper.js @@ -36,7 +36,7 @@ const latestRuntime = semver.gte(process.version, '24.0.0'); const logger = serverlessLogger.init(); coreConfig.init(logger); -let config = coreConfig.normalize({}, lambdaConfigDefaults); +let config = coreConfig.normalize({ defaultsOverride: lambdaConfigDefaults }); let coldStart = true; // Initialize instrumentations early to allow for require statements after our @@ -286,7 +286,7 @@ function init(event, arnInfo, _config) { // - late env variables (less likely) // - custom logger // - we always renormalize unconditionally to ensure safety. - config = coreConfig.normalize(userConfig, lambdaConfigDefaults); + config = coreConfig.normalize({ userConfig, defaultsOverride: lambdaConfigDefaults }); if (!config.tracing.enabled) { return false; diff --git a/packages/collector/src/announceCycle/agentready.js b/packages/collector/src/announceCycle/agentready.js index eac9745ac3..96f0980eb5 100644 --- a/packages/collector/src/announceCycle/agentready.js +++ b/packages/collector/src/announceCycle/agentready.js @@ -12,7 +12,7 @@ try { // Worker threads are not available, so we know that this is the main thread. } -const { tracing } = require('@instana/core'); +const { tracing, coreConfig, util } = require('@instana/core'); const agentConnection = require('../agentConnection'); const agentOpts = require('../agent/opts'); const initializedTooLate = require('../util/initializedTooLate'); @@ -130,7 +130,11 @@ function enter(_ctx) { } } - tracing.activate(agentOpts.config); + const updatedConfig = coreConfig.update({ + externalConfig: agentOpts.config, + source: util.constants.CONFIG_SOURCES.AGENT + }); + tracing.activate(updatedConfig); if (agentOpts.autoProfile && autoprofile) { profiler = autoprofile.start(); diff --git a/packages/collector/src/announceCycle/unannounced.js b/packages/collector/src/announceCycle/unannounced.js index 554575de7b..201cf7161d 100644 --- a/packages/collector/src/announceCycle/unannounced.js +++ b/packages/collector/src/announceCycle/unannounced.js @@ -305,7 +305,7 @@ function applyDisableConfiguration(agentResponse) { ensureNestedObjectExists(agentOpts.config, ['tracing', 'disable']); agentOpts.config.tracing.disable = configNormalizers.disable.normalizeExternalConfig({ tracing: { disable: disablingConfig } - }); + }).value; } module.exports = { init, diff --git a/packages/collector/src/index.js b/packages/collector/src/index.js index 05b5e3c475..cf3e557a9d 100644 --- a/packages/collector/src/index.js +++ b/packages/collector/src/index.js @@ -103,7 +103,7 @@ const instanaSharedMetrics = require('@instana/shared-metrics'); require('./tracing'); // load additional instrumentations const log = require('./logger'); -const normalizeCollectorConfig = require('./util/normalizeConfig'); +const normalizeConfig = require('./util/normalizeConfig'); const experimental = require('./experimental'); // NOTE: Default collector logger && config for cases like `preinit`. @@ -158,8 +158,11 @@ function init(userConfig = {}) { log.init(userConfig); } - config = normalizeCollectorConfig(userConfig); - config = instanaNodeJsCore.coreConfig.normalize(config); + const finalCollectorConfig = normalizeConfig(userConfig); + config = instanaNodeJsCore.coreConfig.normalize({ + userConfig, + finalConfigBase: finalCollectorConfig + }); agentConnection = require('./agentConnection'); const agentOpts = require('./agent/opts'); diff --git a/packages/collector/src/types/collector.d.ts b/packages/collector/src/types/collector.d.ts index 753327e124..cb1bf268ba 100644 --- a/packages/collector/src/types/collector.d.ts +++ b/packages/collector/src/types/collector.d.ts @@ -29,7 +29,7 @@ export interface CollectorConfig { stackTraceLength?: number; [key: string]: any; }; - autoProfile?: boolean | string; + autoProfile?: boolean; reportUnhandledPromiseRejections?: boolean; logger?: GenericLogger; level?: string | number; diff --git a/packages/collector/src/util/normalizeConfig.js b/packages/collector/src/util/normalizeConfig.js index 11e13a5254..714eebe662 100644 --- a/packages/collector/src/util/normalizeConfig.js +++ b/packages/collector/src/util/normalizeConfig.js @@ -3,10 +3,11 @@ * (c) Copyright Instana Inc. and contributors 2019 */ -/* eslint-disable dot-notation */ - 'use strict'; +const util = require('@instana/core/src/config/util'); +const validate = require('@instana/core/src/config/validator'); + const defaults = { agentHost: '127.0.0.1', agentPort: 42699, @@ -16,38 +17,105 @@ const defaults = { /** * Merges the config that was passed to the init function with environment variables and default values. - * @param {import('../types/collector').CollectorConfig} config + * @param {import('../types/collector').CollectorConfig} userConfig * @returns {import('../types/collector').CollectorConfig} */ -module.exports = function normalizeConfig(config = {}) { - config.agentHost = config.agentHost || process.env.INSTANA_AGENT_HOST || defaults.agentHost; - config.agentPort = config.agentPort || parseToPositiveInteger(process.env.INSTANA_AGENT_PORT, defaults.agentPort); - config.agentRequestTimeout = - config.agentRequestTimeout || - parseToPositiveInteger(process.env.INSTANA_AGENT_REQUEST_TIMEOUT, defaults.agentRequestTimeout); - - config.autoProfile = config.autoProfile || process.env.INSTANA_AUTO_PROFILE || defaults.autoProfile; - config.tracing = config.tracing || {}; +module.exports = function normalizeConfig(userConfig = {}) { + const finalConfig = {}; - if (config.reportUnhandledPromiseRejections == null) { - config.reportUnhandledPromiseRejections = false; - } + // NOTE: This function only normalizes collector-specific configuration fields. + // Other userConfig fields (like serviceName, tracing, etc.) are passed through as-is + // and will be normalized later by core/config when this collector config is passed + // as extraFinalConfig to core's normalize function. + finalConfig.agentHost = normalizeAgentHost(userConfig, defaults); + finalConfig.agentPort = normalizeAgentPort(userConfig, defaults); + finalConfig.agentRequestTimeout = normalizeAgentRequestTimeout(userConfig, defaults); + finalConfig.autoProfile = normalizeAutoProfile(userConfig, defaults); + finalConfig.reportUnhandledPromiseRejections = normalizeUnhandledRejections(userConfig); + finalConfig.tracing = userConfig.tracing || {}; - return config; + return finalConfig; }; /** - * @param {string | number} value - * @param {number} defaultValue + * @param {import('../types/collector').CollectorConfig} userConfig + * @param {{ agentHost: string }} defaultConfig + * @returns {string} + */ +function normalizeAgentHost(userConfig, defaultConfig) { + const { value } = util.resolve( + { + envValue: 'INSTANA_AGENT_HOST', + inCodeValue: userConfig.agentHost, + defaultValue: defaultConfig.agentHost + }, + [validate.stringValidator] + ); + return value; +} + +/** + * @param {import('../types/collector').CollectorConfig} userConfig + * @param {{ agentPort: number }} defaultConfig + * @returns {number} + */ +function normalizeAgentPort(userConfig, defaultConfig) { + const { value } = util.resolve( + { + envValue: 'INSTANA_AGENT_PORT', + inCodeValue: userConfig.agentPort, + defaultValue: defaultConfig.agentPort + }, + [validate.numberValidator] + ); + return value; +} + +/** + * @param {import('../types/collector').CollectorConfig} userConfig + * @param {{ agentRequestTimeout: number }} defaultConfig * @returns {number} */ -function parseToPositiveInteger(value, defaultValue) { - if (typeof value !== 'string') { - return defaultValue; - } - value = parseInt(value, 10); - if (!isNaN(value)) { - return Math.abs(Math.round(value)); - } - return defaultValue; +function normalizeAgentRequestTimeout(userConfig, defaultConfig) { + const { value } = util.resolve( + { + envValue: 'INSTANA_AGENT_REQUEST_TIMEOUT', + inCodeValue: userConfig.agentRequestTimeout, + defaultValue: defaultConfig.agentRequestTimeout + }, + [validate.numberValidator] + ); + return value; +} + +/** + * @param {import('../types/collector').CollectorConfig} userConfig + * @param {{ autoProfile: boolean }} defaultConfig + * @returns {boolean} + */ +function normalizeAutoProfile(userConfig, defaultConfig) { + const { value } = util.resolve( + { + envValue: 'INSTANA_AUTO_PROFILE', + inCodeValue: userConfig.autoProfile, + defaultValue: defaultConfig.autoProfile + }, + [validate.booleanValidator] + ); + return value; +} + +/** + * @param {import('../types/collector').CollectorConfig} userConfig + * @returns {boolean} + */ +function normalizeUnhandledRejections(userConfig) { + const { value } = util.resolve( + { + inCodeValue: userConfig.reportUnhandledPromiseRejections, + defaultValue: false + }, + [validate.booleanValidator] + ); + return value; } diff --git a/packages/collector/test/integration/currencies/databases/redis/test_base.js b/packages/collector/test/integration/currencies/databases/redis/test_base.js index cfd811a511..9c82e58712 100644 --- a/packages/collector/test/integration/currencies/databases/redis/test_base.js +++ b/packages/collector/test/integration/currencies/databases/redis/test_base.js @@ -21,6 +21,7 @@ const { } = require('@_local/core/test/test_util'); const ProcessControls = require('@_local/collector/test/test_util/ProcessControls'); const globalAgent = require('@_local/collector/test/globalAgent'); +const { AgentStubControls } = require('@_local/collector/test/apps/agentStubControls'); // v3 is considered the legacy version. // - It does not support Redis clustering. @@ -938,9 +939,69 @@ module.exports = function (name, version, isLatest, mode) { } }); + describe('Config precedence', () => { + describe('when both agent config and env var are set, env var takes precedence', () => { + const customAgentControls = new AgentStubControls(); + let controls; + + before(async () => { + await customAgentControls.startAgent({ + ignoreEndpoints: { redis: ['get', 'set'] } + }); + + controls = new ProcessControls({ + agentControls: customAgentControls, + dirname: __dirname, + appName: isLegacyVersion ? 'legacyApp' : 'app', + env: { + LIBRARY_LATEST: isLatest, + LIBRARY_VERSION: version, + LIBRARY_NAME: name, + REDIS_SETUP_TYPE: mode, + INSTANA_IGNORE_ENDPOINTS: 'redis:get;' + } + }); + await controls.startAndWaitForAgentConnection(5000, Date.now() + 1000 * 60 * 5); + }); + + beforeEach(async () => { + await customAgentControls.clearReceivedTraceData(); + }); + + after(async () => { + await customAgentControls.stopAgent(); + await controls.stop(); + }); + + it('should use env var config and ignore only get (not set)', async () => { + await controls + .sendRequest({ + method: 'POST', + path: '/values', + qs: { + key: 'discount', + value: 50 + } + }) + .then(async () => { + return retry(async () => { + const spans = await customAgentControls.getSpans(); + // 1 x http entry span + // 1 x http client span + // 1 x redis set span (set is NOT ignored because env var only ignores 'get') + expect(spans.length).to.equal(3); + + const redisSpans = spans.filter(span => span.n === 'redis'); + expect(redisSpans.length).to.equal(1); + expect(redisSpans[0].data.redis.command).to.equal('set'); + }); + }); + }); + }); + }); + mochaSuiteFn('ignore-endpoints:', function () { describe('when ignore-endpoints is enabled via agent configuration', () => { - const { AgentStubControls } = require('@_local/collector/test/apps/agentStubControls'); const customAgentControls = new AgentStubControls(); let controls; diff --git a/packages/collector/test/integration/currencies/logging/console/test_base.js b/packages/collector/test/integration/currencies/logging/console/test_base.js index 196807efaf..d3ff7caf5e 100644 --- a/packages/collector/test/integration/currencies/logging/console/test_base.js +++ b/packages/collector/test/integration/currencies/logging/console/test_base.js @@ -379,6 +379,57 @@ module.exports = function (name, version, isLatest) { }); }); }); + + describe('precedence: when both agent and env are configured, env takes precedence', () => { + let customAgentControls; + let precedenceControls; + + before(async () => { + customAgentControls = new AgentStubControls(); + await customAgentControls.startAgent({ + disable: { console: false } + }); + + precedenceControls = new ProcessControls({ + agentControls: customAgentControls, + dirname: __dirname, + env: { + INSTANA_TRACING_DISABLE_INSTRUMENTATIONS: ['console'], + LIBRARY_LATEST: isLatest, + LIBRARY_VERSION: version, + LIBRARY_NAME: name + } + }); + await precedenceControls.startAndWaitForAgentConnection(); + }); + + after(async () => { + await precedenceControls.stop(); + await customAgentControls.stopAgent(); + }); + + it('should not trace console.warn calls (env var takes precedence over agent config)', async () => { + await precedenceControls.sendRequest({ path: '/warn' }); + + await testUtils.retry(async () => { + const spans = await customAgentControls.getSpans(); + const httpEntrySpan = verifyHttpRootEntry({ + spans, + apiPath: '/warn', + pid: String(precedenceControls.getPid()) + }); + + verifyHttpExit({ + spans, + parent: httpEntrySpan, + pid: String(precedenceControls.getPid()) + }); + + const consoleLogSpans = testUtils.getSpansByName(spans, 'log.console'); + expect(consoleLogSpans).to.be.empty; + }); + }); + }); }); }); diff --git a/packages/collector/test/integration/currencies/messaging/kafkajs/test_base.js b/packages/collector/test/integration/currencies/messaging/kafkajs/test_base.js index 0b2fb93bfc..387e4ff740 100644 --- a/packages/collector/test/integration/currencies/messaging/kafkajs/test_base.js +++ b/packages/collector/test/integration/currencies/messaging/kafkajs/test_base.js @@ -411,6 +411,152 @@ module.exports = function (name, version, isLatest, mode) { ); }); + describe('traceCorrelation configuration precedence', function () { + describe('when agent enables traceCorrelation (overriding default)', function () { + const customAgentControls = new AgentStubControls(); + + let consumerControls; + let producerControls; + + before(async () => { + await customAgentControls.startAgent({ + kafkaConfig: { traceCorrelation: true } + }); + + consumerControls = new ProcessControls({ + dirname: __dirname, + appName: 'consumer', + agentControls: customAgentControls, + env: { ...libraryEnv } + }); + producerControls = new ProcessControls({ + dirname: __dirname, + appName: 'producer', + agentControls: customAgentControls, + env: { ...libraryEnv } + }); + + await consumerControls.startAndWaitForAgentConnection(retryTime, retryTimeUntil()); + await producerControls.startAndWaitForAgentConnection(retryTime, retryTimeUntil()); + }); + + beforeEach(async () => { + await customAgentControls.clearReceivedTraceData(); + await resetMessages(consumerControls); + }); + + afterEach(async () => { + await resetMessages(consumerControls); + }); + + after(async () => { + await customAgentControls.stopAgent(); + await producerControls.stop(); + await consumerControls.stop(); + }); + + it('must maintain trace continuity when agent config enables traceCorrelation', async () => { + await producerControls.sendRequest({ + method: 'POST', + path: '/send-messages', + simple: true, + body: JSON.stringify({ + key: 'someKey', + value: 'someMessage' + }), + headers: { + 'Content-Type': 'application/json' + } + }); + + await retry(async () => { + const messages = await getMessages(consumerControls); + checkMessages(messages, {}); + const spans = await customAgentControls.getSpans(); + const httpEntry = verifyHttpEntry(spans); + verifyKafkaExits(spans, httpEntry, {}); + verifyFollowUpHttpExit(spans, httpEntry); + }); + }); + }); + + describe('when both agent and environment variable configuration are provided', function () { + const customAgentControls = new AgentStubControls(); + + let consumerControls; + let producerControls; + + before(async () => { + await customAgentControls.startAgent({ + kafkaConfig: { traceCorrelation: true } + }); + + consumerControls = new ProcessControls({ + dirname: __dirname, + appName: 'consumer', + agentControls: customAgentControls, + env: { + ...libraryEnv, + INSTANA_KAFKA_TRACE_CORRELATION: 'false' + } + }); + producerControls = new ProcessControls({ + dirname: __dirname, + appName: 'producer', + agentControls: customAgentControls, + env: { + ...libraryEnv, + INSTANA_KAFKA_TRACE_CORRELATION: 'false' + } + }); + + await consumerControls.startAndWaitForAgentConnection(retryTime, retryTimeUntil()); + await producerControls.startAndWaitForAgentConnection(retryTime, retryTimeUntil()); + }); + + beforeEach(async () => { + await customAgentControls.clearReceivedTraceData(); + await resetMessages(consumerControls); + }); + + afterEach(async () => { + await resetMessages(consumerControls); + }); + + after(async () => { + await customAgentControls.stopAgent(); + await producerControls.stop(); + await consumerControls.stop(); + }); + + const kafkaCorrelation = 'correlation-disabled'; + + it('must not maintain trace continuity when env var disables traceCorrelation (taking precedence over agent config)', async () => { + await producerControls.sendRequest({ + method: 'POST', + path: '/send-messages', + simple: true, + body: JSON.stringify({ + key: 'someKey', + value: 'someMessage' + }), + headers: { + 'Content-Type': 'application/json' + } + }); + + await retry(async () => { + const messages = await getMessages(consumerControls); + checkMessages(messages, { kafkaCorrelation }); + const spans = await customAgentControls.getSpans(); + const httpEntry = verifyHttpEntry(spans); + verifyKafkaExits(spans, httpEntry, { kafkaCorrelation }); + verifyFollowUpHttpExit(spans, httpEntry); + }); + }); + }); + }); + describe('tracing disabled', () => { let consumerControls; let producerControls; diff --git a/packages/collector/test/integration/currencies/protocols/http/client/clientApp.js b/packages/collector/test/integration/currencies/protocols/http/client/clientApp.js index f9d34d3886..45f52f25d8 100644 --- a/packages/collector/test/integration/currencies/protocols/http/client/clientApp.js +++ b/packages/collector/test/integration/currencies/protocols/http/client/clientApp.js @@ -89,6 +89,14 @@ app.get('/request-options-only', (req, res) => { ], 'x-exit-options-not-captured-header': 'whatever' }; + } else if (req.query.withHeader === 'config-test') { + downstreamRequest.headers = { + 'X-Agent-Header-1': 'agent-value-1', + 'X-Agent-Header-2': 'agent-value-2', + 'X-Incode-Header-1': 'incode-value-1', + 'X-Incode-Header-2': 'incode-value-2', + 'X-Not-Configured-Header': 'should-not-be-captured' + }; } const requestObject = httpModule.request(downstreamRequest, () => res.sendStatus(200)); if (req.query.withHeader === 'set-on-request') { diff --git a/packages/collector/test/integration/currencies/protocols/http/client/test_base.js b/packages/collector/test/integration/currencies/protocols/http/client/test_base.js index 9f6b9cfbac..a550eb043f 100644 --- a/packages/collector/test/integration/currencies/protocols/http/client/test_base.js +++ b/packages/collector/test/integration/currencies/protocols/http/client/test_base.js @@ -38,6 +38,137 @@ module.exports = function (name, version, isLatest) { registerConnectionRefusalTest.call(this, false); registerConnectionRefusalTest.call(this, true); + describe('extraHttpHeadersToCapture configuration precedence', function () { + describe('when only agent configuration is provided', function () { + let serverControls; + let clientControls; + const customAgentControls = new AgentStubControls(); + + before(async () => { + await customAgentControls.startAgent({ + extraHeaders: ['x-agent-header-1', 'x-agent-header-2'] + }); + + serverControls = new ProcessControls({ + agentControls: customAgentControls, + dirname: __dirname, + appName: 'serverApp', + appUsesHttps: false + }); + + clientControls = new ProcessControls({ + dirname: __dirname, + appName: 'clientApp', + agentControls: customAgentControls, + appUsesHttps: false, + env: { + ...commonEnv, + SERVER_PORT: serverControls.getPort() + } + }); + + await serverControls.startAndWaitForAgentConnection(); + await clientControls.startAndWaitForAgentConnection(); + }); + + after(() => Promise.all([serverControls.stop(), clientControls.stop(), customAgentControls.stopAgent()])); + + beforeEach(() => customAgentControls.clearReceivedTraceData()); + + afterEach(() => Promise.all([serverControls.clearIpcMessages(), clientControls.clearIpcMessages()])); + + it('should capture headers configured by agent (overriding defaults)', async () => { + await clientControls.sendRequest({ + method: 'GET', + path: '/request-options-only?withHeader=config-test' + }); + + await retry(async () => { + const spans = await customAgentControls.getSpans(); + expectExactlyOneMatching(spans, [ + span => expect(span.n).to.equal('node.http.client'), + span => expect(span.k).to.equal(constants.EXIT), + span => { + expect(span.data.http.header).to.exist; + + expect(span.data.http.header['x-agent-header-1']).to.equal('agent-value-1'); + expect(span.data.http.header['x-agent-header-2']).to.equal('agent-value-2'); + + expect(span.data.http.header['x-incode-header-1']).to.be.undefined; + expect(span.data.http.header['x-incode-header-2']).to.be.undefined; + expect(span.data.http.header['x-not-configured-header']).to.be.undefined; + } + ]); + }); + }); + }); + + describe('when both agent and environment variable configuration are provided', function () { + let serverControls; + let clientControls; + const customAgentControls = new AgentStubControls(); + + before(async () => { + await customAgentControls.startAgent({ + extraHeaders: ['x-agent-header-1', 'x-agent-header-2'] + }); + + serverControls = new ProcessControls({ + agentControls: customAgentControls, + dirname: __dirname, + appName: 'serverApp', + appUsesHttps: false + }); + + clientControls = new ProcessControls({ + dirname: __dirname, + appName: 'clientApp', + agentControls: customAgentControls, + appUsesHttps: false, + env: { + ...commonEnv, + SERVER_PORT: serverControls.getPort(), + INSTANA_EXTRA_HTTP_HEADERS: 'x-incode-header-1,x-incode-header-2' + } + }); + + await serverControls.startAndWaitForAgentConnection(); + await clientControls.startAndWaitForAgentConnection(); + }); + + after(() => Promise.all([serverControls.stop(), clientControls.stop(), customAgentControls.stopAgent()])); + + beforeEach(() => customAgentControls.clearReceivedTraceData()); + + afterEach(() => Promise.all([serverControls.clearIpcMessages(), clientControls.clearIpcMessages()])); + + it('should capture headers from environment variable config (taking precedence over agent config)', async () => { + await clientControls.sendRequest({ + method: 'GET', + path: '/request-options-only?withHeader=config-test' + }); + + await retry(async () => { + const spans = await customAgentControls.getSpans(); + expectExactlyOneMatching(spans, [ + span => expect(span.n).to.equal('node.http.client'), + span => expect(span.k).to.equal(constants.EXIT), + span => { + expect(span.data.http.header).to.exist; + + expect(span.data.http.header['x-incode-header-1']).to.equal('incode-value-1'); + expect(span.data.http.header['x-incode-header-2']).to.equal('incode-value-2'); + + expect(span.data.http.header['x-agent-header-1']).to.be.undefined; + expect(span.data.http.header['x-agent-header-2']).to.be.undefined; + expect(span.data.http.header['x-not-configured-header']).to.be.undefined; + } + ]); + }); + }); + }); + }); + describe('SDK CASE 1', function () { let sdkControls; diff --git a/packages/collector/test/test_util/ProcessControls.js b/packages/collector/test/test_util/ProcessControls.js index 22e61eea71..f24135df95 100644 --- a/packages/collector/test/test_util/ProcessControls.js +++ b/packages/collector/test/test_util/ProcessControls.js @@ -151,7 +151,6 @@ class ProcessControls { APP_CWD: this.cwd, INSTANA_AGENT_PORT: agentPort, INSTANA_LOG_LEVEL: 'warn', - INSTANA_TRACING_DISABLE: !this.tracingEnabled, INSTANA_FORCE_TRANSMISSION_STARTING_AT: '1', INSTANA_FULL_METRICS_INTERNAL_IN_S: 1, INSTANA_FIRE_MONITORING_EVENT_DURATION_IN_MS: 500, @@ -164,6 +163,13 @@ class ProcessControls { opts.env ); + // Only set INSTANA_TRACING_DISABLE when tracing is actually disabled to avoid + // overriding other disable environment variables (INSTANA_TRACING_DISABLE_INSTRUMENTATIONS, etc.) + // See packages/core/src/config/configNormalizers/disable.js for precedence rules + if (!this.tracingEnabled) { + this.env.INSTANA_TRACING_DISABLE = 'true'; + } + if (this.usePreInit) { this.env.INSTANA_EARLY_INSTRUMENTATION = 'true'; } diff --git a/packages/collector/test/unit/src/announceCycle/agentready.test.js b/packages/collector/test/unit/src/announceCycle/agentready.test.js index 4ecf3f79de..1bafc7fe17 100644 --- a/packages/collector/test/unit/src/announceCycle/agentready.test.js +++ b/packages/collector/test/unit/src/announceCycle/agentready.test.js @@ -35,6 +35,9 @@ describe('agent ready state', () => { let uncaughtStub; let eolStub; beforeEach(() => { + const coreConfig = require('@_local/core/src/config'); + coreConfig.normalize({ finalConfigBase: {} }); + agentOptsStub = { config: {}, agentUuid: 'test-uuid' @@ -90,7 +93,8 @@ describe('agent ready state', () => { it('should forward agent config to tracing component', () => { agentOptsStub.config = { foo: { bar: 'baz' } }; agentReadyState.enter(); - expect(tracingStub.activate).to.have.been.calledWith(agentOptsStub.config); + const updatedConfig = tracingStub.activate.firstCall.args[0]; + expect(updatedConfig.foo.bar).to.equal('baz'); }); }); diff --git a/packages/collector/test/unit/src/util/normalizeConfig.test.js b/packages/collector/test/unit/src/util/normalizeConfig.test.js index 2d923c65e6..dcc5254e47 100644 --- a/packages/collector/test/unit/src/util/normalizeConfig.test.js +++ b/packages/collector/test/unit/src/util/normalizeConfig.test.js @@ -6,70 +6,197 @@ 'use strict'; const expect = require('chai').expect; - +const testUtils = require('@_local/core/test/test_util'); +const coreConfig = require('@instana/core/src/config'); const normalizeConfig = require('@_local/collector/src/util/normalizeConfig'); describe('util.normalizeConfig', () => { + before(() => { + coreConfig.init(testUtils.createFakeLogger()); + }); + beforeEach(resetEnv); afterEach(resetEnv); function resetEnv() { delete process.env.INSTANA_AGENT_HOST; delete process.env.INSTANA_AGENT_PORT; + delete process.env.INSTANA_AGENT_REQUEST_TIMEOUT; + delete process.env.INSTANA_AUTO_PROFILE; } - it('should apply all defaults', () => { - checkDefaults(normalizeConfig()); - checkDefaults(normalizeConfig({})); - checkDefaults(normalizeConfig({ unknowConfigOption: 13 })); + describe('defaults', () => { + it('should apply all defaults when nothing is provided', () => { + const config = normalizeConfig(); + + expect(config.agentHost).to.equal('127.0.0.1'); + expect(config.agentPort).to.equal(42699); + expect(config.agentRequestTimeout).to.equal(5000); + expect(config.autoProfile).to.equal(false); + expect(config.tracing).to.be.an('object'); + expect(config.reportUnhandledPromiseRejections).to.be.false; + }); }); - it('should accept custom agent connection configuration', () => { - const config = normalizeConfig({ - agentHost: 'LOKAL_HORST', - agentPort: 1207 + describe('agentHost', () => { + it('should use env over config over default', () => { + process.env.INSTANA_AGENT_HOST = 'env-host'; + + const config = normalizeConfig({ + agentHost: 'config-host' + }); + + expect(config.agentHost).to.equal('env-host'); + }); + + it('should use config when env is not set', () => { + const config = normalizeConfig({ + agentHost: 'config-host' + }); + + expect(config.agentHost).to.equal('config-host'); + }); + + it('should fallback to default', () => { + const config = normalizeConfig({}); + + expect(config.agentHost).to.equal('127.0.0.1'); }); - expect(config.agentHost).to.equal('LOKAL_HORST'); - expect(config.agentPort).to.equal(1207); }); - it('should accept custom agent connection configuration from environment', () => { - process.env.INSTANA_AGENT_HOST = 'yadayada'; - process.env.INSTANA_AGENT_PORT = '1357'; - const config = normalizeConfig(); - expect(config.agentHost).to.equal('yadayada'); - expect(config.agentPort).to.equal(1357); - expect(config.agentPort).to.be.a('number'); + describe('agentPort', () => { + it('should use env over config over default', () => { + process.env.INSTANA_AGENT_PORT = '9999'; + + const config = normalizeConfig({ + agentPort: 1234 + }); + + expect(config.agentPort).to.equal(9999); + }); + + it('should use config when env is not set', () => { + const config = normalizeConfig({ + agentPort: 1234 + }); + + expect(config.agentPort).to.equal(1234); + }); + + it('should fallback to default', () => { + const config = normalizeConfig({}); + + expect(config.agentPort).to.equal(42699); + }); + + it('should fallback to default for invalid env value', () => { + process.env.INSTANA_AGENT_PORT = 'invalid'; + + const config = normalizeConfig(); + + expect(config.agentPort).to.equal(42699); + }); }); - it('should custom stack trace length', () => { - const config = normalizeConfig({ - tracing: { - stackTraceLength: 7 - } + describe('agentRequestTimeout', () => { + it('should use env over config over default', () => { + process.env.INSTANA_AGENT_REQUEST_TIMEOUT = '8000'; + + const config = normalizeConfig({ + agentRequestTimeout: 2000 + }); + + expect(config.agentRequestTimeout).to.equal(8000); + }); + + it('should use config when env is not set', () => { + const config = normalizeConfig({ + agentRequestTimeout: 2000 + }); + + expect(config.agentRequestTimeout).to.equal(2000); + }); + + it('should fallback to default', () => { + const config = normalizeConfig({}); + + expect(config.agentRequestTimeout).to.equal(5000); + }); + + it('should fallback to default for invalid env value', () => { + process.env.INSTANA_AGENT_REQUEST_TIMEOUT = 'abc'; + + const config = normalizeConfig(); + + expect(config.agentRequestTimeout).to.equal(5000); }); - expect(config.tracing.stackTraceLength).to.equal(7); }); - it('should disable unhandled promises', () => { - const config = normalizeConfig({ - reportUnhandledPromiseRejections: false + describe('autoProfile', () => { + it('should use env over config over default', () => { + process.env.INSTANA_AUTO_PROFILE = 'true'; + + const config = normalizeConfig({ + autoProfile: false + }); + + expect(config.autoProfile).to.equal(true); + }); + + it('should use config when env is not set', () => { + const config = normalizeConfig({ + autoProfile: true + }); + + expect(config.autoProfile).to.equal(true); + }); + + it('should fallback to default', () => { + const config = normalizeConfig({}); + + expect(config.autoProfile).to.equal(false); }); - expect(config.reportUnhandledPromiseRejections).to.be.false; }); - it('should enable unhandled promises', () => { - const config = normalizeConfig({ - reportUnhandledPromiseRejections: true + describe('tracing', () => { + it('should initialize tracing object if missing', () => { + const config = normalizeConfig(); + + expect(config.tracing).to.be.an('object'); + }); + + it('should preserve provided tracing config', () => { + const config = normalizeConfig({ + tracing: { + stackTraceLength: 7 + } + }); + + expect(config.tracing.stackTraceLength).to.equal(7); }); - expect(config.reportUnhandledPromiseRejections).to.be.true; }); - function checkDefaults(config) { - expect(config).to.be.an('object'); - expect(config.agentHost).to.equal('127.0.0.1'); - expect(config.agentPort).to.equal(42699); - expect(config.tracing).to.be.an('object'); - expect(config.reportUnhandledPromiseRejections).to.be.false; - } + describe('reportUnhandledPromiseRejections', () => { + it('should default to false', () => { + const config = normalizeConfig(); + + expect(config.reportUnhandledPromiseRejections).to.be.false; + }); + + it('should allow explicit false', () => { + const config = normalizeConfig({ + reportUnhandledPromiseRejections: false + }); + + expect(config.reportUnhandledPromiseRejections).to.be.false; + }); + + it('should allow explicit true', () => { + const config = normalizeConfig({ + reportUnhandledPromiseRejections: true + }); + + expect(config.reportUnhandledPromiseRejections).to.be.true; + }); + }); }); diff --git a/packages/core/src/config/configNormalizers/disable.js b/packages/core/src/config/configNormalizers/disable.js index 0c3477f2b2..538cb0381e 100644 --- a/packages/core/src/config/configNormalizers/disable.js +++ b/packages/core/src/config/configNormalizers/disable.js @@ -5,6 +5,7 @@ 'use strict'; const { DISABLABLE_INSTRUMENTATION_GROUPS } = require('../../tracing/constants'); +const { CONFIG_SOURCES } = require('../../util/constants'); /** @type {import('../../core').GenericLogger} */ let logger; @@ -19,33 +20,53 @@ exports.init = function init(_config) { * Handles environment variables, and array inputs. * * Precedence order (highest to lowest): - * 1. `tracing.disable` - * 2. Environment variables (`INSTANA_TRACING_DISABLE*`) + * 1. Environment variables (`INSTANA_TRACING_DISABLE*`) + * 2. In-code tracing.disable * * @param {import('../../config').InstanaConfig} config */ exports.normalize = function normalize(config) { if (!config?.tracing) config.tracing = {}; try { - // Disable all tracing if explicitly set 'disable' to true + const envDisableConfig = getDisableFromEnv(); + + if (envDisableConfig !== null) { + if (envDisableConfig === true) { + return { value: true, source: CONFIG_SOURCES.ENV }; + } + + if (envDisableConfig === false) { + return { value: {}, source: CONFIG_SOURCES.ENV }; + } + + if (envDisableConfig.instrumentations?.length || envDisableConfig.groups?.length) { + logger?.debug(`[config] env:INSTANA_TRACING_DISABLE* = ${JSON.stringify(envDisableConfig)}`); + + if (envDisableConfig.instrumentations) { + envDisableConfig.instrumentations = normalizeArray(envDisableConfig.instrumentations); + } + if (envDisableConfig.groups) { + envDisableConfig.groups = normalizeArray(envDisableConfig.groups); + } + + return { value: envDisableConfig, source: CONFIG_SOURCES.ENV }; + } + } + if (config.tracing.disable === true) { - logger?.info('Tracing has been disabled via "tracing.disable: true" configuration.'); - return true; + logger?.debug('[config] incode:tracing.disable = true'); + return { value: true, source: CONFIG_SOURCES.INCODE }; } + const hasDisableConfig = isDisableConfigNonEmpty(config); if (hasDisableConfig) { - logger?.info( - `Tracing selectively disabled as per "tracing.disable" configuration: ${JSON.stringify(config.tracing.disable)}` - ); + logger?.debug(`[config] incode:tracing.disable = ${JSON.stringify(config.tracing.disable)}`); } - // Fallback to environment variables if `disable` is not explicitly configured - const disableConfig = isDisableConfigNonEmpty(config) ? config.tracing.disable : getDisableFromEnv(); + const disableConfig = isDisableConfigNonEmpty(config) ? config.tracing.disable : null; - if (!disableConfig) return {}; - - if (disableConfig === true) return true; + if (!disableConfig) return { value: {}, source: CONFIG_SOURCES.DEFAULT }; // Normalize instrumentations and groups if (disableConfig?.instrumentations) { @@ -57,14 +78,14 @@ exports.normalize = function normalize(config) { // Handle if tracing.disable is an array if (Array.isArray(disableConfig)) { - return categorizeDisableEntries(disableConfig); + return { value: categorizeDisableEntries(disableConfig), source: CONFIG_SOURCES.INCODE }; } - return disableConfig || {}; + return { value: disableConfig || {}, source: CONFIG_SOURCES.INCODE }; } catch (error) { // Fallback to an empty disable config on error logger?.debug(`Error while normalizing tracing.disable config: ${error?.message} ${error?.stack}`); - return {}; + return { value: {}, source: CONFIG_SOURCES.DEFAULT }; } }; @@ -76,13 +97,13 @@ exports.normalizeExternalConfig = function normalizeExternalConfig(config) { try { if (isNonEmptyObject(config.tracing.disable)) { const flattenedEntries = flattenDisableConfigs(config.tracing.disable); - return categorizeDisableEntries(flattenedEntries); + return { value: categorizeDisableEntries(flattenedEntries), source: CONFIG_SOURCES.AGENT }; } } catch (error) { logger?.debug(`Error while normalizing external tracing.disable config: ${error?.message} ${error?.stack}`); } - return {}; + return { value: {}, source: CONFIG_SOURCES.DEFAULT }; }; /** @@ -91,7 +112,7 @@ exports.normalizeExternalConfig = function normalizeExternalConfig(config) { * 2. INSTANA_TRACING_DISABLE_INSTRUMENTATIONS / INSTANA_TRACING_DISABLE_GROUPS * 3. INSTANA_TRACING_DISABLE=list * - * @returns {import('../../config/types').Disable} + * @returns {import('../../config/types').Disable | boolean | null} */ function getDisableFromEnv() { const disable = {}; @@ -101,11 +122,16 @@ function getDisableFromEnv() { const envVarValue = process.env.INSTANA_TRACING_DISABLE; if (envVarValue === 'true') { - logger?.info('Tracing has been disabled via environment variable "INSTANA_TRACING_DISABLE=true".'); + logger?.debug('[config] env:INSTANA_TRACING_DISABLE = true'); return true; } - if (envVarValue !== 'false' && envVarValue !== '') { + if (envVarValue === 'false') { + logger?.debug('[config] env:INSTANA_TRACING_DISABLE = false'); + return false; + } + + if (envVarValue !== '') { const categorized = categorizeDisableEntries(parseEnvVar(envVarValue)); if (categorized?.instrumentations?.length) { disable.instrumentations = categorized.instrumentations; @@ -114,24 +140,20 @@ function getDisableFromEnv() { disable.groups = categorized.groups; } - logger?.info(`Tracing has been disabled via "INSTANA_TRACING_DISABLE=${envVarValue}"`); + logger?.debug(`[config] env:INSTANA_TRACING_DISABLE = ${envVarValue}`); } } if (process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS) { disable.instrumentations = parseEnvVar(process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS); - logger?.info( - `Tracing instrumentations disabled via "INSTANA_TRACING_DISABLE_INSTRUMENTATIONS": ${JSON.stringify( - disable.instrumentations - )}` + logger?.debug( + `[config] env:INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = ${process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS}` ); } if (process.env.INSTANA_TRACING_DISABLE_GROUPS) { disable.groups = parseEnvVar(process.env.INSTANA_TRACING_DISABLE_GROUPS); - logger?.info( - `Tracing instrumentation groups disabled via "INSTANA_TRACING_DISABLE_GROUPS": ${JSON.stringify(disable.groups)}` - ); + logger?.debug(`[config] env:INSTANA_TRACING_DISABLE_GROUPS = ${process.env.INSTANA_TRACING_DISABLE_GROUPS}`); } return Object.keys(disable).length > 0 ? disable : null; diff --git a/packages/core/src/config/configNormalizers/stackTrace.js b/packages/core/src/config/configNormalizers/stackTrace.js index 2a0bb89ef2..9b0a596b04 100644 --- a/packages/core/src/config/configNormalizers/stackTrace.js +++ b/packages/core/src/config/configNormalizers/stackTrace.js @@ -25,7 +25,7 @@ exports.normalizeStackTraceMode = function (config) { /** * Normalizes stack trace length configuration based on precedence. - * Precedence: global config > config > env var > default + * Precedence: env var > global config > config > default * @param {import('../../config').InstanaConfig} config * @returns {number} - Normalized value */ diff --git a/packages/core/src/config/index.js b/packages/core/src/config/index.js index dde9e8e041..f9503f6cf6 100644 --- a/packages/core/src/config/index.js +++ b/packages/core/src/config/index.js @@ -3,16 +3,53 @@ * (c) Copyright Instana Inc. and contributors 2019 */ -/* eslint-disable */ - 'use strict'; -const supportedTracingVersion = require('../tracing/supportedVersion'); const configNormalizers = require('./configNormalizers'); const configValidators = require('./configValidators'); const deepMerge = require('../util/deepMerge'); -const { DEFAULT_STACK_TRACE_LENGTH, DEFAULT_STACK_TRACE_MODE } = require('../util/constants'); +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 @@ -141,311 +178,345 @@ module.exports.configValidators = configValidators; 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 {InstanaConfig} [userConfig] - * @param {InstanaConfig} [defaultsOverride] + * @param {{ userConfig?: InstanaConfig, finalConfigBase?: Object, defaultsOverride?: InstanaConfig }} [options] * @returns {InstanaConfig} */ -module.exports.normalize = (userConfig, defaultsOverride = {}) => { - if (defaultsOverride && typeof defaultsOverride === 'object') { +module.exports.normalize = ({ userConfig = {}, finalConfigBase = {}, defaultsOverride = {} } = {}) => { + if (defaultsOverride && typeof defaultsOverride === 'object' && Object.keys(defaultsOverride).length > 0) { defaults = deepMerge(defaults, defaultsOverride); } - /** @type InstanaConfig */ - let targetConfig = {}; + let normalizedUserConfig; - // NOTE: Do not modify the original object - if (userConfig !== null) { - targetConfig = Object.assign({}, userConfig); + // 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. - targetConfig.logger = logger; - - normalizeServiceName(targetConfig); - normalizePackageJsonPath(targetConfig); - normalizeMetricsConfig(targetConfig); - normalizeTracingConfig(targetConfig); - normalizeSecrets(targetConfig); - normalizePreloadOpentelemetry(targetConfig); - return targetConfig; + 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 {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeServiceName(config) { - if (config.serviceName == null && process.env['INSTANA_SERVICE_NAME']) { - config.serviceName = process.env['INSTANA_SERVICE_NAME']; - } - if (config.serviceName != null && typeof config.serviceName !== 'string') { - logger.warn( - `Invalid configuration: config.serviceName is not a string, the value will be ignored: ${config.serviceName}` - ); - config.serviceName = defaults.serviceName; - } +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; } /** - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizePackageJsonPath(config) { - if (config.packageJsonPath == null && process.env['INSTANA_PACKAGE_JSON_PATH']) { - config.packageJsonPath = process.env['INSTANA_PACKAGE_JSON_PATH']; - } - if (config.packageJsonPath != null && typeof config.packageJsonPath !== 'string') { - logger.warn( - `Invalid configuration: config.packageJsonPath is not a string, the value will be ignored: ${config.packageJsonPath}` - ); +function normalizePackageJsonPath({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + const { value, source } = util.resolve( + { + envValue: 'INSTANA_PACKAGE_JSON_PATH', + inCodeValue: userConfig.packageJsonPath, + defaultValue: defaultConfig.packageJsonPath + }, + [validate.stringValidator] + ); - config.packageJsonPath = null; - } + configStore.set('config.packageJsonPath', { source }); + finalConfig.packageJsonPath = value; } /** - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeMetricsConfig(config) { - if (config.metrics == null) { - config.metrics = {}; - } +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] + ); - config.metrics.transmissionDelay = normalizeSingleValue( - config.metrics.transmissionDelay, - defaults.metrics.transmissionDelay, - 'config.metrics.transmissionDelay', - 'INSTANA_METRICS_TRANSMISSION_DELAY' + finalConfig.metrics.transmissionDelay = transmissionDelay; + configStore.set('config.metrics.transmissionDelay', { source: transmissionDelaySource }); + + const { value: healthcheckInterval, source: healthcheckSource } = util.resolve( + { + inCodeValue: userMetrics?.timeBetweenHealthcheckCalls, + defaultValue: defaultConfig.metrics.timeBetweenHealthcheckCalls + }, + [validate.numberValidator] ); - config.metrics.timeBetweenHealthcheckCalls = - config.metrics.timeBetweenHealthcheckCalls || defaults.metrics.timeBetweenHealthcheckCalls; + finalConfig.metrics.timeBetweenHealthcheckCalls = healthcheckInterval; + configStore.set('config.metrics.timeBetweenHealthcheckCalls', { + source: healthcheckSource + }); } /** - * - * @param {InstanaConfig} config - */ -function normalizeTracingConfig(config) { - if (config.tracing == null) { - config.tracing = {}; - } - normalizeTracingEnabled(config); - normalizeUseOpentelemetry(config); - normalizeDisableTracing(config); - normalizeAutomaticTracingEnabled(config); - normalizeActivateImmediately(config); - normalizeTracingTransmission(config); - normalizeTracingHttp(config); - normalizeTracingStackTrace(config); - normalizeSpanBatchingEnabled(config); - normalizeDisableW3cTraceCorrelation(config); - normalizeTracingKafka(config); - normalizeAllowRootExitSpan(config); - normalizeIgnoreEndpoints(config); - normalizeIgnoreEndpointsDisableSuppression(config); - normalizeDisableEOLEvents(config); + * @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 }); } /** - * - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeTracingEnabled(config) { - if (config.tracing.enabled === false) { - logger.info('Not enabling tracing as it is explicitly disabled via config.'); - return; - } - if (config.tracing.enabled === true) { - return; - } +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; - config.tracing.enabled = defaults.tracing.enabled; + configStore.set('config.tracing.enabled', { source }); + finalConfig.tracing.enabled = finalValue; } /** - * - * @param {InstanaConfig} config + * @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] + ); -function normalizeAllowRootExitSpan(config) { - if (config.tracing.allowRootExitSpan === false) { - return; - } - if (config.tracing.allowRootExitSpan === true) { - return; - } - - const INSTANA_ALLOW_ROOT_EXIT_SPAN = process.env['INSTANA_ALLOW_ROOT_EXIT_SPAN']?.toLowerCase(); - - config.tracing.allowRootExitSpan = - INSTANA_ALLOW_ROOT_EXIT_SPAN === '1' || - INSTANA_ALLOW_ROOT_EXIT_SPAN === 'true' || - defaults.tracing.allowRootExitSpan; - return; + configStore.set('config.tracing.allowRootExitSpan', { source }); + finalConfig.tracing.allowRootExitSpan = value; } /** - * - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeUseOpentelemetry(config) { - if (config.tracing.useOpentelemetry === false) { - return; - } - if (config.tracing.useOpentelemetry === true) { - return; - } - if (process.env['INSTANA_DISABLE_USE_OPENTELEMETRY'] === 'true') { - config.tracing.useOpentelemetry = false; - return; - } +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; - config.tracing.useOpentelemetry = defaults.tracing.useOpentelemetry; + configStore.set('config.tracing.useOpentelemetry', { source }); + finalConfig.tracing.useOpentelemetry = finalValue; } /** - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeAutomaticTracingEnabled(config) { - if (!config.tracing.enabled) { - logger.info('Not enabling automatic tracing as tracing in general is explicitly disabled via config.'); - config.tracing.automaticTracingEnabled = false; +function normalizeAutomaticTracingEnabled({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + if (!finalConfig.tracing.enabled) { + finalConfig.tracing.automaticTracingEnabled = false; return; } - if (config.tracing.automaticTracingEnabled === false) { - logger.info('Not enabling automatic tracing as it is explicitly disabled via config.'); - config.tracing.automaticTracingEnabled = false; - return; - } - - if (process.env['INSTANA_DISABLE_AUTO_INSTR'] === 'true') { - logger.info( - 'Not enabling automatic tracing as it is explicitly disabled via environment variable INSTANA_DISABLE_AUTO_INSTR.' - ); - config.tracing.automaticTracingEnabled = false; - return; - } + const { value, source } = util.resolve( + { + envValue: 'INSTANA_DISABLE_AUTO_INSTR', + inCodeValue: userConfig.tracing.automaticTracingEnabled, + defaultValue: defaultConfig.tracing.automaticTracingEnabled + }, + [validate.booleanValidator] + ); - if (!supportedTracingVersion(process.versions.node)) { - logger.warn( - 'Not enabling automatic tracing, this is an unsupported version of Node.js. ' + - 'See: https://www.ibm.com/docs/en/instana-observability/current?topic=nodejs-support-information#supported-nodejs-versions' - ); - config.tracing.automaticTracingEnabled = false; - return; - } + // 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; - config.tracing.automaticTracingEnabled = defaults.tracing.automaticTracingEnabled; + configStore.set('config.tracing.automaticTracingEnabled', { source }); + finalConfig.tracing.automaticTracingEnabled = finalValue; } /** - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeActivateImmediately(config) { - if (!config.tracing.enabled) { - config.tracing.activateImmediately = false; +function normalizeActivateImmediately({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + if (!finalConfig.tracing.enabled) { + finalConfig.tracing.activateImmediately = false; return; } - if (typeof config.tracing.activateImmediately === 'boolean') { - return; - } - - if (process.env['INSTANA_TRACE_IMMEDIATELY'] === 'true') { - config.tracing.activateImmediately = true; - return; - } + const { value, source } = util.resolve( + { + envValue: 'INSTANA_TRACE_IMMEDIATELY', + inCodeValue: userConfig.tracing.activateImmediately, + defaultValue: defaultConfig.tracing.activateImmediately + }, + [validate.booleanValidator] + ); - config.tracing.activateImmediately = defaults.tracing.activateImmediately; + configStore.set('config.tracing.activateImmediately', { source }); + finalConfig.tracing.activateImmediately = value; } /** - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeTracingTransmission(config) { - config.tracing.maxBufferedSpans = config.tracing.maxBufferedSpans || defaults.tracing.maxBufferedSpans; +function normalizeTracingTransmission({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + finalConfig.tracing.maxBufferedSpans = userConfig.tracing.maxBufferedSpans ?? defaultConfig.tracing.maxBufferedSpans; + + configStore.set('config.tracing.maxBufferedSpans', { + source: userConfig.tracing.maxBufferedSpans !== undefined ? CONFIG_SOURCES.INCODE : CONFIG_SOURCES.DEFAULT + }); - config.tracing.transmissionDelay = normalizeSingleValue( - config.tracing.transmissionDelay, - defaults.tracing.transmissionDelay, - 'config.tracing.transmissionDelay', - 'INSTANA_TRACING_TRANSMISSION_DELAY' + const { value: tracingTransmissionDelay, source: tracingTransmissionDelaySource } = util.resolve( + { + envValue: 'INSTANA_TRACING_TRANSMISSION_DELAY', + inCodeValue: userConfig.tracing.transmissionDelay, + defaultValue: defaultConfig.tracing.transmissionDelay + }, + [validate.numberValidator] ); - // DEPRECATED! This was never documented, but we shared it with a customer. - if (process.env['INSTANA_DEV_MIN_DELAY_BEFORE_SENDING_SPANS']) { - logger.warn( - 'The environment variable INSTANA_DEV_MIN_DELAY_BEFORE_SENDING_SPANS is deprecated and will be removed in the next major release. ' + - 'Please use INSTANA_TRACING_TRANSMISSION_DELAY instead.' - ); + configStore.set('config.tracing.transmissionDelay', { source: tracingTransmissionDelaySource }); + finalConfig.tracing.transmissionDelay = tracingTransmissionDelay; - config.tracing.transmissionDelay = parseInt(process.env['INSTANA_DEV_MIN_DELAY_BEFORE_SENDING_SPANS'], 10); + const { value: forceTransmissionStartingAt, source: forceTransmissionStartingAtSource } = util.resolve( + { + envValue: 'INSTANA_FORCE_TRANSMISSION_STARTING_AT', + inCodeValue: userConfig.tracing.forceTransmissionStartingAt, + defaultValue: defaultConfig.tracing.forceTransmissionStartingAt + }, + [validate.numberValidator] + ); - if (isNaN(config.tracing.transmissionDelay)) { - logger.warn( - `The value of INSTANA_DEV_MIN_DELAY_BEFORE_SENDING_SPANS is not a number. Falling back to the default value ${defaults.tracing.transmissionDelay}.` - ); + configStore.set('config.tracing.forceTransmissionStartingAt', { source: forceTransmissionStartingAtSource }); + finalConfig.tracing.forceTransmissionStartingAt = forceTransmissionStartingAt; - config.tracing.transmissionDelay = defaults.tracing.transmissionDelay; - } - } - - config.tracing.forceTransmissionStartingAt = normalizeSingleValue( - config.tracing.forceTransmissionStartingAt, - defaults.tracing.forceTransmissionStartingAt, - 'config.tracing.forceTransmissionStartingAt', - '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] ); - config.tracing.initialTransmissionDelay = normalizeSingleValue( - config.tracing.initialTransmissionDelay, - defaults.tracing.initialTransmissionDelay, - 'config.tracing.initialTransmissionDelay', - 'INSTANA_TRACING_INITIAL_TRANSMISSION_DELAY' - ); + configStore.set('config.tracing.initialTransmissionDelay', { source: initialTransmissionDelaySource }); + finalConfig.tracing.initialTransmissionDelay = initialTransmissionDelay; } /** - * @param {InstanaConfig} config + * 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(config) { - config.tracing.http = config.tracing.http || {}; +function normalizeTracingHttp({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + const userHttp = userConfig.tracing.http; + finalConfig.tracing.http = {}; + + const userHeaders = userHttp?.extraHttpHeadersToCapture; - let fromEnvVar; + // 1. Check environment variable if (process.env.INSTANA_EXTRA_HTTP_HEADERS) { - fromEnvVar = parseHeadersEnvVar(process.env.INSTANA_EXTRA_HTTP_HEADERS); - } + const fromEnvVar = parseHeadersEnvVar(process.env.INSTANA_EXTRA_HTTP_HEADERS); + finalConfig.tracing.http.extraHttpHeadersToCapture = fromEnvVar; - if (!config.tracing.http.extraHttpHeadersToCapture && !fromEnvVar) { - config.tracing.http.extraHttpHeadersToCapture = defaults.tracing.http.extraHttpHeadersToCapture; + configStore.set('config.tracing.http.extraHttpHeadersToCapture', { source: CONFIG_SOURCES.ENV }); return; - } else if (!config.tracing.http.extraHttpHeadersToCapture && fromEnvVar) { - config.tracing.http.extraHttpHeadersToCapture = fromEnvVar; } - if (!Array.isArray(config.tracing.http.extraHttpHeadersToCapture)) { - logger.warn( - `Invalid configuration: config.tracing.http.extraHttpHeadersToCapture is not an array, the value will be ignored: ${JSON.stringify( - config.tracing.http.extraHttpHeadersToCapture - )}` - ); - config.tracing.http.extraHttpHeadersToCapture = defaults.tracing.http.extraHttpHeadersToCapture; - 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 }); + logger.debug('[config] incode:config.tracing.http.extraHttpHeadersToCapture'); + return; + } } - config.tracing.http.extraHttpHeadersToCapture = config.tracing.http.extraHttpHeadersToCapture.map( - ( - s // Node.js HTTP API turns all incoming HTTP headers into lowercase. - ) => s.toLowerCase() - ); + // 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} @@ -453,72 +524,94 @@ function normalizeTracingHttp(config) { function parseHeadersEnvVar(envVarValue) { return envVarValue .split(/[;,]/) - .map(header => header.trim()) + .map(header => header.trim().toLowerCase()) .filter(header => header !== ''); } /** * Handles both stackTrace and stackTraceLength configuration - * @param {InstanaConfig} config + * + * 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(config) { - const tracing = config.tracing; +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']; + 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) { - tracing.stackTrace = normalized; + finalConfig.tracing.stackTrace = normalized; + configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.ENV }); } else { - tracing.stackTrace = defaults.tracing.stackTrace; + 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}`); - tracing.stackTrace = defaults.tracing.stackTrace; + finalConfig.tracing.stackTrace = defaultConfig.tracing.stackTrace; + configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.DEFAULT }); } - } else if (tracing.global?.stackTrace !== undefined) { - const result = validateStackTraceMode(tracing.global.stackTrace); + } else if (userGlobal?.stackTrace !== undefined) { + // Priority 2: In-code configuration + const result = validateStackTraceMode(userGlobal.stackTrace); if (result.isValid) { - const normalized = configNormalizers.stackTrace.normalizeStackTraceMode(config); + const normalized = configNormalizers.stackTrace.normalizeStackTraceMode(userConfig); if (normalized !== null) { - tracing.stackTrace = normalized; + finalConfig.tracing.stackTrace = normalized; + configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.INCODE }); } else { - tracing.stackTrace = defaults.tracing.stackTrace; + 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}`); - tracing.stackTrace = defaults.tracing.stackTrace; + finalConfig.tracing.stackTrace = defaultConfig.tracing.stackTrace; + configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.DEFAULT }); } } else { - tracing.stackTrace = defaults.tracing.stackTrace; + finalConfig.tracing.stackTrace = defaultConfig.tracing.stackTrace; + configStore.set('config.tracing.stackTrace', { source: CONFIG_SOURCES.DEFAULT }); } - const isLegacyLengthDefined = tracing.stackTraceLength !== undefined; - const stackTraceConfigValue = tracing.global?.stackTraceLength || tracing.stackTraceLength; + 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) { - tracing.stackTraceLength = normalized; + finalConfig.tracing.stackTraceLength = normalized; + configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.ENV }); } else { - tracing.stackTraceLength = defaults.tracing.stackTraceLength; + 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}`); - tracing.stackTraceLength = defaults.tracing.stackTraceLength; + 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.' ); @@ -527,115 +620,123 @@ function normalizeTracingStackTrace(config) { const result = validateStackTraceLength(stackTraceConfigValue); if (result.isValid) { - const normalized = configNormalizers.stackTrace.normalizeStackTraceLength(config); + const normalized = configNormalizers.stackTrace.normalizeStackTraceLength(userConfig); if (normalized !== null) { - tracing.stackTraceLength = normalized; + finalConfig.tracing.stackTraceLength = normalized; + configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.INCODE }); } else { - tracing.stackTraceLength = defaults.tracing.stackTraceLength; + finalConfig.tracing.stackTraceLength = defaultConfig.tracing.stackTraceLength; + configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.DEFAULT }); } } else { logger.warn(`Invalid stackTraceLength value: ${result.error}`); - tracing.stackTraceLength = defaults.tracing.stackTraceLength; + finalConfig.tracing.stackTraceLength = defaultConfig.tracing.stackTraceLength; + configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.DEFAULT }); } } else { - tracing.stackTraceLength = defaults.tracing.stackTraceLength; + finalConfig.tracing.stackTraceLength = defaultConfig.tracing.stackTraceLength; + configStore.set('config.tracing.stackTraceLength', { source: CONFIG_SOURCES.DEFAULT }); } } /** - * @param {InstanaConfig} config + * 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(config) { - const disableConfig = configNormalizers.disable.normalize(config); +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) { - config.tracing.enabled = false; - config.tracing.disable = {}; + finalConfig.tracing.enabled = false; + finalConfig.tracing.disable = {}; + configStore.set('config.tracing.disable', { + source: CONFIG_SOURCES.DEFAULT + }); return; } - if (typeof disableConfig === 'object' && (disableConfig.instrumentations?.length || disableConfig.groups?.length)) { - config.tracing.disable = disableConfig; + if (typeof disableConfig === 'object' && disableRes.source !== CONFIG_SOURCES.DEFAULT) { + finalConfig.tracing.disable = disableConfig; + configStore.set('config.tracing.disable', { + source: disableRes.source + }); return; } - config.tracing.disable = defaults.tracing.disable; + finalConfig.tracing.disable = defaultConfig.tracing.disable; + configStore.set('config.tracing.disable', { source: CONFIG_SOURCES.DEFAULT }); } /** - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeSpanBatchingEnabled(config) { - if (config.tracing.spanBatchingEnabled != null) { - if (typeof config.tracing.spanBatchingEnabled === 'boolean') { - if (config.tracing.spanBatchingEnabled) { - logger.info('Span batching is enabled via config.'); - } - return; - } else { - logger.warn( - `Invalid configuration: config.tracing.spanBatchingEnabled is not a boolean value, will be ignored: ${JSON.stringify( - config.tracing.spanBatchingEnabled - )}` - ); - } - } - - if (process.env['INSTANA_SPANBATCHING_ENABLED'] === 'true') { - logger.info('Span batching is enabled via environment variable INSTANA_SPANBATCHING_ENABLED.'); - config.tracing.spanBatchingEnabled = true; - return; - } +function normalizeSpanBatchingEnabled({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + const { value, source } = util.resolve( + { + envValue: 'INSTANA_SPANBATCHING_ENABLED', + inCodeValue: userConfig.tracing.spanBatchingEnabled, + defaultValue: defaultConfig.tracing.spanBatchingEnabled + }, + [validate.booleanValidator] + ); - config.tracing.spanBatchingEnabled = defaults.tracing.spanBatchingEnabled; + configStore.set('config.tracing.spanBatchingEnabled', { source }); + finalConfig.tracing.spanBatchingEnabled = value; } /** - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeDisableW3cTraceCorrelation(config) { - if (config.tracing.disableW3cTraceCorrelation === true) { - logger.info('W3C trace correlation has been disabled via config.'); - return; - } - if (process.env['INSTANA_DISABLE_W3C_TRACE_CORRELATION']) { - logger.info( - 'W3C trace correlation has been disabled via environment variable INSTANA_DISABLE_W3C_TRACE_CORRELATION.' - ); - config.tracing.disableW3cTraceCorrelation = true; - return; - } +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] + ); - config.tracing.disableW3cTraceCorrelation = defaults.tracing.disableW3cTraceCorrelation; + configStore.set('config.tracing.disableW3cTraceCorrelation', { source }); + finalConfig.tracing.disableW3cTraceCorrelation = value; } /** - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeTracingKafka(config) { - config.tracing.kafka = config.tracing.kafka || {}; +function normalizeTracingKafka({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + const userKafka = userConfig.tracing.kafka || {}; - if (config.tracing.kafka.traceCorrelation === false) { - logger.info('Kafka trace correlation has been disabled via config.'); - } else if ( - process.env['INSTANA_KAFKA_TRACE_CORRELATION'] != null && - process.env['INSTANA_KAFKA_TRACE_CORRELATION'].toLowerCase() === 'false' - ) { - logger.info('Kafka trace correlation has been disabled via environment variable INSTANA_KAFKA_TRACE_CORRELATION.'); - config.tracing.kafka.traceCorrelation = false; - } else { - config.tracing.kafka.traceCorrelation = defaults.tracing.kafka.traceCorrelation; - } + 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; } /** - * @param {InstanaConfig} config + * 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(config) { - if (config.secrets == null) { - config.secrets = {}; - } +function normalizeSecrets({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + const userSecrets = userConfig.secrets; + finalConfig.secrets = {}; /** @type {InstanaSecretsOption} */ let fromEnvVar = {}; @@ -643,27 +744,61 @@ function normalizeSecrets(config) { fromEnvVar = parseSecretsEnvVar(process.env.INSTANA_SECRETS); } - config.secrets.matcherMode = config.secrets.matcherMode || fromEnvVar.matcherMode || defaults.secrets.matcherMode; - config.secrets.keywords = config.secrets.keywords || fromEnvVar.keywords || defaults.secrets.keywords; + if (finalConfig.secrets.matcherMode) { + logger.debug(`[config] incode:config.secrets.matcherMode = ${finalConfig.secrets.matcherMode}`); + configStore.set('config.secrets.matcherMode', { source: CONFIG_SOURCES.INCODE }); + } else if (fromEnvVar.matcherMode) { + logger.debug(`[config] env:INSTANA_SECRETS (matcherMode) = ${fromEnvVar.matcherMode}`); + configStore.set('config.secrets.matcherMode', { source: CONFIG_SOURCES.ENV }); + } + + if (finalConfig.secrets.keywords) { + logger.debug('[config] incode:config.secrets.keywords'); + configStore.set('config.secrets.keywords', { source: CONFIG_SOURCES.INCODE }); + } else if (fromEnvVar.keywords) { + logger.debug('[config] env:INSTANA_SECRETS (keywords)'); + configStore.set('config.secrets.keywords', { source: CONFIG_SOURCES.ENV }); + } + const matcherMode = userSecrets?.matcherMode || fromEnvVar.matcherMode || defaultConfig.secrets.matcherMode; - if (typeof config.secrets.matcherMode !== 'string') { + const keywords = userSecrets?.keywords || fromEnvVar.keywords || defaultConfig.secrets.keywords; + + if (typeof matcherMode !== 'string') { logger.warn( - `The value of config.secrets.matcherMode ("${config.secrets.matcherMode}") is not a string. Assuming the default value ${defaults.secrets.matcherMode}.` + // eslint-disable-next-line max-len + `The value of config.secrets.matcherMode ("${matcherMode}") is not a string. Assuming the default value ${defaults.secrets.matcherMode}.` ); - config.secrets.matcherMode = defaults.secrets.matcherMode; - } else if (validSecretsMatcherModes.indexOf(config.secrets.matcherMode) < 0) { + finalConfig.secrets.matcherMode = defaultConfig.secrets.matcherMode; + configStore.set('config.secrets.matcherMode', { source: CONFIG_SOURCES.INCODE }); + } else if (validSecretsMatcherModes.indexOf(matcherMode) < 0) { logger.warn( - `The value of config.secrets.matcherMode (or the matcher mode parsed from INSTANA_SECRETS) (${config.secrets.matcherMode}) is not a supported matcher mode. Assuming the default value ${defaults.secrets.matcherMode}.` + // 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}.` ); - config.secrets.matcherMode = defaults.secrets.matcherMode; - } else if (!Array.isArray(config.secrets.keywords)) { + finalConfig.secrets.matcherMode = defaultConfig.secrets.matcherMode; + configStore.set('config.secrets.matcherMode', { + source: CONFIG_SOURCES.INCODE + }); + } else { + finalConfig.secrets.matcherMode = matcherMode; + configStore.set('config.secrets.matcherMode', { source: CONFIG_SOURCES.INCODE }); + } + + if (!Array.isArray(keywords)) { logger.warn( - `The value of config.secrets.keywords (${config.secrets.keywords}) is not an array. Assuming the default value ${defaults.secrets.keywords}.` + // eslint-disable-next-line max-len + `The value of config.secrets.keywords (${keywords}) is not an array. Assuming the default value ${defaults.secrets.keywords}.` ); - config.secrets.keywords = defaults.secrets.keywords; + finalConfig.secrets.keywords = defaultConfig.secrets.keywords; + configStore.set('config.secrets.keywords', { source: CONFIG_SOURCES.INCODE }); + } else { + finalConfig.secrets.keywords = keywords; + configStore.set('config.secrets.keywords', { source: CONFIG_SOURCES.INCODE }); } - if (config.secrets.matcherMode === 'none') { - config.secrets.keywords = []; + + if (finalConfig.secrets.matcherMode === 'none') { + finalConfig.secrets.keywords = []; + configStore.set('config.secrets.keywords', { source: CONFIG_SOURCES.INCODE }); } } @@ -687,7 +822,7 @@ function parseMatcherMode(matcherMode) { * @returns {InstanaSecretsOption} */ function parseSecretsEnvVar(envVarValue) { - let [matcherMode, keywords] = envVarValue.split(':', 2); + const [matcherMode, keywords] = envVarValue.split(':', 2); const parsedMatcherMode = parseMatcherMode(matcherMode); @@ -701,6 +836,7 @@ function parseSecretsEnvVar(envVarValue) { 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=:[,]. This setting will be ignored.` ); return {}; @@ -712,120 +848,149 @@ function parseSecretsEnvVar(envVarValue) { keywords: keywordsArray }; } - -/** - * @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 + * 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(config) { - if (!config.tracing.ignoreEndpoints) { - config.tracing.ignoreEndpoints = {}; - } - - const ignoreEndpointsConfig = config.tracing.ignoreEndpoints; - - if (typeof ignoreEndpointsConfig !== 'object' || Array.isArray(ignoreEndpointsConfig)) { - logger.warn( - `Invalid tracing.ignoreEndpoints configuration. Expected an object, but received: ${JSON.stringify( - ignoreEndpointsConfig - )}` - ); - config.tracing.ignoreEndpoints = {}; - return; - } - // Case 1: Use in-code configuration if available - if (Object.keys(ignoreEndpointsConfig).length) { - config.tracing.ignoreEndpoints = configNormalizers.ignoreEndpoints.normalizeConfig(ignoreEndpointsConfig); - logger.debug(`Ignore endpoints have been configured: ${JSON.stringify(config.tracing.ignoreEndpoints)}`); - return; - } +function normalizeIgnoreEndpoints({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + const userIgnoreEndpoints = userConfig.tracing.ignoreEndpoints; - // Case 2: Load from a YAML file if `INSTANA_IGNORE_ENDPOINTS_PATH` is set + // 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) { - config.tracing.ignoreEndpoints = configNormalizers.ignoreEndpoints.fromYaml( + finalConfig.tracing.ignoreEndpoints = configNormalizers.ignoreEndpoints.fromYaml( process.env.INSTANA_IGNORE_ENDPOINTS_PATH ); - - logger.debug(`Ignore endpoints have been configured: ${JSON.stringify(config.tracing.ignoreEndpoints)}`); + logger.debug('[config] env:INSTANA_IGNORE_ENDPOINTS_PATH'); + configStore.set('config.tracing.ignoreEndpoints', { source: CONFIG_SOURCES.ENV }); return; } - // Case 3: Load from the `INSTANA_IGNORE_ENDPOINTS` environment variable + // 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) { - config.tracing.ignoreEndpoints = configNormalizers.ignoreEndpoints.fromEnv(process.env.INSTANA_IGNORE_ENDPOINTS); - logger.debug(`Ignore endpoints have been configured: ${JSON.stringify(config.tracing.ignoreEndpoints)}`); + 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; } -} -/** - * @param {InstanaConfig} config - */ -function normalizeIgnoreEndpointsDisableSuppression(config) { - if (process.env['INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION'] === 'true') { - logger.info( - 'Disabling downstream suppression for ignoring endpoints feature as it is explicitly disabled via environment variable "INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION".' + // 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 + )}` ); - config.tracing.ignoreEndpointsDisableSuppression = true; + 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; } - config.tracing.ignoreEndpointsDisableSuppression = defaults.tracing.ignoreEndpointsDisableSuppression; + finalConfig.tracing.ignoreEndpoints = defaultConfig.tracing.ignoreEndpoints; + configStore.set('config.tracing.ignoreEndpoints', { source: CONFIG_SOURCES.DEFAULT }); } /** - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizeDisableEOLEvents(config) { - config.tracing = config.tracing || {}; +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] + ); - if (process.env['INSTANA_TRACING_DISABLE_EOL_EVENTS'] === 'true') { - logger.info( - 'Disabling EOL events as it is explicitly disabled via environment variable "INSTANA_TRACING_DISABLE_EOL_EVENTS".' - ); - config.tracing.disableEOLEvents = true; - return; - } + configStore.set('config.tracing.ignoreEndpointsDisableSuppression', { source }); + finalConfig.tracing.ignoreEndpointsDisableSuppression = value; +} - config.tracing.disableEOLEvents = defaults.tracing.disableEOLEvents; +/** + * @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; } /** - * @param {InstanaConfig} config + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ -function normalizePreloadOpentelemetry(config) { - if (config.preloadOpentelemetry === true) { - return; - } +function normalizePreloadOpentelemetry({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + const { value, source } = util.resolve( + { + inCodeValue: userConfig.preloadOpentelemetry, + defaultValue: defaultConfig.preloadOpentelemetry + }, + [validate.booleanValidator] + ); - config.preloadOpentelemetry = defaults.preloadOpentelemetry; + finalConfig.preloadOpentelemetry = value; + configStore.set('config.preloadOpentelemetry', { source }); } + +/** + * Updates configuration values dynamically from external sources (e.g., agent) + * + * @param {Object} [params] + * @param {Record} [params.externalConfig] + * @param {number} [params.source] + * @param {Record} [params.target=currentConfig] + * @param {string} [params.basePath='config'] + * @returns {Record} + */ +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 }); + } + }); + + return currentConfig; +}; diff --git a/packages/core/src/config/util.js b/packages/core/src/config/util.js new file mode 100644 index 0000000000..7fb06446c5 --- /dev/null +++ b/packages/core/src/config/util.js @@ -0,0 +1,82 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +const { CONFIG_SOURCES } = require('../util/constants'); + +/** @type {import('../core').GenericLogger} */ +let logger; + +/** + * @param {import('../core').GenericLogger} [_logger] + */ +exports.init = _logger => { + logger = _logger; +}; + +const CONFIG_PRIORITY = Object.entries(CONFIG_SOURCES) + .sort((a, b) => a[1] - b[1]) + .map(([key]) => { + return key.toLowerCase(); + }); + +/** + * + * @param {Object} params + * @param {string} [params.envValue] + * @param {any} [params.inCodeValue] + * @param {any} [params.agentValue] + * @param {any} params.defaultValue + * @param {Function|Function[]} validators - validator(s) returning value | undefined + * @returns {{ value: any, source: number }} + */ +exports.resolve = function resolve({ envValue, inCodeValue, agentValue, defaultValue }, validators) { + let resolved; + + const validatorList = Array.isArray(validators) ? validators : [validators]; + + const inputs = { + env: envValue ? process.env[envValue] : undefined, + incode: inCodeValue, + agent: agentValue, + default: defaultValue + }; + + CONFIG_PRIORITY.some(sourceKey => { + const rawValue = inputs[/** @type {keyof typeof inputs} */ (sourceKey)]; + + if (rawValue === undefined && sourceKey !== 'default') { + return false; + } + + const parsedValue = validatorList.reduce((val, fn) => { + if (val === undefined) return undefined; + return fn(val); + }, rawValue); + + if (parsedValue !== undefined) { + resolved = { + value: parsedValue, + source: CONFIG_SOURCES[/** @type {keyof typeof CONFIG_SOURCES} */ (sourceKey.toUpperCase())] + }; + + if ( + CONFIG_SOURCES[/** @type {keyof typeof CONFIG_SOURCES} */ (sourceKey.toUpperCase())] !== CONFIG_SOURCES.DEFAULT + ) { + logger?.debug(`[config] Resolved from ${sourceKey}: ${JSON.stringify(parsedValue)}`); + } + return true; + } + + return false; + }); + + return ( + resolved || { + value: defaultValue, + source: CONFIG_SOURCES.DEFAULT + } + ); +}; diff --git a/packages/core/src/config/validator.js b/packages/core/src/config/validator.js new file mode 100644 index 0000000000..a10c82cefe --- /dev/null +++ b/packages/core/src/config/validator.js @@ -0,0 +1,51 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +/** + * @param {any} value + * @returns {number|undefined} + */ +exports.numberValidator = function numberValidator(value) { + if (value == null) return undefined; + const num = typeof value === 'number' ? value : Number(value); + return Number.isNaN(num) ? undefined : num; +}; + +/** + * @param {any} value + * @returns {boolean|undefined} + */ +exports.booleanValidator = function booleanValidator(value) { + if (value == null) return undefined; + + if (typeof value === 'boolean') return value; + + if (typeof value === 'string') { + const normalized = value.toLowerCase(); + if (normalized === 'true' || normalized === '1') return true; + if (normalized === 'false' || normalized === '0') return false; + } + + return undefined; +}; + +/** + * @param {any} value + * @returns {string|undefined} + */ +exports.stringValidator = function stringValidator(value) { + if (value == null) return undefined; + return typeof value === 'string' ? value : undefined; +}; + +/** + * @param {any} value + * @returns {boolean|undefined} + */ +exports.validateTruthyBoolean = function validateTruthyBoolean(value) { + // Return true if value is truthy, undefined otherwise + return value ? true : undefined; +}; diff --git a/packages/core/src/tracing/index.js b/packages/core/src/tracing/index.js index 65fd713186..80d4db8f91 100644 --- a/packages/core/src/tracing/index.js +++ b/packages/core/src/tracing/index.js @@ -260,12 +260,12 @@ function initInstanaInstrumentations(_config) { } } -exports.activate = function activate(extraConfig = {}) { +exports.activate = function activate(_config = config) { if (tracingEnabled && !tracingActivated) { tracingActivated = true; - coreUtil.activate(extraConfig); - tracingUtil.activate(extraConfig); - spanBuffer.activate(extraConfig); + coreUtil.activate(_config); + tracingUtil.activate(_config); + spanBuffer.activate(_config); opentracing.activate(); sdk.activate(); @@ -285,7 +285,7 @@ exports.activate = function activate(extraConfig = {}) { instrumentationKey }) ) { - instrumentationModules[instrumentationKey].activate(extraConfig); + instrumentationModules[instrumentationKey].activate(_config); } }); } diff --git a/packages/core/src/tracing/instrumentation/messaging/kafkaJs.js b/packages/core/src/tracing/instrumentation/messaging/kafkaJs.js index f887225f13..9d5365763f 100644 --- a/packages/core/src/tracing/instrumentation/messaging/kafkaJs.js +++ b/packages/core/src/tracing/instrumentation/messaging/kafkaJs.js @@ -28,12 +28,9 @@ exports.updateConfig = function updateConfig(config) { traceCorrelationEnabled = config.tracing.kafka.traceCorrelation; }; -exports.activate = function activate(extraConfig) { - if (extraConfig && extraConfig.tracing && extraConfig.tracing.kafka) { - if (extraConfig.tracing.kafka.traceCorrelation != null) { - traceCorrelationEnabled = extraConfig.tracing.kafka.traceCorrelation; - } - } +// TODO: We will remove _config as soon as the config object is a config instance (`config.get`) +exports.activate = function activate(_config) { + traceCorrelationEnabled = _config.tracing.kafka.traceCorrelation; isActive = true; }; diff --git a/packages/core/src/tracing/instrumentation/messaging/rdkafka.js b/packages/core/src/tracing/instrumentation/messaging/rdkafka.js index 58251265f4..09e40e0395 100644 --- a/packages/core/src/tracing/instrumentation/messaging/rdkafka.js +++ b/packages/core/src/tracing/instrumentation/messaging/rdkafka.js @@ -27,13 +27,10 @@ exports.init = function init(config) { exports.updateConfig = function updateConfig(config) { traceCorrelationEnabled = config.tracing.kafka.traceCorrelation; }; -// The extraConfig is coming from the agent configs. You can set the kafka format in the agent. -exports.activate = function activate(extraConfig) { - if (extraConfig && extraConfig.tracing && extraConfig.tracing.kafka) { - if (extraConfig.tracing.kafka.traceCorrelation != null) { - traceCorrelationEnabled = extraConfig.tracing.kafka.traceCorrelation; - } - } + +// +exports.activate = function activate(_config) { + traceCorrelationEnabled = _config.tracing.kafka.traceCorrelation; isActive = true; }; diff --git a/packages/core/src/tracing/instrumentation/protocols/http2Client.js b/packages/core/src/tracing/instrumentation/protocols/http2Client.js index af5fd11ad5..5cfae7d721 100644 --- a/packages/core/src/tracing/instrumentation/protocols/http2Client.js +++ b/packages/core/src/tracing/instrumentation/protocols/http2Client.js @@ -35,15 +35,9 @@ exports.updateConfig = config => { extraHttpHeadersToCapture = config.tracing.http.extraHttpHeadersToCapture; }; -exports.activate = function activate(extraConfig) { - if ( - extraConfig && - extraConfig.tracing && - extraConfig.tracing.http && - Array.isArray(extraConfig.tracing.http.extraHttpHeadersToCapture) - ) { - extraHttpHeadersToCapture = extraConfig.tracing.http.extraHttpHeadersToCapture; - } +exports.activate = function activate(_config) { + extraHttpHeadersToCapture = _config.tracing.http.extraHttpHeadersToCapture; + isActive = true; }; diff --git a/packages/core/src/tracing/instrumentation/protocols/http2Server.js b/packages/core/src/tracing/instrumentation/protocols/http2Server.js index 93c49477ff..30cfefc4e3 100644 --- a/packages/core/src/tracing/instrumentation/protocols/http2Server.js +++ b/packages/core/src/tracing/instrumentation/protocols/http2Server.js @@ -38,15 +38,9 @@ exports.updateConfig = function updateConfig(config) { extraHttpHeadersToCapture = config.tracing.http.extraHttpHeadersToCapture; }; -exports.activate = function activate(extraConfig) { - if ( - extraConfig && - extraConfig.tracing && - extraConfig.tracing.http && - Array.isArray(extraConfig.tracing.http.extraHttpHeadersToCapture) - ) { - extraHttpHeadersToCapture = extraConfig.tracing.http.extraHttpHeadersToCapture; - } +exports.activate = function activate(_config) { + extraHttpHeadersToCapture = _config.tracing.http.extraHttpHeadersToCapture; + isActive = true; }; diff --git a/packages/core/src/tracing/instrumentation/protocols/httpClient.js b/packages/core/src/tracing/instrumentation/protocols/httpClient.js index 6ad73a78b1..def238877f 100644 --- a/packages/core/src/tracing/instrumentation/protocols/httpClient.js +++ b/packages/core/src/tracing/instrumentation/protocols/httpClient.js @@ -43,15 +43,9 @@ exports.updateConfig = function updateConfig(config) { extraHttpHeadersToCapture = config.tracing.http.extraHttpHeadersToCapture; }; -exports.activate = function activate(extraConfig) { - if ( - extraConfig && - extraConfig.tracing && - extraConfig.tracing.http && - Array.isArray(extraConfig.tracing.http.extraHttpHeadersToCapture) - ) { - extraHttpHeadersToCapture = extraConfig.tracing.http.extraHttpHeadersToCapture; - } +exports.activate = function activate(_config) { + extraHttpHeadersToCapture = _config.tracing.http.extraHttpHeadersToCapture; + isActive = true; }; diff --git a/packages/core/src/tracing/instrumentation/protocols/httpServer.js b/packages/core/src/tracing/instrumentation/protocols/httpServer.js index 352cb7af28..5ebc25227c 100644 --- a/packages/core/src/tracing/instrumentation/protocols/httpServer.js +++ b/packages/core/src/tracing/instrumentation/protocols/httpServer.js @@ -32,15 +32,9 @@ exports.updateConfig = function updateConfig(config) { extraHttpHeadersToCapture = config.tracing.http.extraHttpHeadersToCapture; }; -exports.activate = function activate(extraConfig) { - if ( - extraConfig && - extraConfig.tracing && - extraConfig.tracing.http && - Array.isArray(extraConfig.tracing.http.extraHttpHeadersToCapture) - ) { - extraHttpHeadersToCapture = extraConfig.tracing.http.extraHttpHeadersToCapture; - } +exports.activate = function activate(_config) { + extraHttpHeadersToCapture = _config.tracing.http.extraHttpHeadersToCapture; + isActive = true; }; diff --git a/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js b/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js index 9b72e210f6..e8c1a62e0e 100644 --- a/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js +++ b/packages/core/src/tracing/instrumentation/protocols/nativeFetch.js @@ -53,20 +53,14 @@ exports.updateConfig = function updateConfig(config) { extraHttpHeadersToCapture = config.tracing.http.extraHttpHeadersToCapture; }; -exports.activate = function activate(extraConfig) { +exports.activate = function activate(_config) { if (originalFetch == null) { // Do nothing in Node.js versions that do not support native fetch. return; } - if ( - extraConfig && - extraConfig.tracing && - extraConfig.tracing.http && - Array.isArray(extraConfig.tracing.http.extraHttpHeadersToCapture) - ) { - extraHttpHeadersToCapture = extraConfig.tracing.http.extraHttpHeadersToCapture; - } + extraHttpHeadersToCapture = _config.tracing.http.extraHttpHeadersToCapture; + isActive = true; }; diff --git a/packages/core/src/tracing/spanBuffer.js b/packages/core/src/tracing/spanBuffer.js index f6bf5b19b9..a27ebd4aef 100644 --- a/packages/core/src/tracing/spanBuffer.js +++ b/packages/core/src/tracing/spanBuffer.js @@ -103,9 +103,9 @@ exports.init = function init(config, _downstreamConnection) { }; /** - * @param {import('@instana/collector/src/types/collector').AgentConfig} extraConfig + * @param {import('../config').InstanaConfig} _config */ -exports.activate = function activate(extraConfig) { +exports.activate = function activate(_config) { if (!downstreamConnection) { logger.error('No downstreamConnection has been set.'); return; @@ -119,11 +119,7 @@ exports.activate = function activate(extraConfig) { return; } - if (extraConfig?.tracing) { - if (extraConfig.tracing.spanBatchingEnabled) { - batchingEnabled = true; - } - } + batchingEnabled = _config.tracing.spanBatchingEnabled; isActive = true; if (activatedAt == null) { diff --git a/packages/core/src/tracing/tracingUtil.js b/packages/core/src/tracing/tracingUtil.js index e8d3efa461..57b713f6a3 100644 --- a/packages/core/src/tracing/tracingUtil.js +++ b/packages/core/src/tracing/tracingUtil.js @@ -10,7 +10,7 @@ const path = require('path'); const StringDecoder = require('string_decoder').StringDecoder; const stackTrace = require('../util/stackTrace'); -const { DEFAULT_STACK_TRACE_LENGTH, DEFAULT_STACK_TRACE_MODE, STACK_TRACE_MODES } = require('../util/constants'); +const { STACK_TRACE_MODES } = require('../util/constants'); /** @type {import('../core').GenericLogger} */ let logger; @@ -34,26 +34,11 @@ exports.init = function (config) { }; /** - * @param {import('@instana/collector/src/types/collector').AgentConfig} extraConfig + * @param {import('../config').InstanaConfig} _config */ -exports.activate = function activate(extraConfig) { - const agentTraceConfig = extraConfig?.tracing; - - // Note: We check whether the already-initialized stackTraceLength equals the default value. - // If it does, we can safely override it, since the user did not explicitly configure it. - - // Note: If the user configured a value via env or code and also configured a different value in the agent, - // but the env/code value happens to equal the default, the agent value would overwrite it. - // This is a rare edge case and acceptable for now. - - if (agentTraceConfig?.stackTrace && stackTraceMode === DEFAULT_STACK_TRACE_MODE) { - stackTraceMode = agentTraceConfig.stackTrace; - } - - // stackTraceLength is valid when set to any number, including 0 - if (agentTraceConfig?.stackTraceLength != null && stackTraceLength === DEFAULT_STACK_TRACE_LENGTH) { - stackTraceLength = agentTraceConfig.stackTraceLength; - } +exports.activate = function activate(_config) { + stackTraceLength = _config.tracing.stackTraceLength; + stackTraceMode = _config.tracing.stackTrace; }; /** diff --git a/packages/core/src/util/constants.js b/packages/core/src/util/constants.js index 471d5340f2..fefc2d4255 100644 --- a/packages/core/src/util/constants.js +++ b/packages/core/src/util/constants.js @@ -17,3 +17,10 @@ exports.STACK_TRACE_MODES = { ALL: 'all', NONE: 'none' }; + +exports.CONFIG_SOURCES = { + ENV: 1, + INCODE: 2, + AGENT: 3, + DEFAULT: 4 +}; diff --git a/packages/core/src/util/disableInstrumentation.js b/packages/core/src/util/disableInstrumentation.js index ffc27b4764..32ef62bc1b 100644 --- a/packages/core/src/util/disableInstrumentation.js +++ b/packages/core/src/util/disableInstrumentation.js @@ -9,9 +9,6 @@ const { DISABLABLE_INSTRUMENTATION_GROUPS } = require('../tracing/constants'); /** @type {import('../config').InstanaConfig} */ let config; -/** @type {import('@instana/collector/src/types/collector').AgentConfig} */ -let agentConfig; - /** * @param {import('../config').InstanaConfig} _config */ @@ -20,10 +17,10 @@ function init(_config) { } /** - * @param {import('@instana/collector/src/types/collector').AgentConfig} _agentConfig + * @param {import('../config').InstanaConfig} _config */ -function activate(_agentConfig) { - agentConfig = _agentConfig; +function activate(_config) { + config = _config; } /** @@ -110,17 +107,10 @@ function isInstrumentationDisabled({ instrumentationModules = {}, instrumentatio const context = { moduleName, instrumentationName, group }; - // Give priority to service-level config if (config && shouldDisable(config, context)) { return true; } - // Fallback to agent-level config if not disabled above - // NOTE: We currently have no single config object. - if (agentConfig && shouldDisable(agentConfig, context)) { - return true; - } - return false; } diff --git a/packages/core/src/util/index.js b/packages/core/src/util/index.js index c675887b66..02e6a5bf3b 100644 --- a/packages/core/src/util/index.js +++ b/packages/core/src/util/index.js @@ -44,11 +44,11 @@ exports.init = function init(config) { }; /** - * @param {import('@instana/collector/src/types/collector').AgentConfig} extraConfig + * @param {import('@instana/core/src/config').InstanaConfig} config */ -exports.activate = function activate(extraConfig) { - disableInstrumentation.activate(extraConfig); - spanFilter.activate(extraConfig); +exports.activate = function activate(config) { + disableInstrumentation.activate(config); + spanFilter.activate(config); }; exports.applicationUnderMonitoring = applicationUnderMonitoring; diff --git a/packages/core/src/util/spanFilter.js b/packages/core/src/util/spanFilter.js index 189469f170..dd820f4b61 100644 --- a/packages/core/src/util/spanFilter.js +++ b/packages/core/src/util/spanFilter.js @@ -19,26 +19,11 @@ function init(config) { } /** - * @param {import('@instana/collector/src/types/collector').AgentConfig} extraConfig + * @param {import('../config').InstanaConfig} _config */ -function activate(extraConfig) { - /** - * Configuration priority order: - * 1. In-code configuration - * 2. Environment variables: - * - `INSTANA_IGNORE_ENDPOINTS_PATH` - * - `INSTANA_IGNORE_ENDPOINTS` - * 3. Agent configuration (loaded later) - * - * Since the agent configuration is loaded later, we first check - * that `ignoreEndpoints` MUST be empty. If yes, we - * are allowed to fall back to the agent's configuration (`extraConfig.tracing.ignoreEndpoints`). - * - * TODO: Perform a major refactoring of configuration priority ordering in INSTA-817. - */ - const isIgnoreEndpointsEmpty = !ignoreEndpoints || Object.keys(ignoreEndpoints).length === 0; - if (isIgnoreEndpointsEmpty && extraConfig?.tracing?.ignoreEndpoints) { - ignoreEndpoints = extraConfig.tracing.ignoreEndpoints; +function activate(_config) { + if (_config?.tracing?.ignoreEndpoints) { + ignoreEndpoints = _config.tracing.ignoreEndpoints; } } diff --git a/packages/core/test/config/configNormalizers/disable_test.js b/packages/core/test/config/configNormalizers/disable_test.js index eaf3c68114..328335ead1 100644 --- a/packages/core/test/config/configNormalizers/disable_test.js +++ b/packages/core/test/config/configNormalizers/disable_test.js @@ -8,6 +8,7 @@ const { describe, it, beforeEach, afterEach } = require('mocha'); const { expect } = require('chai'); const { normalize, normalizeExternalConfig } = require('../../../src/config/configNormalizers/disable'); +const { CONFIG_SOURCES } = require('../../../src/util/constants'); function resetEnv() { delete process.env.INSTANA_TRACING_DISABLE; @@ -29,7 +30,10 @@ describe('util.configNormalizers.disable', () => { const config = {}; const result = normalize(config); - expect(result).to.deep.equal({}); + expect(result).to.deep.equal({ + value: {}, + source: CONFIG_SOURCES.DEFAULT + }); expect(config.tracing).to.exist; }); @@ -43,7 +47,8 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.value.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.source).to.equal(CONFIG_SOURCES.INCODE); }); it('should handle non-array "instrumentations" input gracefully', () => { @@ -56,7 +61,8 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result.instrumentations).to.deep.equal([]); + expect(result.value.instrumentations).to.deep.equal([]); + expect(result.source).to.equal(CONFIG_SOURCES.INCODE); }); it('should handle flat disable config', () => { @@ -67,7 +73,8 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.value.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.source).to.equal(CONFIG_SOURCES.INCODE); }); it('should support disabling by group names', () => { @@ -80,7 +87,8 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result.groups).to.deep.equal(['logging', 'databases']); + expect(result.value.groups).to.deep.equal(['logging', 'databases']); + expect(result.source).to.equal(CONFIG_SOURCES.INCODE); }); it('should normalize group names: lowercase and trim whitespace', () => { @@ -93,7 +101,8 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result.groups).to.deep.equal(['logging', 'databases', 'messaging']); + expect(result.value.groups).to.deep.equal(['logging', 'databases', 'messaging']); + expect(result.source).to.equal(CONFIG_SOURCES.INCODE); }); it('should handle non-array "groups" input gracefully', () => { @@ -106,7 +115,8 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result.groups).to.deep.equal([]); + expect(result.value.groups).to.deep.equal([]); + expect(result.source).to.equal(CONFIG_SOURCES.INCODE); }); it('should handle mixed array of instrumentations and groups', () => { @@ -118,8 +128,11 @@ describe('util.configNormalizers.disable', () => { const result = normalize(config); expect(result).to.deep.equal({ - instrumentations: ['aws-sdk', 'mongodb'], - groups: ['logging', 'databases'] + value: { + instrumentations: ['aws-sdk', 'mongodb'], + groups: ['logging', 'databases'] + }, + source: CONFIG_SOURCES.INCODE }); }); @@ -131,15 +144,24 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result).to.deep.equal({}); + expect(result).to.deep.equal({ + value: {}, + source: CONFIG_SOURCES.DEFAULT + }); }); it('should return an empty object if disable is null or undefined', () => { const config1 = { tracing: { disable: null } }; const config2 = { tracing: { disable: undefined } }; - expect(normalize(config1)).to.deep.equal({}); - expect(normalize(config2)).to.deep.equal({}); + expect(normalize(config1)).to.deep.equal({ + value: {}, + source: CONFIG_SOURCES.DEFAULT + }); + expect(normalize(config2)).to.deep.equal({ + value: {}, + source: CONFIG_SOURCES.DEFAULT + }); }); it('should ignore non-string values in disable array', () => { @@ -150,7 +172,8 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result.instrumentations).to.deep.equal(['aws-sdk', 'mongodb']); + expect(result.value.instrumentations).to.deep.equal(['aws-sdk', 'mongodb']); + expect(result.source).to.equal(CONFIG_SOURCES.INCODE); }); it('should ignore non-string values inside disable.instrumentations', () => { @@ -161,7 +184,8 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result.instrumentations).to.deep.equal(['aws-sdk', 'mongodb']); + expect(result.value.instrumentations).to.deep.equal(['aws-sdk', 'mongodb']); + expect(result.source).to.equal(CONFIG_SOURCES.INCODE); }); it('should return true if tracing is globally disabled (disable = true)', () => { @@ -172,7 +196,10 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result).to.equal(true); + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.INCODE + }); }); it('should return an empty object if tracing disable is set to false', () => { @@ -183,7 +210,10 @@ describe('util.configNormalizers.disable', () => { }; const result = normalize(config); - expect(result).to.deep.equal({}); + expect(result).to.deep.equal({ + value: {}, + source: CONFIG_SOURCES.DEFAULT + }); }); }); @@ -194,7 +224,8 @@ describe('util.configNormalizers.disable', () => { const config = {}; const result = normalize(config); - expect(result.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.value.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.source).to.equal(CONFIG_SOURCES.ENV); }); it('should parse "INSTANA_TRACING_DISABLE" as instrumentations', () => { @@ -203,7 +234,8 @@ describe('util.configNormalizers.disable', () => { const config = {}; const result = normalize(config); - expect(result.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.value.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.source).to.equal(CONFIG_SOURCES.ENV); }); it('should parse "INSTANA_TRACING_DISABLE_GROUPS"', () => { @@ -212,7 +244,8 @@ describe('util.configNormalizers.disable', () => { const config = {}; const result = normalize(config); - expect(result.groups).to.deep.equal(['logging', 'databases']); + expect(result.value.groups).to.deep.equal(['logging', 'databases']); + expect(result.source).to.equal(CONFIG_SOURCES.ENV); }); it('should support semicolon-separated values in environment variable', () => { @@ -221,7 +254,8 @@ describe('util.configNormalizers.disable', () => { const config = {}; const result = normalize(config); - expect(result.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.value.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.source).to.equal(CONFIG_SOURCES.ENV); }); it('should ignore empty or whitespace-only entries in environment variable', () => { @@ -231,8 +265,9 @@ describe('util.configNormalizers.disable', () => { const config = {}; const result = normalize(config); - expect(result.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); - expect(result.groups).to.deep.equal(['logging', 'databases', 'messaging']); + expect(result.value.instrumentations).to.deep.equal(['aws-sdk', 'mongodb', 'postgres']); + expect(result.value.groups).to.deep.equal(['logging', 'databases', 'messaging']); + expect(result.source).to.equal(CONFIG_SOURCES.ENV); }); it('should combine env instrumentation and group variables', () => { @@ -243,8 +278,11 @@ describe('util.configNormalizers.disable', () => { const result = normalize(config); expect(result).to.deep.equal({ - instrumentations: ['aws-sdk', 'mongodb'], - groups: ['logging', 'databases'] + value: { + instrumentations: ['aws-sdk', 'mongodb'], + groups: ['logging', 'databases'] + }, + source: CONFIG_SOURCES.ENV }); }); @@ -255,8 +293,11 @@ describe('util.configNormalizers.disable', () => { const result = normalize(config); expect(result).to.deep.equal({ - instrumentations: ['aws-sdk', 'mongodb'], - groups: ['logging', 'databases'] + value: { + instrumentations: ['aws-sdk', 'mongodb'], + groups: ['logging', 'databases'] + }, + source: CONFIG_SOURCES.ENV }); }); @@ -267,7 +308,10 @@ describe('util.configNormalizers.disable', () => { const config = {}; const result = normalize(config); - expect(result).to.deep.equal({}); + expect(result).to.deep.equal({ + value: {}, + source: CONFIG_SOURCES.DEFAULT + }); }); it('should return true if INSTANA_TRACING_DISABLE is "true"', () => { @@ -276,7 +320,10 @@ describe('util.configNormalizers.disable', () => { const config = {}; const result = normalize(config); - expect(result).to.equal(true); + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.ENV + }); }); it('should return empty object if INSTANA_TRACING_DISABLE is "false"', () => { @@ -285,7 +332,44 @@ describe('util.configNormalizers.disable', () => { const config = {}; const result = normalize(config); - expect(result).to.deep.equal({}); + expect(result).to.deep.equal({ + value: {}, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should give precedence to INSTANA_TRACING_DISABLE=false over config.tracing.disable=true', () => { + process.env.INSTANA_TRACING_DISABLE = 'false'; + + const config = { + tracing: { + disable: true + } + }; + const result = normalize(config); + + expect(result).to.deep.equal({ + value: {}, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should give precedence to INSTANA_TRACING_DISABLE=false over config with instrumentations', () => { + process.env.INSTANA_TRACING_DISABLE = 'false'; + + const config = { + tracing: { + disable: { + instrumentations: ['aws-sdk', 'mongodb'] + } + } + }; + const result = normalize(config); + + expect(result).to.deep.equal({ + value: {}, + source: CONFIG_SOURCES.ENV + }); }); it('should give precedence to INSTANA_TRACING_DISABLE=true over other env vars', () => { @@ -296,7 +380,10 @@ describe('util.configNormalizers.disable', () => { const config = {}; const result = normalize(config); - expect(result).to.equal(true); + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.ENV + }); }); }); @@ -312,7 +399,12 @@ describe('util.configNormalizers.disable', () => { }; const result = normalizeExternalConfig(config); - expect(result.instrumentations).to.deep.equal(['redis', 'console']); + expect(result).to.deep.equal({ + value: { + instrumentations: ['redis', 'console'] + }, + source: CONFIG_SOURCES.AGENT + }); }); it('should correctly categorize known group names', () => { @@ -326,8 +418,9 @@ describe('util.configNormalizers.disable', () => { }; const result = normalizeExternalConfig(config); - expect(result.groups).to.include('messaging'); - expect(result.instrumentations).to.include('kafka'); + expect(result.source).to.equal(CONFIG_SOURCES.AGENT); + expect(result.value.groups).to.include('messaging'); + expect(result.value.instrumentations).to.include('kafka'); }); it('should represent false values with negated names', () => { @@ -343,8 +436,9 @@ describe('util.configNormalizers.disable', () => { }; const result = normalizeExternalConfig(config); - expect(result.instrumentations).to.deep.equal(['redis', '!console']); - expect(result.groups).to.include('logging', '!databases'); + expect(result.source).to.equal(CONFIG_SOURCES.AGENT); + expect(result.value.instrumentations).to.deep.equal(['redis', '!console']); + expect(result.value.groups).to.include('logging', '!databases'); }); it('should return negated names if all values are false', () => { @@ -358,7 +452,12 @@ describe('util.configNormalizers.disable', () => { }; const result = normalizeExternalConfig(config); - expect(result.instrumentations).to.deep.equal(['!redis', '!pg']); + expect(result).to.deep.equal({ + value: { + instrumentations: ['!redis', '!pg'] + }, + source: CONFIG_SOURCES.AGENT + }); }); it('should ignore non-boolean entries in config object', () => { @@ -373,7 +472,12 @@ describe('util.configNormalizers.disable', () => { }; const result = normalizeExternalConfig(config); - expect(result.instrumentations).to.deep.equal(['redis']); + expect(result).to.deep.equal({ + value: { + instrumentations: ['redis'] + }, + source: CONFIG_SOURCES.AGENT + }); }); }); }); diff --git a/packages/core/test/config/configNormalizers/stackTrace_test.js b/packages/core/test/config/configNormalizers/stackTrace_test.js index b3e0ffc6e0..8e67101635 100644 --- a/packages/core/test/config/configNormalizers/stackTrace_test.js +++ b/packages/core/test/config/configNormalizers/stackTrace_test.js @@ -376,6 +376,28 @@ describe('config.configNormalizers.stackTrace', () => { expect(result).to.equal(20); }); + + it('should return null when tracing.stackTraceLength is a non-numeric string', () => { + const config = { + tracing: { + stackTraceLength: 'not-a-number' + } + }; + const result = stackTraceNormalizer.normalizeStackTraceLength(config); + + expect(result).to.be.null; + }); + + it('should return null when tracing.stackTraceLength results in NaN after parsing', () => { + const config = { + tracing: { + stackTraceLength: 'abc123' + } + }; + const result = stackTraceNormalizer.normalizeStackTraceLength(config); + + expect(result).to.be.null; + }); }); describe('normalizeStackTraceModeFromAgent()', () => { diff --git a/packages/core/test/config/normalizeConfig_test.js b/packages/core/test/config/normalizeConfig_test.js index b61295fe6b..32bc26a6d9 100644 --- a/packages/core/test/config/normalizeConfig_test.js +++ b/packages/core/test/config/normalizeConfig_test.js @@ -20,6 +20,7 @@ describe('config.normalizeConfig', () => { afterEach(resetEnv); function resetEnv() { + delete process.env.INSTANA_TRACING_DISABLE; delete process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS; delete process.env.INSTANA_TRACING_DISABLE_GROUPS; delete process.env.INSTANA_TRACING_DISABLE_EOL_EVENTS; @@ -33,9 +34,11 @@ describe('config.normalizeConfig', () => { delete process.env.INSTANA_STACK_TRACE; delete process.env.INSTANA_STACK_TRACE_LENGTH; delete process.env.INSTANA_TRACING_TRANSMISSION_DELAY; + delete process.env.INSTANA_TRACING_INITIAL_TRANSMISSION_DELAY; delete process.env.INSTANA_SPANBATCHING_ENABLED; delete process.env.INSTANA_DISABLE_SPANBATCHING; delete process.env.INSTANA_DISABLE_W3C_TRACE_CORRELATION; + delete process.env.INSTANA_DISABLE_USE_OPENTELEMETRY; delete process.env.INSTANA_KAFKA_TRACE_CORRELATION; delete process.env.INSTANA_PACKAGE_JSON_PATH; delete process.env.INSTANA_ALLOW_ROOT_EXIT_SPAN; @@ -44,1000 +47,1929 @@ describe('config.normalizeConfig', () => { delete process.env.INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION; } - it('should apply all defaults', () => { - checkDefaults(coreConfig.normalize()); - checkDefaults(coreConfig.normalize({})); - checkDefaults(coreConfig.normalize({ tracing: {}, metrics: {} })); - checkDefaults(coreConfig.normalize({ unknowConfigOption: 13 })); + describe('default configuration', () => { + it('should apply all defaults', () => { + checkDefaults(coreConfig.normalize()); + checkDefaults(coreConfig.normalize({ userConfig: { userConfig: {} } })); + checkDefaults(coreConfig.normalize({ userConfig: { userConfig: { tracing: {}, metrics: {} } } })); + checkDefaults(coreConfig.normalize({ userConfig: { userConfig: { unknowConfigOption: 13 } } })); + }); }); - it('should accept service name', () => { - const config = coreConfig.normalize({ serviceName: 'custom-service-name' }); - expect(config.serviceName).to.equal('custom-service-name'); - }); + describe('service name configuration', () => { + it('should accept service name', () => { + const config = coreConfig.normalize({ userConfig: { serviceName: 'custom-service-name' } }); + expect(config.serviceName).to.equal('custom-service-name'); + }); - it('should accept service name from env var', () => { - process.env.INSTANA_SERVICE_NAME = 'very-custom-service-name'; - const config = coreConfig.normalize(); - expect(config.serviceName).to.equal('very-custom-service-name'); - }); + it('should accept service name from env var', () => { + process.env.INSTANA_SERVICE_NAME = 'very-custom-service-name'; + const config = coreConfig.normalize(); + expect(config.serviceName).to.equal('very-custom-service-name'); + }); - it('should not accept non-string service name', () => { - const config = coreConfig.normalize({ serviceName: 42 }); - expect(config.serviceName).to.not.exist; - }); + it('should not accept non-string service name', () => { + const config = coreConfig.normalize({ userConfig: { serviceName: 42 } }); + expect(config.serviceName).to.not.exist; + }); - it('should use custom metrics transmission settings from config', () => { - const config = coreConfig.normalize({ - metrics: { - transmissionDelay: 9753 - } + it('should use config when env not set', () => { + const config = coreConfig.normalize({ userConfig: { serviceName: 'config-service-name' } }); + expect(config.serviceName).to.equal('config-service-name'); }); - expect(config.metrics.transmissionDelay).to.equal(9753); - }); - it('should use custom metrics transmission settings from env vars', () => { - process.env.INSTANA_METRICS_TRANSMISSION_DELAY = '2500'; - const config = coreConfig.normalize(); - expect(config.metrics.transmissionDelay).to.equal(2500); + it('should give precedence to INSTANA_SERVICE_NAME env var over config', () => { + process.env.INSTANA_SERVICE_NAME = 'env-service'; + const config = coreConfig.normalize({ userConfig: { serviceName: 'config-service' } }); + expect(config.serviceName).to.equal('env-service'); + }); }); - it('should use default metrics transmission settings when env vars are non-numerical', () => { - process.env.INSTANA_METRICS_TRANSMISSION_DELAY = 'x2500'; - const config = coreConfig.normalize(); - expect(config.metrics.transmissionDelay).to.equal(1000); - }); + describe('metrics configuration', () => { + it('should use custom metrics transmission settings from config', () => { + const config = coreConfig.normalize({ + userConfig: { + metrics: { + transmissionDelay: 9753 + } + } + }); + expect(config.metrics.transmissionDelay).to.equal(9753); + }); - it('should use custom config.metrics.timeBetweenHealthcheckCalls', () => { - const config = coreConfig.normalize({ - metrics: { - timeBetweenHealthcheckCalls: 9876 - } + it('should use custom metrics transmission settings from env vars', () => { + process.env.INSTANA_METRICS_TRANSMISSION_DELAY = '2500'; + const config = coreConfig.normalize(); + expect(config.metrics.transmissionDelay).to.equal(2500); }); - expect(config.metrics.timeBetweenHealthcheckCalls).to.equal(9876); - }); - it('should disable tracing with enabled: false', () => { - const config = coreConfig.normalize({ tracing: { enabled: false } }); - expect(config.tracing.enabled).to.be.false; - expect(config.tracing.automaticTracingEnabled).to.be.false; - }); + it('should use default metrics transmission settings when env vars are non-numerical', () => { + process.env.INSTANA_METRICS_TRANSMISSION_DELAY = 'x2500'; + const config = coreConfig.normalize(); + expect(config.metrics.transmissionDelay).to.equal(1000); + }); - it('should disable tracing with disable: true', () => { - const config = coreConfig.normalize({ tracing: { enabled: false } }); - expect(config.tracing.enabled).to.be.false; - expect(config.tracing.automaticTracingEnabled).to.be.false; - }); + it('should use default (1000) for transmissionDelay when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.metrics.transmissionDelay).to.equal(1000); + }); - it('should disable automatic tracing', () => { - const config = coreConfig.normalize({ tracing: { automaticTracingEnabled: false } }); - expect(config.tracing.enabled).to.be.true; - expect(config.tracing.automaticTracingEnabled).to.be.false; - }); + it('should give precedence to INSTANA_METRICS_TRANSMISSION_DELAY env var over config', () => { + process.env.INSTANA_METRICS_TRANSMISSION_DELAY = '3000'; + const config = coreConfig.normalize({ userConfig: { metrics: { transmissionDelay: 5000 } } }); + expect(config.metrics.transmissionDelay).to.equal(3000); + }); - it('should disable automatic tracing via INSTANA_DISABLE_AUTO_INSTR', () => { - process.env.INSTANA_DISABLE_AUTO_INSTR = 'true'; - const config = coreConfig.normalize(); - expect(config.tracing.enabled).to.be.true; - expect(config.tracing.automaticTracingEnabled).to.be.false; - }); + it('should fall back to config when env var is invalid', () => { + process.env.INSTANA_METRICS_TRANSMISSION_DELAY = 'invalid'; + const config = coreConfig.normalize({ userConfig: { metrics: { transmissionDelay: 5000 } } }); + expect(config.metrics.transmissionDelay).to.equal(5000); + }); - it('should not enable automatic tracing when tracing is disabled in general', () => { - const config = coreConfig.normalize({ - tracing: { - enabled: false, - automaticTracingEnabled: true - } + it('should fall back to default when both env and config are invalid', () => { + process.env.INSTANA_METRICS_TRANSMISSION_DELAY = 'invalid'; + const config = coreConfig.normalize({ userConfig: { metrics: { transmissionDelay: 'also-invalid' } } }); + expect(config.metrics.transmissionDelay).to.equal(1000); }); - expect(config.tracing.enabled).to.be.false; - expect(config.tracing.automaticTracingEnabled).to.be.false; - }); - it('should enable immediate tracing activation', () => { - const config = coreConfig.normalize({ tracing: { activateImmediately: true } }); - expect(config.tracing.activateImmediately).to.be.true; + it('should use custom config.metrics.timeBetweenHealthcheckCalls', () => { + const config = coreConfig.normalize({ + userConfig: { + metrics: { + timeBetweenHealthcheckCalls: 9876 + } + } + }); + expect(config.metrics.timeBetweenHealthcheckCalls).to.equal(9876); + }); }); - it('should enable immediate tracing activation via INSTANA_TRACE_IMMEDIATELY', () => { - process.env.INSTANA_TRACE_IMMEDIATELY = 'true'; - const config = coreConfig.normalize(); - expect(config.tracing.activateImmediately).to.be.true; - }); + describe('tracing configuration', () => { + describe('enabling and disabling tracing', () => { + it('should disable tracing with enabled: false', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { enabled: false } } }); + expect(config.tracing.enabled).to.be.false; + expect(config.tracing.automaticTracingEnabled).to.be.false; + }); - it('should not enable immediate tracing activation when tracing is disabled in general', () => { - const config = coreConfig.normalize({ - tracing: { - enabled: false, - activateImmediately: true - } + it('should disable tracing with disable: true', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { enabled: false } } }); + expect(config.tracing.enabled).to.be.false; + expect(config.tracing.automaticTracingEnabled).to.be.false; + }); + + it('should disable automatic tracing', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { automaticTracingEnabled: false } } }); + expect(config.tracing.enabled).to.be.true; + expect(config.tracing.automaticTracingEnabled).to.be.false; + }); + + it('should disable automatic tracing via INSTANA_DISABLE_AUTO_INSTR', () => { + process.env.INSTANA_DISABLE_AUTO_INSTR = 'true'; + const config = coreConfig.normalize(); + expect(config.tracing.enabled).to.be.true; + expect(config.tracing.automaticTracingEnabled).to.be.false; + }); + + it('should not enable automatic tracing when tracing is disabled in general', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + enabled: false, + automaticTracingEnabled: true + } + } + }); + expect(config.tracing.enabled).to.be.false; + expect(config.tracing.automaticTracingEnabled).to.be.false; + }); + + it('should use default (true) for tracing.enabled when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.enabled).to.be.true; + }); + + it('should give precedence to INSTANA_TRACING_DISABLE env var set to true over config set to true', () => { + process.env.INSTANA_TRACING_DISABLE = 'true'; + const config = coreConfig.normalize({ userConfig: { tracing: { enabled: true } } }); + expect(config.tracing.enabled).to.be.false; + }); + + it('should give precedence to INSTANA_TRACING_DISABLE env var set to false over config set to false', () => { + process.env.INSTANA_TRACING_DISABLE = 'false'; + const config = coreConfig.normalize({ userConfig: { tracing: { enabled: false } } }); + expect(config.tracing.enabled).to.be.true; + }); + + it('should give precedence to INSTANA_TRACING_DISABLE env var over default', () => { + process.env.INSTANA_TRACING_DISABLE = 'true'; + const config = coreConfig.normalize({}); + expect(config.tracing.enabled).to.be.false; + }); + + it('should enable tracing if env var conatin non-boolean value', () => { + process.env.INSTANA_TRACING_DISABLE = 'redis'; + const config = coreConfig.normalize({}); + expect(config.tracing.enabled).to.be.true; + }); + + it('should use default (true) for automaticTracingEnabled when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.automaticTracingEnabled).to.be.true; + }); + + it('should give precedence to INSTANA_DISABLE_AUTO_INSTR env var set to true over config set to true', () => { + process.env.INSTANA_DISABLE_AUTO_INSTR = 'true'; + const config = coreConfig.normalize({ userConfig: { tracing: { automaticTracingEnabled: true } } }); + expect(config.tracing.automaticTracingEnabled).to.be.false; + }); + + it('should give precedence to INSTANA_DISABLE_AUTO_INSTR env var set to false over config set to false', () => { + process.env.INSTANA_DISABLE_AUTO_INSTR = 'false'; + const config = coreConfig.normalize({ userConfig: { tracing: { automaticTracingEnabled: false } } }); + expect(config.tracing.automaticTracingEnabled).to.be.true; + }); + + it('should give precedence to INSTANA_DISABLE_AUTO_INSTR env var over default', () => { + process.env.INSTANA_DISABLE_AUTO_INSTR = 'true'; + const config = coreConfig.normalize({}); + expect(config.tracing.automaticTracingEnabled).to.be.false; + }); }); - expect(config.tracing.enabled).to.be.false; - expect(config.tracing.activateImmediately).to.be.false; - }); - it('should use custom tracing transmission settings from config', () => { - const config = coreConfig.normalize({ - tracing: { - maxBufferedSpans: 13, - forceTransmissionStartingAt: 2, - transmissionDelay: 9753 - } + describe('immediate activation', () => { + it('should enable immediate tracing activation', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { activateImmediately: true } } }); + expect(config.tracing.activateImmediately).to.be.true; + }); + + it('should enable immediate tracing activation via INSTANA_TRACE_IMMEDIATELY', () => { + process.env.INSTANA_TRACE_IMMEDIATELY = 'true'; + const config = coreConfig.normalize(); + expect(config.tracing.activateImmediately).to.be.true; + }); + + it('should not enable immediate tracing activation when tracing is disabled in general', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + enabled: false, + activateImmediately: true + } + } + }); + expect(config.tracing.enabled).to.be.false; + expect(config.tracing.activateImmediately).to.be.false; + }); + + it('should use default (false) for activateImmediately when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.activateImmediately).to.be.false; + }); + + it('should give precedence to INSTANA_TRACE_IMMEDIATELY env var set to true over config set to false', () => { + process.env.INSTANA_TRACE_IMMEDIATELY = 'true'; + const config = coreConfig.normalize({ userConfig: { tracing: { activateImmediately: false } } }); + expect(config.tracing.activateImmediately).to.be.true; + }); + + it('should give precedence to INSTANA_TRACE_IMMEDIATELY env var set to false over config set to true', () => { + process.env.INSTANA_TRACE_IMMEDIATELY = 'false'; + const config = coreConfig.normalize({ userConfig: { tracing: { activateImmediately: true } } }); + expect(config.tracing.activateImmediately).to.be.false; + }); }); - expect(config.tracing.maxBufferedSpans).to.equal(13); - expect(config.tracing.forceTransmissionStartingAt).to.equal(2); - expect(config.tracing.transmissionDelay).to.equal(9753); - }); - it('should use custom tracing transmission settings from env vars', () => { - process.env.INSTANA_FORCE_TRANSMISSION_STARTING_AT = '2468'; - process.env.INSTANA_TRACING_TRANSMISSION_DELAY = '2500'; - const config = coreConfig.normalize(); - expect(config.tracing.forceTransmissionStartingAt).to.equal(2468); - expect(config.tracing.transmissionDelay).to.equal(2500); - }); + describe('transmission settings', () => { + it('should use custom tracing transmission settings from config', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + maxBufferedSpans: 13, + forceTransmissionStartingAt: 2, + transmissionDelay: 9753 + } + } + }); + expect(config.tracing.maxBufferedSpans).to.equal(13); + expect(config.tracing.forceTransmissionStartingAt).to.equal(2); + expect(config.tracing.transmissionDelay).to.equal(9753); + }); - it('should use default tracing transmission settings when env vars are non-numerical', () => { - process.env.INSTANA_FORCE_TRANSMISSION_STARTING_AT = 'a2468'; - process.env.INSTANA_TRACING_TRANSMISSION_DELAY = 'x2500'; - const config = coreConfig.normalize(); - expect(config.tracing.forceTransmissionStartingAt).to.equal(500); - expect(config.tracing.transmissionDelay).to.equal(1000); - }); + it('should use custom tracing transmission settings from env vars', () => { + process.env.INSTANA_FORCE_TRANSMISSION_STARTING_AT = '2468'; + process.env.INSTANA_TRACING_TRANSMISSION_DELAY = '2500'; + const config = coreConfig.normalize(); + expect(config.tracing.forceTransmissionStartingAt).to.equal(2468); + expect(config.tracing.transmissionDelay).to.equal(2500); + }); - it('should use extra http headers (and normalize to lower case)', () => { - const config = coreConfig.normalize({ - tracing: { - http: { - extraHttpHeadersToCapture: ['yo', 'LO'] - } - } + it('should use default tracing transmission settings when env vars are non-numerical', () => { + process.env.INSTANA_FORCE_TRANSMISSION_STARTING_AT = 'a2468'; + process.env.INSTANA_TRACING_TRANSMISSION_DELAY = 'x2500'; + const config = coreConfig.normalize(); + expect(config.tracing.forceTransmissionStartingAt).to.equal(500); + expect(config.tracing.transmissionDelay).to.equal(1000); + }); + + it('should give precedence to INSTANA_TRACING_TRANSMISSION_DELAY env var over config', () => { + process.env.INSTANA_TRACING_TRANSMISSION_DELAY = '4000'; + const config = coreConfig.normalize({ userConfig: { tracing: { transmissionDelay: 2000 } } }); + expect(config.tracing.transmissionDelay).to.equal(4000); + }); + + it('should give precedence to INSTANA_FORCE_TRANSMISSION_STARTING_AT env var over config', () => { + process.env.INSTANA_FORCE_TRANSMISSION_STARTING_AT = '700'; + const config = coreConfig.normalize({ userConfig: { tracing: { forceTransmissionStartingAt: 300 } } }); + expect(config.tracing.forceTransmissionStartingAt).to.equal(700); + }); + + it('should fall back to config when env var is invalid for transmissionDelay', () => { + process.env.INSTANA_TRACING_TRANSMISSION_DELAY = 'invalid'; + const config = coreConfig.normalize({ userConfig: { tracing: { transmissionDelay: 5000 } } }); + expect(config.tracing.transmissionDelay).to.equal(5000); + }); + + it('should fall back to default when both env and config are invalid for transmissionDelay', () => { + process.env.INSTANA_TRACING_TRANSMISSION_DELAY = 'invalid'; + const config = coreConfig.normalize({ userConfig: { tracing: { transmissionDelay: 'also-invalid' } } }); + expect(config.tracing.transmissionDelay).to.equal(1000); + }); }); - expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal(['yo', 'lo']); - }); - it('should reject non-array extra http headers configuration value', () => { - const config = coreConfig.normalize({ - tracing: { - http: { - extraHttpHeadersToCapture: 'yolo' - } - } + describe('HTTP headers configuration', () => { + it('should use extra http headers (and normalize to lower case)', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + http: { + extraHttpHeadersToCapture: ['yo', 'LO'] + } + } + } + }); + expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal(['yo', 'lo']); + }); + + it('should reject non-array extra http headers configuration value', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + http: { + extraHttpHeadersToCapture: 'yolo' + } + } + } + }); + expect(config.tracing.http.extraHttpHeadersToCapture).to.be.an('array'); + expect(config.tracing.http.extraHttpHeadersToCapture).to.be.empty; + }); + + it('should parse extra headers from env var', () => { + process.env.INSTANA_EXTRA_HTTP_HEADERS = ' X-Header-1 ; X-hEADer-2 , X-Whatever '; + const config = coreConfig.normalize(); + expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal(['x-header-1', 'x-header-2', 'x-whatever']); + }); + + it('must use default extra headers (empty list) when INSTANA_EXTRA_HTTP_HEADERS is invalid', () => { + process.env.INSTANA_EXTRA_HTTP_HEADERS = ' \n \t '; + const config = coreConfig.normalize(); + expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal([]); + }); }); - expect(config.tracing.http.extraHttpHeadersToCapture).to.be.an('array'); - expect(config.tracing.http.extraHttpHeadersToCapture).to.be.empty; - }); - it('should parse extra headers from env var', () => { - process.env.INSTANA_EXTRA_HTTP_HEADERS = ' X-Header-1 ; X-hEADer-2 , X-Whatever '; - const config = coreConfig.normalize(); - expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal(['x-header-1', 'x-header-2', 'x-whatever']); - }); + describe('stack trace configuration', () => { + it('should accept numerical custom stack trace length', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: 666 } } }); + expect(config.tracing.stackTraceLength).to.equal(500); + }); + it('should normalize numbers for custom stack trace length', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: -28.08 } } }); - it('must use default extra headers (empty list) when INSTANA_EXTRA_HTTP_HEADERS is invalid', () => { - process.env.INSTANA_EXTRA_HTTP_HEADERS = ' \n \t '; - const config = coreConfig.normalize(); - expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal([]); - }); + expect(config.tracing.stackTraceLength).to.be.a('number'); + expect(config.tracing.stackTraceLength).to.equal(28); + }); - it('should accept numerical custom stack trace length', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: 666 } }); - expect(config.tracing.stackTraceLength).to.equal(500); - }); + it('should accept number-like strings for custom stack trace length', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: '1302' } } }); + expect(config.tracing.stackTraceLength).to.be.a('number'); + expect(config.tracing.stackTraceLength).to.equal(500); + }); - it('should normalize numbers for custom stack trace length', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: -28.08 } }); - expect(config.tracing.stackTraceLength).to.be.a('number'); - expect(config.tracing.stackTraceLength).to.equal(28); - }); + it('should normalize number-like strings for custom stack trace length', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: '-16.04' } } }); + expect(config.tracing.stackTraceLength).to.be.a('number'); + expect(config.tracing.stackTraceLength).to.equal(16); + }); - it('should accept number-like strings for custom stack trace length', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: '1302' } }); - expect(config.tracing.stackTraceLength).to.be.a('number'); - expect(config.tracing.stackTraceLength).to.equal(500); - }); + it('should reject non-numerical strings for custom stack trace length', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: 'three' } } }); + expect(config.tracing.stackTraceLength).to.be.a('number'); + expect(config.tracing.stackTraceLength).to.equal(10); + }); - it('should normalize number-like strings for custom stack trace length', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: '-16.04' } }); - expect(config.tracing.stackTraceLength).to.be.a('number'); - expect(config.tracing.stackTraceLength).to.equal(16); - }); + it('should reject custom stack trace length which is neither a number nor a string', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: false } } }); + expect(config.tracing.stackTraceLength).to.be.a('number'); + expect(config.tracing.stackTraceLength).to.equal(10); + }); - it('should reject non-numerical strings for custom stack trace length', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: 'three' } }); - expect(config.tracing.stackTraceLength).to.be.a('number'); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + it('should read stack trace length from INSTANA_STACK_TRACE_LENGTH', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = '3'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(3); + }); - it('should reject custom stack trace length which is neither a number nor a string', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: false } }); - expect(config.tracing.stackTraceLength).to.be.a('number'); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + it('should give precedence to INSTANA_STACK_TRACE_LENGTH over config', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = '5'; + const normalizedConfig = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: 20 } } }); + expect(normalizedConfig.tracing.stackTraceLength).to.equal(5); + delete process.env.INSTANA_STACK_TRACE_LENGTH; + }); - it('should read stack trace length from INSTANA_STACK_TRACE_LENGTH', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = '3'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTraceLength).to.equal(3); - }); + it('should use default stack trace mode', () => { + const config = coreConfig.normalize(); + expect(config.tracing.stackTrace).to.equal('all'); + }); - it('should give precedence to INSTANA_STACK_TRACE_LENGTH over config', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = '5'; - const normalizedConfig = coreConfig.normalize({ tracing: { stackTraceLength: 20 } }); - expect(normalizedConfig.tracing.stackTraceLength).to.equal(5); - delete process.env.INSTANA_STACK_TRACE_LENGTH; - }); + it('should accept valid stack trace mode from config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: 'error' } } } }); + expect(config.tracing.stackTrace).to.equal('error'); + }); - it('should use default stack trace mode', () => { - const config = coreConfig.normalize(); - expect(config.tracing.stackTrace).to.equal('all'); - }); + it('should accept "none" stack trace mode from config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: 'none' } } } }); + expect(config.tracing.stackTrace).to.equal('none'); + }); - it('should accept valid stack trace mode from config', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: 'error' } } }); - expect(config.tracing.stackTrace).to.equal('error'); - }); + it('should normalize stack trace mode to lowercase from config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: 'ERROR' } } } }); + expect(config.tracing.stackTrace).to.equal('error'); + }); - it('should accept "none" stack trace mode from config', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: 'none' } } }); - expect(config.tracing.stackTrace).to.equal('none'); - }); + it('should read stack trace mode from INSTANA_STACK_TRACE', () => { + process.env.INSTANA_STACK_TRACE = 'error'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTrace).to.equal('error'); + }); - it('should normalize stack trace mode to lowercase from config', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: 'ERROR' } } }); - expect(config.tracing.stackTrace).to.equal('error'); - }); + it('should normalize stack trace mode to lowercase from INSTANA_STACK_TRACE', () => { + process.env.INSTANA_STACK_TRACE = 'NONE'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTrace).to.equal('none'); + }); - it('should read stack trace mode from INSTANA_STACK_TRACE', () => { - process.env.INSTANA_STACK_TRACE = 'error'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTrace).to.equal('error'); - }); + it('should give precedence to env INSTANA_STACK_TRACE over config', () => { + process.env.INSTANA_STACK_TRACE = 'none'; + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: 'all' } } } }); + expect(config.tracing.stackTrace).to.equal('none'); + }); - it('should normalize stack trace mode to lowercase from INSTANA_STACK_TRACE', () => { - process.env.INSTANA_STACK_TRACE = 'NONE'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTrace).to.equal('none'); - }); + it('should reject invalid stack trace mode from config and fallback to default', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: 'invalid' } } } }); + expect(config.tracing.stackTrace).to.equal('all'); + }); - it('should give precedence to env INSTANA_STACK_TRACE over config', () => { - process.env.INSTANA_STACK_TRACE = 'none'; - const config = coreConfig.normalize({ tracing: { global: { stackTrace: 'all' } } }); - expect(config.tracing.stackTrace).to.equal('none'); - }); + it('should reject invalid stack trace mode from INSTANA_STACK_TRACE and use default', () => { + process.env.INSTANA_STACK_TRACE = 'invalid'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTrace).to.equal('all'); + }); - it('should reject invalid stack trace mode from config and fallback to default', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: 'invalid' } } }); - expect(config.tracing.stackTrace).to.equal('all'); - }); + it('should reject non-string stack trace mode from config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: 123 } } } }); + expect(config.tracing.stackTrace).to.equal('all'); + }); - it('should reject invalid stack trace mode from INSTANA_STACK_TRACE and use default', () => { - process.env.INSTANA_STACK_TRACE = 'invalid'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTrace).to.equal('all'); - }); + it('should handle null stack trace mode from config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: null } } } }); + expect(config.tracing.stackTrace).to.equal('all'); + }); - it('should reject non-string stack trace mode from config', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: 123 } } }); - expect(config.tracing.stackTrace).to.equal('all'); - }); + it('should handle undefined stack trace mode from config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: undefined } } } }); + expect(config.tracing.stackTrace).to.equal('all'); + }); - it('should handle null stack trace mode from config', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: null } } }); - expect(config.tracing.stackTrace).to.equal('all'); - }); + it('should handle empty string stack trace mode from config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: '' } } } }); + expect(config.tracing.stackTrace).to.equal('all'); + }); - it('should handle undefined stack trace mode from config', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: undefined } } }); - expect(config.tracing.stackTrace).to.equal('all'); - }); + it('should handle boolean stack trace mode from config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: true } } } }); + expect(config.tracing.stackTrace).to.equal('all'); + }); - it('should handle empty string stack trace mode from config', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: '' } } }); - expect(config.tracing.stackTrace).to.equal('all'); - }); + it('should handle object stack trace mode from config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: {} } } } }); + expect(config.tracing.stackTrace).to.equal('all'); + }); - it('should handle boolean stack trace mode from config', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: true } } }); - expect(config.tracing.stackTrace).to.equal('all'); - }); + it('should handle array stack trace mode from config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: ['error'] } } } }); + expect(config.tracing.stackTrace).to.equal('all'); + }); - it('should handle object stack trace mode from config', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: {} } } }); - expect(config.tracing.stackTrace).to.equal('all'); - }); + it('should accept zero as valid stack trace length', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: 0 } } }); + expect(config.tracing.stackTraceLength).to.equal(0); + }); - it('should handle array stack trace mode from config', () => { - const config = coreConfig.normalize({ tracing: { global: { stackTrace: ['error'] } } }); - expect(config.tracing.stackTrace).to.equal('all'); - }); + it('should handle negative stack trace length', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: -10 } } }); + expect(config.tracing.stackTraceLength).to.equal(10); + }); - it('should accept zero as valid stack trace length', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: 0 } }); - expect(config.tracing.stackTraceLength).to.equal(0); - }); + it('should handle very large negative stack trace length', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: -100 } } }); + expect(config.tracing.stackTraceLength).to.equal(100); + }); - it('should handle negative stack trace length', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: -10 } }); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + it('should handle stack trace length as positive float', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: 15.9 } } }); + expect(config.tracing.stackTraceLength).to.equal(16); + }); - it('should handle very large negative stack trace length', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: -100 } }); - expect(config.tracing.stackTraceLength).to.equal(100); - }); + it('should handle stack trace length as negative float', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: -15.9 } } }); + expect(config.tracing.stackTraceLength).to.equal(16); + }); - it('should handle stack trace length as positive float', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: 15.9 } }); - expect(config.tracing.stackTraceLength).to.equal(16); - }); + it('should handle stack trace length as string with leading zeros', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: '007' } } }); + expect(config.tracing.stackTraceLength).to.equal(7); + }); + + it('should handle stack trace length as string with whitespace', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: ' 25 ' } } }); + expect(config.tracing.stackTraceLength).to.equal(25); + }); + + it('should handle stack trace length as string with plus sign', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: '+30' } } }); + expect(config.tracing.stackTraceLength).to.equal(30); + }); + + it('should reject stack trace length as null', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: null } } }); + expect(config.tracing.stackTraceLength).to.equal(10); + }); + + it('should reject stack trace length as undefined', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: undefined } } }); + expect(config.tracing.stackTraceLength).to.equal(10); + }); + + it('should reject stack trace length as empty string', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: '' } } }); + expect(config.tracing.stackTraceLength).to.equal(10); + }); + + it('should reject stack trace length as object', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: {} } } }); + expect(config.tracing.stackTraceLength).to.equal(10); + }); + + it('should reject stack trace length as array', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { stackTraceLength: [10] } } }); + expect(config.tracing.stackTraceLength).to.equal(10); + }); + + it('should handle stack trace length from INSTANA_STACK_TRACE_LENGTH as zero', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = '0'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(0); + }); + + it('should handle stack trace length from INSTANA_STACK_TRACE_LENGTH with negative value', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = '-20'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(20); + }); + + it('should handle stack trace length from INSTANA_STACK_TRACE_LENGTH exceeding max', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = '1000'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(500); + }); + + it('should handle stack trace length from INSTANA_STACK_TRACE_LENGTH as float', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = '12.3'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(12); + }); + + it('should reject invalid INSTANA_STACK_TRACE_LENGTH', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = 'not-a-number'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(10); + }); + + it('should reject empty INSTANA_STACK_TRACE_LENGTH', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = ''; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(10); + }); + + it('should use default when INSTANA_STACK_TRACE passes validation but normalizer returns null', () => { + const stackTraceNormalizers = require('../../src/config/configNormalizers/stackTrace'); + const original = stackTraceNormalizers.normalizeStackTraceModeFromEnv; + stackTraceNormalizers.normalizeStackTraceModeFromEnv = () => null; + + process.env.INSTANA_STACK_TRACE = 'all'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTrace).to.equal('all'); + + stackTraceNormalizers.normalizeStackTraceModeFromEnv = original; + }); + + it('should use default when config stackTrace passes validation but normalizer returns null', () => { + const stackTraceNormalizers = require('../../src/config/configNormalizers/stackTrace'); + const original = stackTraceNormalizers.normalizeStackTraceMode; + stackTraceNormalizers.normalizeStackTraceMode = () => null; + + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTrace: 'all' } } } }); + expect(config.tracing.stackTrace).to.equal('all'); + + stackTraceNormalizers.normalizeStackTraceMode = original; + }); + + it('should use default when INSTANA_STACK_TRACE_LENGTH passes validation but normalizer returns null', () => { + const stackTraceNormalizers = require('../../src/config/configNormalizers/stackTrace'); + const original = stackTraceNormalizers.normalizeStackTraceLengthFromEnv; + stackTraceNormalizers.normalizeStackTraceLengthFromEnv = () => null; + + process.env.INSTANA_STACK_TRACE_LENGTH = '10'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(10); + + stackTraceNormalizers.normalizeStackTraceLengthFromEnv = original; + }); + + it('should use default when config stackTraceLength passes validation but normalizer returns null', () => { + const stackTraceNormalizers = require('../../src/config/configNormalizers/stackTrace'); + const original = stackTraceNormalizers.normalizeStackTraceLength; + stackTraceNormalizers.normalizeStackTraceLength = () => null; + + const config = coreConfig.normalize({ userConfig: { tracing: { global: { stackTraceLength: 20 } } } }); + expect(config.tracing.stackTraceLength).to.equal(10); + + stackTraceNormalizers.normalizeStackTraceLength = original; + }); + + it('should reject INSTANA_STACK_TRACE_LENGTH with only whitespace', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = ' '; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(10); + }); + + it('should handle INSTANA_STACK_TRACE_LENGTH with mixed valid and invalid characters', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = '15abc'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(15); + }); + + it('should return null from normalizeStackTraceLength when value is valid but normalized is null', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + global: { + stackTraceLength: Infinity + } + } + } + }); + expect(config.tracing.stackTraceLength).to.equal(10); + }); + + it('should handle both INSTANA_STACK_TRACE and INSTANA_STACK_TRACE_LENGTH together', () => { + process.env.INSTANA_STACK_TRACE = 'error'; + process.env.INSTANA_STACK_TRACE_LENGTH = '25'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTrace).to.equal('error'); + expect(config.tracing.stackTraceLength).to.equal(25); + }); + + it('should handle config with both stackTrace and stackTraceLength', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + global: { + stackTrace: 'none', + stackTraceLength: 30 + } + } + } + }); + expect(config.tracing.stackTrace).to.equal('none'); + expect(config.tracing.stackTraceLength).to.equal(30); + }); + + it('should give precedence to env vars for both stack trace settings over config', () => { + process.env.INSTANA_STACK_TRACE = 'error'; + process.env.INSTANA_STACK_TRACE_LENGTH = '15'; + const config = coreConfig.normalize({ + userConfig: { + tracing: { + global: { + stackTrace: 'all', + stackTraceLength: 40 + } + } + } + }); + expect(config.tracing.stackTrace).to.equal('error'); + expect(config.tracing.stackTraceLength).to.equal(15); + }); + + it('should use INSTANA_STACK_TRACE_LENGTH when STACK_TRACE_LENGTH is not set', () => { + process.env.INSTANA_STACK_TRACE_LENGTH = '18'; + const config = coreConfig.normalize(); + expect(config.tracing.stackTraceLength).to.equal(18); + delete process.env.INSTANA_STACK_TRACE_LENGTH; + }); + }); + + describe('disabling instrumentations and groups', () => { + it('should not disable individual instrumentations by default', () => { + const config = coreConfig.normalize(); + expect(config.tracing.disable).to.deep.equal({}); + }); + it('should disable individual instrumentations via disable config', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + disable: ['graphQL', 'GRPC'] + } + } + }); + expect(config.tracing.disable.instrumentations).to.deep.equal(['graphql', 'grpc']); + }); + + it('should disable individual instrumentations via disable.instrumentations config', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + disable: { instrumentations: ['graphQL', 'GRPC'] } + } + } + }); + expect(config.tracing.disable.instrumentations).to.deep.equal(['graphql', 'grpc']); + }); + + it('env var INSTANA_TRACING_DISABLE_INSTRUMENTATIONS over config', () => { + process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = 'foo, bar'; + const config = coreConfig.normalize({ + userConfig: { + tracing: { + disable: { instrumentations: ['baz', 'fizz'] } + } + } + }); + expect(config.tracing.disable.instrumentations).to.deep.equal(['foo', 'bar']); + }); + + it('should disable multiple instrumentations via env var INSTANA_TRACING_DISABLE_INSTRUMENTATIONS', () => { + process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = 'graphQL , GRPC, http'; + const config = coreConfig.normalize(); + expect(config.tracing.disable.instrumentations).to.deep.equal(['graphql', 'grpc', 'http']); + }); + + it('should handle single instrumentations via INSTANA_TRACING_DISABLE_INSTRUMENTATIONS', () => { + process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = 'console'; + const config = coreConfig.normalize(); + expect(config.tracing.disable.instrumentations).to.deep.equal(['console']); + }); + + it('should trim whitespace from tracer names', () => { + process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = ' graphql , grpc '; + const config = coreConfig.normalize(); + expect(config.tracing.disable.instrumentations).to.deep.equal(['graphql', 'grpc']); + }); + + it('should disable individual groups via disable config', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + disable: { groups: ['logging'] } + } + } + }); + expect(config.tracing.disable.groups).to.deep.equal(['logging']); + }); + + it('config should disable when env var INSTANA_TRACING_DISABLE_GROUPS is set', () => { + process.env.INSTANA_TRACING_DISABLE_GROUPS = 'frameworks, databases'; + const config = coreConfig.normalize({}); + expect(config.tracing.disable.groups).to.deep.equal(['frameworks', 'databases']); + }); + + it('env var should take precedence over config when disabling groups', () => { + process.env.INSTANA_TRACING_DISABLE_GROUPS = 'frameworks, databases'; + const config = coreConfig.normalize({ + userConfig: { + tracing: { + disable: { groups: ['LOGGING'] } + } + } + }); + expect(config.tracing.disable.groups).to.deep.equal(['frameworks', 'databases']); + }); + + it('should disable instrumentations and groups when both configured', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + disable: { groups: ['LOGGING'], instrumentations: ['redis', 'kafka'] } + } + } + }); + expect(config.tracing.disable.groups).to.deep.equal(['logging']); + expect(config.tracing.disable.instrumentations).to.deep.equal(['redis', 'kafka']); + }); + + it('should disable instrumentations and groups when both env variables provided', () => { + process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = 'redis'; + process.env.INSTANA_TRACING_DISABLE_GROUPS = 'logging'; + const config = coreConfig.normalize(); + expect(config.tracing.disable.instrumentations).to.deep.equal(['redis']); + expect(config.tracing.disable.groups).to.deep.equal(['logging']); + }); + + it('should disable all tracing via INSTANA_TRACING_DISABLE', () => { + process.env.INSTANA_TRACING_DISABLE = true; + const config = coreConfig.normalize(); + expect(config.tracing.enabled).to.be.false; + expect(config.tracing.disable).to.deep.equal({}); + expect(config.tracing.automaticTracingEnabled).to.be.false; + }); + + it('should disable all tracing via config tracing.disable', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + disable: true + } + } + }); + expect(config.tracing.enabled).to.be.false; + expect(config.tracing.disable).to.deep.equal({}); + expect(config.tracing.automaticTracingEnabled).to.be.false; + }); + }); + + describe('span batching', () => { + // delete this test when we switch to opt-out + it('should enable span batching via config in transition phase', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { spanBatchingEnabled: true } } }); + expect(config.tracing.spanBatchingEnabled).to.be.true; + }); + + // delete this test when we switch to opt-out + it('should enable span batching via INSTANA_SPANBATCHING_ENABLED in transition phase', () => { + process.env.INSTANA_SPANBATCHING_ENABLED = 'true'; + const config = coreConfig.normalize(); + expect(config.tracing.spanBatchingEnabled).to.be.true; + }); + + it('should ignore non-boolean span batching config value', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { spanBatchingEnabled: 73 } } }); + // test needs to be updated once we switch to opt-out + expect(config.tracing.spanBatchingEnabled).to.be.false; + }); + + it('should disable span batching', () => { + // test only becomes relevant once we switch to opt-out + const config = coreConfig.normalize({ userConfig: { tracing: { spanBatchingEnabled: false } } }); + expect(config.tracing.spanBatchingEnabled).to.be.false; + }); + + it('should disable span batching via INSTANA_DISABLE_SPANBATCHING', () => { + // test only becomes relevant once we switch to opt-out + process.env.INSTANA_DISABLE_SPANBATCHING = 'true'; + const config = coreConfig.normalize(); + expect(config.tracing.spanBatchingEnabled).to.be.false; + }); + + it('should use default (false) for spanBatchingEnabled when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.spanBatchingEnabled).to.be.false; + }); + + it('should give precedence to INSTANA_SPANBATCHING_ENABLED env var set to true over config set to false', () => { + process.env.INSTANA_SPANBATCHING_ENABLED = 'true'; + const config = coreConfig.normalize({ userConfig: { tracing: { spanBatchingEnabled: false } } }); + expect(config.tracing.spanBatchingEnabled).to.be.true; + }); + + it('should give precedence to INSTANA_SPANBATCHING_ENABLED env var set to false over config set to true', () => { + process.env.INSTANA_SPANBATCHING_ENABLED = 'false'; + const config = coreConfig.normalize({ userConfig: { tracing: { spanBatchingEnabled: true } } }); + expect(config.tracing.spanBatchingEnabled).to.be.false; + }); + }); + + describe('W3C trace correlation', () => { + it('should disable W3C trace correlation', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { disableW3cTraceCorrelation: true } } }); + expect(config.tracing.disableW3cTraceCorrelation).to.be.true; + }); + + it('should disable W3C trace correlation via INSTANA_DISABLE_W3C_TRACE_CORRELATION', () => { + process.env.INSTANA_DISABLE_W3C_TRACE_CORRELATION = 'false'; // any non-empty string will disable, even "false"! + const config = coreConfig.normalize(); + expect(config.tracing.disableW3cTraceCorrelation).to.be.true; + }); + + it('should use default (false) for disableW3cTraceCorrelation when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.disableW3cTraceCorrelation).to.be.false; + }); + + it('should give precedence to INSTANA_DISABLE_W3C_TRACE_CORRELATION env var over config (truthy env)', () => { + process.env.INSTANA_DISABLE_W3C_TRACE_CORRELATION = 'any-value'; + const config = coreConfig.normalize({ userConfig: { tracing: { disableW3cTraceCorrelation: false } } }); + expect(config.tracing.disableW3cTraceCorrelation).to.be.true; + }); + }); + + describe('Kafka trace correlation', () => { + it('should disable Kafka trace correlation', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { kafka: { traceCorrelation: false } } } }); + expect(config.tracing.kafka.traceCorrelation).to.be.false; + }); + + it('should disable Kafka trace correlation via INSTANA_KAFKA_TRACE_CORRELATION', () => { + process.env.INSTANA_KAFKA_TRACE_CORRELATION = 'false'; + const config = coreConfig.normalize(); + expect(config.tracing.kafka.traceCorrelation).to.be.false; + }); + + it('should use default (true) for kafka.traceCorrelation when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.kafka.traceCorrelation).to.be.true; + }); + + it('should give precedence to INSTANA_KAFKA_TRACE_CORRELATION env var set to false over config set to true', () => { + process.env.INSTANA_KAFKA_TRACE_CORRELATION = 'false'; + const config = coreConfig.normalize({ userConfig: { tracing: { kafka: { traceCorrelation: true } } } }); + expect(config.tracing.kafka.traceCorrelation).to.be.false; + }); + + it('should give precedence to INSTANA_KAFKA_TRACE_CORRELATION env var set to true over config set to false', () => { + process.env.INSTANA_KAFKA_TRACE_CORRELATION = 'true'; + const config = coreConfig.normalize({ userConfig: { tracing: { kafka: { traceCorrelation: false } } } }); + expect(config.tracing.kafka.traceCorrelation).to.be.true; + }); + }); + + describe('OpenTelemetry configuration', () => { + it('should disable opentelemetry if config is set', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { useOpentelemetry: false } + } + }); + expect(config.tracing.useOpentelemetry).to.equal(false); + }); + + it('should enable opentelemetry if config is set', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { useOpentelemetry: true } + } + }); + expect(config.tracing.useOpentelemetry).to.equal(true); + }); + + it('should disable opentelemetry if INSTANA_DISABLE_USE_OPENTELEMETRY is set', () => { + process.env.INSTANA_DISABLE_USE_OPENTELEMETRY = 'true'; + const config = coreConfig.normalize(); + expect(config.tracing.useOpentelemetry).to.equal(false); + }); + + it('should enable opentelemetry if INSTANA_DISABLE_USE_OPENTELEMETRY is set', () => { + process.env.INSTANA_DISABLE_USE_OPENTELEMETRY = 'false'; + const config = coreConfig.normalize(); + expect(config.tracing.useOpentelemetry).to.equal(true); + }); + + it('should use default (true) for useOpentelemetry when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.useOpentelemetry).to.be.true; + }); + + it('should give precedence to INSTANA_DISABLE_USE_OPENTELEMETRY env var set to true over config set to true', () => { + process.env.INSTANA_DISABLE_USE_OPENTELEMETRY = 'true'; + const config = coreConfig.normalize({ userConfig: { tracing: { useOpentelemetry: true } } }); + expect(config.tracing.useOpentelemetry).to.be.false; + }); + + it('should give precedence to INSTANA_DISABLE_USE_OPENTELEMETRY env var set to false over config set to false', () => { + process.env.INSTANA_DISABLE_USE_OPENTELEMETRY = 'false'; + const config = coreConfig.normalize({ userConfig: { tracing: { useOpentelemetry: false } } }); + expect(config.tracing.useOpentelemetry).to.be.true; + }); + }); + + describe('secrets configuration', () => { + it('should accept custom secrets config', () => { + const config = coreConfig.normalize({ + userConfig: { + secrets: { + matcherMode: 'equals', + keywords: ['custom-secret', 'sheesh'] + } + } + }); + expect(config.secrets.matcherMode).to.equal('equals'); + expect(config.secrets.keywords).to.deep.equal(['custom-secret', 'sheesh']); + }); + + it("should set keywords to empty array for matcher mode 'none'", () => { + const config = coreConfig.normalize({ + userConfig: { + secrets: { + matcherMode: 'none' + } + } + }); + expect(config.secrets.matcherMode).to.equal('none'); + expect(config.secrets.keywords).to.deep.equal([]); + }); + + it('should reject non-string matcher mode', () => { + const config = coreConfig.normalize({ userConfig: { secrets: { matcherMode: 43 } } }); + expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); + expect(config.secrets.keywords).to.deep.equal(['key', 'pass', 'secret']); + }); + + it('should reject unknown matcher mode from config', () => { + const config = coreConfig.normalize({ userConfig: { secrets: { matcherMode: 'whatever' } } }); + expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); + expect(config.secrets.keywords).to.deep.equal(['key', 'pass', 'secret']); + }); + + it('should reject non-array keywords', () => { + const config = coreConfig.normalize({ userConfig: { secrets: { keywords: 'yes' } } }); + expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); + expect(config.secrets.keywords).to.deep.equal(['key', 'pass', 'secret']); + }); + + it('should parse secrets from env var', () => { + process.env.INSTANA_SECRETS = ' eQuaLs-igNore-case : concealed , hush '; + const config = coreConfig.normalize(); + expect(config.secrets.matcherMode).to.equal('equals-ignore-case'); + expect(config.secrets.keywords).to.deep.equal(['concealed', 'hush']); + }); + + it('must use default secrets when INSTANA_SECRETS is invalid', () => { + process.env.INSTANA_SECRETS = 'whatever'; + const config = coreConfig.normalize(); + expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); + expect(config.secrets.keywords).to.deep.equal(['key', 'pass', 'secret']); + }); + + it("must accept INSTANA_SECRETS without secrets list if matcher mode is 'none'", () => { + process.env.INSTANA_SECRETS = 'NONE'; + const config = coreConfig.normalize(); + expect(config.secrets.matcherMode).to.equal('none'); + expect(config.secrets.keywords).to.deep.equal([]); + }); + + it('should reject unknown matcher mode from INSTANA_SECRETS', () => { + process.env.INSTANA_SECRETS = 'unknown-matcher:nope,never'; + const config = coreConfig.normalize(); + expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); + expect(config.secrets.keywords).to.deep.equal(['nope', 'never']); + }); + }); + + describe('package.json path configuration', () => { + it('should accept packageJsonPath', () => { + const config = coreConfig.normalize({ userConfig: { packageJsonPath: './something' } }); + expect(config.packageJsonPath).to.equal('./something'); + }); + + it('should not accept packageJsonPath', () => { + const config = coreConfig.normalize({ userConfig: { packageJsonPath: 1234 } }); + expect(config.packageJsonPath).to.not.exist; + }); + + it('should accept INSTANA_PACKAGE_JSON_PATH', () => { + process.env.INSTANA_PACKAGE_JSON_PATH = '/my/path'; + const config = coreConfig.normalize({}); + expect(config.packageJsonPath).to.equal('/my/path'); + }); + + it('should use default (null) when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.packageJsonPath).to.be.null; + }); + + it('should give precedence to INSTANA_PACKAGE_JSON_PATH env var over config', () => { + process.env.INSTANA_PACKAGE_JSON_PATH = '/env/path/package.json'; + const config = coreConfig.normalize({ userConfig: { packageJsonPath: '/config/path/package.json' } }); + expect(config.packageJsonPath).to.equal('/env/path/package.json'); + }); + }); + + describe('allow root exit span', () => { + it('should disable allow root exit span if config is set to false', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { allowRootExitSpan: false } + } + }); + expect(config.tracing.allowRootExitSpan).to.equal(false); + }); + + it('should enable allow root exit span if config is set to true', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { allowRootExitSpan: true } + } + }); + expect(config.tracing.allowRootExitSpan).to.equal(true); + }); + it('should disable allow root exit span if INSTANA_ALLOW_ROOT_EXIT_SPAN is not set', () => { + process.env.INSTANA_ALLOW_ROOT_EXIT_SPAN = false; + const config = coreConfig.normalize(); + expect(config.tracing.allowRootExitSpan).to.equal(false); + }); + + it('should enable allow root exit span if INSTANA_ALLOW_ROOT_EXIT_SPAN is set to true', () => { + process.env.INSTANA_ALLOW_ROOT_EXIT_SPAN = true; + const config = coreConfig.normalize(); + expect(config.tracing.allowRootExitSpan).to.equal(true); + }); + + it('should use default (false) for allowRootExitSpan when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.allowRootExitSpan).to.be.false; + }); + + it('should give precedence to INSTANA_ALLOW_ROOT_EXIT_SPAN env var set to true over config set to false', () => { + process.env.INSTANA_ALLOW_ROOT_EXIT_SPAN = 'true'; + const config = coreConfig.normalize({ userConfig: { tracing: { allowRootExitSpan: false } } }); + expect(config.tracing.allowRootExitSpan).to.be.true; + }); + + it('should give precedence to INSTANA_ALLOW_ROOT_EXIT_SPAN env var set to false over config set to true', () => { + process.env.INSTANA_ALLOW_ROOT_EXIT_SPAN = 'false'; + const config = coreConfig.normalize({ userConfig: { tracing: { allowRootExitSpan: true } } }); + expect(config.tracing.allowRootExitSpan).to.be.false; + }); + }); + + describe('ignore endpoints configuration', () => { + it('should not set ignore endpoints tracers by default', () => { + const config = coreConfig.normalize(); + expect(config.tracing.ignoreEndpoints).to.deep.equal({}); + }); + + it('should apply ignore endpoints if the INSTANA_IGNORE_ENDPOINTS is set and valid', () => { + process.env.INSTANA_IGNORE_ENDPOINTS = 'redis:get,set;'; + const config = coreConfig.normalize(); + + expect(config.tracing.ignoreEndpoints).to.deep.equal({ redis: [{ methods: ['get', 'set'] }] }); + }); + + it('should correctly parse INSTANA_IGNORE_ENDPOINTS containing multiple services and endpoints', () => { + process.env.INSTANA_IGNORE_ENDPOINTS = 'redis:get,set; dynamodb:query'; + const config = coreConfig.normalize(); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + redis: [{ methods: ['get', 'set'] }], + dynamodb: [{ methods: ['query'] }] + }); + }); + + it('should fallback to default if INSTANA_IGNORE_ENDPOINTS is set but has an invalid format', () => { + process.env.INSTANA_IGNORE_ENDPOINTS = '"redis=get,set"'; + const config = coreConfig.normalize(); + expect(config.tracing.ignoreEndpoints).to.deep.equal({}); + }); + + it('should apply ignore endpoints via config', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { redis: ['get'] } + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ redis: [{ methods: ['get'] }] }); + }); + it('should apply multiple ignore endpoints via config', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { redis: ['GET', 'TYPE'] } + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ redis: [{ methods: ['get', 'type'] }] }); + }); + it('should apply ignore endpoints via config for multiple packages', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { redis: ['get'], dynamodb: ['querey'] } + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + redis: [{ methods: ['get'] }], + dynamodb: [{ methods: ['querey'] }] + }); + }); + + it('should normalize case and trim spaces in method names and endpoint paths', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { + redis: [' GET ', 'TyPe'], + kafka: [{ methods: [' PUBLISH '], endpoints: [' Topic1 ', 'TOPIC2 '] }] + } + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + redis: [{ methods: ['get', 'type'] }], + kafka: [{ methods: ['publish'], endpoints: ['topic1', 'topic2'] }] + }); + }); + + it('should return an empty list if all configurations are invalid', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { redis: {}, kafka: true, mysql: null } + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + redis: [], + kafka: [], + mysql: [] + }); + }); + + it('should normalize objects when unsupported additional fields applied', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { + redis: [{ extra: 'data' }], + kafka: [{ methods: ['publish'], extra: 'info' }] + } + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + redis: [], + kafka: [{ methods: ['publish'] }] + }); + }); + + it('should normalize objects with only methods and no endpoints', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { + kafka: [{ methods: ['PUBLISH'] }] + } + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + kafka: [{ methods: ['publish'] }] + }); + }); + + it('should normalize objects with only endpoints and no methods', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { + kafka: [{ endpoints: ['Topic1'] }] + } + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + kafka: [{ endpoints: ['topic1'] }] + }); + }); - it('should handle stack trace length as negative float', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: -15.9 } }); - expect(config.tracing.stackTraceLength).to.equal(16); - }); + it('should normalize objects where methods or endpoints are invalid types', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { + kafka: [{ methods: 123, endpoints: 'invalid' }] + } + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({}); + }); - it('should handle stack trace length as string with leading zeros', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: '007' } }); - expect(config.tracing.stackTraceLength).to.equal(7); - }); + it('should handle ignoreEndpoints when config is an array instead of object', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: ['redis', 'kafka'] + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({}); + }); - it('should handle stack trace length as string with whitespace', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: ' 25 ' } }); - expect(config.tracing.stackTraceLength).to.equal(25); - }); + it('should handle ignoreEndpoints when config is a non-object type', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: 'invalid-string' + } + } + }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({}); + }); - it('should handle stack trace length as string with plus sign', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: '+30' } }); - expect(config.tracing.stackTraceLength).to.equal(30); - }); + it('should return false when INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION is not set', () => { + const config = coreConfig.normalize(); + expect(config.tracing.ignoreEndpointsDisableSuppression).to.equal(false); + }); - it('should reject stack trace length as null', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: null } }); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + it('should return true when INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION is set to true', () => { + process.env.INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION = true; + const config = coreConfig.normalize(); + expect(config.tracing.ignoreEndpointsDisableSuppression).to.equal(true); + }); - it('should reject stack trace length as undefined', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: undefined } }); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + it('should use default (false) for ignoreEndpointsDisableSuppression when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.ignoreEndpointsDisableSuppression).to.be.false; + }); - it('should reject stack trace length as empty string', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: '' } }); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + it('should give precedence to INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION env var set to true over config set to false', () => { + process.env.INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION = 'true'; + const config = coreConfig.normalize({ userConfig: { tracing: { ignoreEndpointsDisableSuppression: false } } }); + expect(config.tracing.ignoreEndpointsDisableSuppression).to.be.true; + }); - it('should reject stack trace length as object', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: {} } }); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + it('should give precedence to INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION env var set to false over config set to true', () => { + process.env.INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION = 'false'; + const config = coreConfig.normalize({ userConfig: { tracing: { ignoreEndpointsDisableSuppression: true } } }); + expect(config.tracing.ignoreEndpointsDisableSuppression).to.be.false; + }); - it('should reject stack trace length as array', () => { - const config = coreConfig.normalize({ tracing: { stackTraceLength: [10] } }); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + describe('when testing ignore endpoints reading from INSTANA_IGNORE_ENDPOINTS_PATH env variable', () => { + let filePaths; + + before(() => { + filePaths = setupTestYamlFiles(__dirname); + }); + + after(() => { + cleanupTestYamlFiles(filePaths); + }); + + it('should normalize YAML with "tracing" key', () => { + process.env.INSTANA_IGNORE_ENDPOINTS_PATH = filePaths.tracingYamlPath; + const config = coreConfig.normalize(); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + kafka: [{ methods: ['consume', 'publish'], endpoints: ['topic1', 'topic2'] }] + }); + }); + + it('should normalize YAML with "com.instana.tracing" key', () => { + process.env.INSTANA_IGNORE_ENDPOINTS_PATH = filePaths.comInstanaTracingYamlPath; + const config = coreConfig.normalize(); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + kafka: [{ methods: ['consume', 'publish'], endpoints: ['topic1', 'topic2'] }] + }); + }); + + it('should return an empty object for invalid YAML content', () => { + process.env.INSTANA_IGNORE_ENDPOINTS_PATH = filePaths.invalidYamlPath; + const config = coreConfig.normalize(); + expect(config.tracing.ignoreEndpoints).to.deep.equal({}); + }); + + it('should return an empty object for YAML with missing root keys', () => { + process.env.INSTANA_IGNORE_ENDPOINTS_PATH = filePaths.missingRootKeyYamlPath; + const config = coreConfig.normalize(); + expect(config.tracing.ignoreEndpoints).to.deep.equal({}); + }); + }); + }); - it('should handle stack trace length from INSTANA_STACK_TRACE_LENGTH as zero', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = '0'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTraceLength).to.equal(0); - }); + describe('preloadOpentelemetry', () => { + it('preloadOpentelemetry should default to false', () => { + const config = coreConfig.normalize({}); + expect(config.preloadOpentelemetry).to.be.false; + }); - it('should handle stack trace length from INSTANA_STACK_TRACE_LENGTH with negative value', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = '-20'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTraceLength).to.equal(20); - }); + it('preloadOpentelemetry should accept true value', () => { + const config = coreConfig.normalize({ + userConfig: { + preloadOpentelemetry: true + } + }); + expect(config.preloadOpentelemetry).to.be.true; + }); - it('should handle stack trace length from INSTANA_STACK_TRACE_LENGTH exceeding max', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = '1000'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTraceLength).to.equal(500); - }); + it('preloadOpentelemetry should work with custom defaults', () => { + const customDefaults = { + preloadOpentelemetry: true, + tracing: { + forceTransmissionStartingAt: 25 + } + }; + const config = coreConfig.normalize({ defaultsOverride: customDefaults }); + expect(config.preloadOpentelemetry).to.be.true; + expect(config.tracing.forceTransmissionStartingAt).to.equal(25); + }); + }); - it('should handle stack trace length from INSTANA_STACK_TRACE_LENGTH as float', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = '12.3'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTraceLength).to.equal(12); - }); + describe('EOL events configuration', () => { + it('should return false when INSTANA_TRACING_DISABLE_EOL_EVENTS is set to false', () => { + const config = coreConfig.normalize(); + expect(config.tracing.disableEOLEvents).to.equal(false); + }); - it('should reject invalid INSTANA_STACK_TRACE_LENGTH', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = 'not-a-number'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + it('should return true when INSTANA_TRACING_DISABLE_EOL_EVENTS is set to true', () => { + process.env.INSTANA_TRACING_DISABLE_EOL_EVENTS = 'true'; + const config = coreConfig.normalize(); + expect(config.tracing.disableEOLEvents).to.equal(true); + }); - it('should reject empty INSTANA_STACK_TRACE_LENGTH', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = ''; - const config = coreConfig.normalize(); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + it('should return false when INSTANA_TRACING_DISABLE_EOL_EVENTS is set to false', () => { + process.env.INSTANA_TRACING_DISABLE_EOL_EVENTS = 'false'; + const config = coreConfig.normalize(); + expect(config.tracing.disableEOLEvents).to.equal(false); + }); + it('should return false when INSTANA_TRACING_DISABLE_EOL_EVENTS is set to any other value', () => { + process.env.INSTANA_TRACING_DISABLE_EOL_EVENTS = 'test'; + const config = coreConfig.normalize(); + expect(config.tracing.disableEOLEvents).to.equal(false); + }); - it('should reject INSTANA_STACK_TRACE_LENGTH with only whitespace', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = ' '; - const config = coreConfig.normalize(); - expect(config.tracing.stackTraceLength).to.equal(10); - }); + it('should use default (false) for disableEOLEvents when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.disableEOLEvents).to.be.false; + }); - it('should handle INSTANA_STACK_TRACE_LENGTH with mixed valid and invalid characters', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = '15abc'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTraceLength).to.equal(15); - }); + it('should use config value when env is not set', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { disableEOLEvents: true } } }); + expect(config.tracing.disableEOLEvents).to.be.true; + }); - it('should handle both INSTANA_STACK_TRACE and INSTANA_STACK_TRACE_LENGTH together', () => { - process.env.INSTANA_STACK_TRACE = 'error'; - process.env.INSTANA_STACK_TRACE_LENGTH = '25'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTrace).to.equal('error'); - expect(config.tracing.stackTraceLength).to.equal(25); - }); + it('should give precedence to INSTANA_TRACING_DISABLE_EOL_EVENTS env var set to true over config set to false', () => { + process.env.INSTANA_TRACING_DISABLE_EOL_EVENTS = 'true'; + const config = coreConfig.normalize({ userConfig: { tracing: { disableEOLEvents: false } } }); + expect(config.tracing.disableEOLEvents).to.be.true; + }); - it('should handle config with both stackTrace and stackTraceLength', () => { - const config = coreConfig.normalize({ - tracing: { - global: { - stackTrace: 'none', - stackTraceLength: 30 - } - } + it('should give precedence to INSTANA_TRACING_DISABLE_EOL_EVENTS env var set to false over config set to true', () => { + process.env.INSTANA_TRACING_DISABLE_EOL_EVENTS = 'false'; + const config = coreConfig.normalize({ userConfig: { tracing: { disableEOLEvents: true } } }); + expect(config.tracing.disableEOLEvents).to.be.false; + }); }); - expect(config.tracing.stackTrace).to.equal('none'); - expect(config.tracing.stackTraceLength).to.equal(30); }); - it('should give precedence to env vars for both stack trace settings over config', () => { - process.env.INSTANA_STACK_TRACE = 'error'; - process.env.INSTANA_STACK_TRACE_LENGTH = '15'; - const config = coreConfig.normalize({ - tracing: { - global: { - stackTrace: 'all', - stackTraceLength: 40 - } - } + describe('finalConfigBase parameter', () => { + it('should always preserve finalConfigBase', () => { + const finalConfigBase = { + agentHost: '192.168.1.100', + agentPort: 3000, + agentRequestTimeout: 10000 + }; + const config = coreConfig.normalize({ finalConfigBase }); + expect(config.agentHost).to.equal('192.168.1.100'); + expect(config.agentPort).to.equal(3000); + expect(config.agentRequestTimeout).to.equal(10000); }); - expect(config.tracing.stackTrace).to.equal('error'); - expect(config.tracing.stackTraceLength).to.equal(15); - }); - - it('should use INSTANA_STACK_TRACE_LENGTH when STACK_TRACE_LENGTH is not set', () => { - process.env.INSTANA_STACK_TRACE_LENGTH = '18'; - const config = coreConfig.normalize(); - expect(config.tracing.stackTraceLength).to.equal(18); - delete process.env.INSTANA_STACK_TRACE_LENGTH; - }); - it('should not disable individual instrumentations by default', () => { - const config = coreConfig.normalize(); - expect(config.tracing.disable).to.deep.equal({}); - }); - - it('should disable individual instrumentations via disable config', () => { - const config = coreConfig.normalize({ - tracing: { - disable: ['graphQL', 'GRPC'] - } + it('should merge finalConfigBase with userConfig', () => { + const finalConfigBase = { + agentHost: '192.168.1.100', + agentPort: 3000, + agentRequestTimeout: 5000 + }; + const userConfig = { + serviceName: 'my-app', + tracing: { + enabled: true + } + }; + const config = coreConfig.normalize({ userConfig, finalConfigBase }); + expect(config.agentHost).to.equal('192.168.1.100'); + expect(config.agentPort).to.equal(3000); + expect(config.agentRequestTimeout).to.equal(5000); + expect(config.serviceName).to.equal('my-app'); + expect(config.tracing.enabled).to.be.true; }); - expect(config.tracing.disable.instrumentations).to.deep.equal(['graphql', 'grpc']); - }); - it('should disable individual instrumentations via disable.instrumentations config', () => { - const config = coreConfig.normalize({ - tracing: { - disable: { instrumentations: ['graphQL', 'GRPC'] } - } + it('should work with empty finalConfigBase', () => { + const config = coreConfig.normalize({ finalConfigBase: {} }); + expect(config.serviceName).to.be.null; + expect(config.tracing.enabled).to.be.true; }); - expect(config.tracing.disable.instrumentations).to.deep.equal(['graphql', 'grpc']); - }); - it('config should take precedence over INSTANA_TRACING_DISABLE_INSTRUMENTATIONS for config', () => { - process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = 'foo, bar'; - const config = coreConfig.normalize({ - tracing: { - disable: { instrumentations: ['baz', 'fizz'] } - } + it('should work without finalConfigBase parameter', () => { + const config = coreConfig.normalize({ userConfig: { serviceName: 'test' } }); + expect(config.serviceName).to.equal('test'); }); - expect(config.tracing.disable.instrumentations).to.deep.equal(['baz', 'fizz']); }); - it('should disable multiple instrumentations via env var INSTANA_TRACING_DISABLE_INSTRUMENTATIONS', () => { - process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = 'graphQL , GRPC, http'; - const config = coreConfig.normalize(); - expect(config.tracing.disable.instrumentations).to.deep.equal(['graphql', 'grpc', 'http']); - }); + describe('config.update', () => { + const { CONFIG_SOURCES } = require('../../src/util/constants'); - it('should handle single instrumentations via INSTANA_TRACING_DISABLE_INSTRUMENTATIONS', () => { - process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = 'console'; - const config = coreConfig.normalize(); - expect(config.tracing.disable.instrumentations).to.deep.equal(['console']); - }); + it('should update config from external source when not previously set', () => { + const config = coreConfig.normalize({}); - it('should trim whitespace from tracer names', () => { - process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = ' graphql , grpc '; - const config = coreConfig.normalize(); - expect(config.tracing.disable.instrumentations).to.deep.equal(['graphql', 'grpc']); - }); + coreConfig.update({ + externalConfig: { + serviceName: 'agent-service' + }, + source: CONFIG_SOURCES.AGENT + }); - it('should disable individual groups via disable config', () => { - const config = coreConfig.normalize({ - tracing: { - disable: { groups: ['logging'] } - } + expect(config.serviceName).to.equal('agent-service'); }); - expect(config.tracing.disable.groups).to.deep.equal(['logging']); - }); - it('config should disable when env var INSTANA_TRACING_DISABLE_GROUPS is set', () => { - process.env.INSTANA_TRACING_DISABLE_GROUPS = 'frameworks, databases'; - const config = coreConfig.normalize({}); - expect(config.tracing.disable.groups).to.deep.equal(['frameworks', 'databases']); - }); + it('should not update config from external source when ENV var is set', () => { + process.env.INSTANA_SERVICE_NAME = 'env-service'; + const config = coreConfig.normalize({}); + expect(config.serviceName).to.equal('env-service'); - it('config should take precedence over INSTANA_TRACING_DISABLE_GROUPS when disabling groups', () => { - process.env.INSTANA_TRACING_DISABLE_GROUPS = 'frameworks, databases'; - const config = coreConfig.normalize({ - tracing: { - disable: { groups: ['LOGGING'] } - } - }); - expect(config.tracing.disable.groups).to.deep.equal(['logging']); - }); + coreConfig.update({ + externalConfig: { + serviceName: 'agent-service' + }, + source: CONFIG_SOURCES.AGENT + }); - it('should disable instrumentations and groups when both configured', () => { - const config = coreConfig.normalize({ - tracing: { - disable: { groups: ['LOGGING'], instrumentations: ['redis', 'kafka'] } - } + expect(config.serviceName).to.equal('env-service'); }); - expect(config.tracing.disable.groups).to.deep.equal(['logging']); - expect(config.tracing.disable.instrumentations).to.deep.equal(['redis', 'kafka']); - }); - it('should disable instrumentations and groups when both env variables provided', () => { - process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = 'redis'; - process.env.INSTANA_TRACING_DISABLE_GROUPS = 'logging'; - const config = coreConfig.normalize(); - expect(config.tracing.disable.instrumentations).to.deep.equal(['redis']); - expect(config.tracing.disable.groups).to.deep.equal(['logging']); - }); + it('should not update config from external source when in-code config is set', () => { + const config = coreConfig.normalize({ + userConfig: { + serviceName: 'code-service' + } + }); + expect(config.serviceName).to.equal('code-service'); - it('should disable all tracing via INSTANA_TRACING_DISABLE', () => { - process.env.INSTANA_TRACING_DISABLE = true; - const config = coreConfig.normalize(); - expect(config.tracing.enabled).to.be.false; - expect(config.tracing.disable).to.deep.equal({}); - expect(config.tracing.automaticTracingEnabled).to.be.false; - }); + coreConfig.update({ + externalConfig: { + serviceName: 'agent-service' + }, + source: CONFIG_SOURCES.AGENT + }); - it('should disable all tracing via config tracing.disable', () => { - const config = coreConfig.normalize({ - tracing: { - disable: true - } + expect(config.serviceName).to.equal('code-service'); }); - expect(config.tracing.enabled).to.be.false; - expect(config.tracing.disable).to.deep.equal({}); - expect(config.tracing.automaticTracingEnabled).to.be.false; - }); - - // delete this test when we switch to opt-out - it('should enable span batching via config in transition phase', () => { - const config = coreConfig.normalize({ tracing: { spanBatchingEnabled: true } }); - expect(config.tracing.spanBatchingEnabled).to.be.true; - }); - // delete this test when we switch to opt-out - it('should enable span batching via INSTANA_SPANBATCHING_ENABLED in transition phase', () => { - process.env.INSTANA_SPANBATCHING_ENABLED = 'true'; - const config = coreConfig.normalize(); - expect(config.tracing.spanBatchingEnabled).to.be.true; - }); + it('should update multiple config values from external source', () => { + const config = coreConfig.normalize(); - it('should ignore non-boolean span batching config value', () => { - const config = coreConfig.normalize({ tracing: { spanBatchingEnabled: 73 } }); - // test needs to be updated once we switch to opt-out - expect(config.tracing.spanBatchingEnabled).to.be.false; - }); + coreConfig.update({ + externalConfig: { + serviceName: 'agent-service', + 'metrics.transmissionDelay': 2000 + }, + source: CONFIG_SOURCES.AGENT + }); - it('should disable span batching', () => { - // test only becomes relevant once we switch to opt-out - const config = coreConfig.normalize({ tracing: { spanBatchingEnabled: false } }); - expect(config.tracing.spanBatchingEnabled).to.be.false; - }); + expect(config.serviceName).to.equal('agent-service'); + expect(config['metrics.transmissionDelay']).to.equal(2000); + }); - it('should disable span batching via INSTANA_DISABLE_SPANBATCHING', () => { - // test only becomes relevant once we switch to opt-out - process.env.INSTANA_DISABLE_SPANBATCHING = 'true'; - const config = coreConfig.normalize(); - expect(config.tracing.spanBatchingEnabled).to.be.false; - }); + it('should handle empty external config', () => { + const config = coreConfig.normalize(); + const originalServiceName = config.serviceName; - it('should disable W3C trace correlation', () => { - const config = coreConfig.normalize({ tracing: { disableW3cTraceCorrelation: true } }); - expect(config.tracing.disableW3cTraceCorrelation).to.be.true; - }); + coreConfig.update({ externalConfig: {}, source: CONFIG_SOURCES.AGENT }); - it('should disable W3C trace correlation via INSTANA_DISABLE_W3C_TRACE_CORRELATION', () => { - process.env.INSTANA_DISABLE_W3C_TRACE_CORRELATION = 'false'; // any non-empty string will disable, even "false"! - const config = coreConfig.normalize(); - expect(config.tracing.disableW3cTraceCorrelation).to.be.true; - }); + expect(config.serviceName).to.equal(originalServiceName); + }); - it('should disable Kafka trace correlation', () => { - const config = coreConfig.normalize({ tracing: { kafka: { traceCorrelation: false } } }); - expect(config.tracing.kafka.traceCorrelation).to.be.false; - }); + it('should handle null external config', () => { + const config = coreConfig.normalize(); + const originalServiceName = config.serviceName; - it('should disable Kafka trace correlation via INSTANA_KAFKA_TRACE_CORRELATION', () => { - process.env.INSTANA_KAFKA_TRACE_CORRELATION = 'false'; - const config = coreConfig.normalize(); - expect(config.tracing.kafka.traceCorrelation).to.be.false; - }); + coreConfig.update({ externalConfig: null, source: CONFIG_SOURCES.AGENT }); - it('should disable opentelemetry if config is set', () => { - const config = coreConfig.normalize({ - tracing: { useOpentelemetry: false } + expect(config.serviceName).to.equal(originalServiceName); }); - expect(config.tracing.useOpentelemetry).to.equal(false); - }); - it('should enable opentelemetry if config is set', () => { - const config = coreConfig.normalize({ - tracing: { useOpentelemetry: true } - }); - expect(config.tracing.useOpentelemetry).to.equal(true); - }); + it('should update external config over default values', () => { + const config = coreConfig.normalize(); - it('should disable opentelemetry if INSTANA_DISABLE_USE_OPENTELEMETRY is set', () => { - process.env.INSTANA_DISABLE_USE_OPENTELEMETRY = 'true'; - const config = coreConfig.normalize(); - expect(config.tracing.useOpentelemetry).to.equal(false); - }); + expect(config.serviceName).to.be.null; - it('should enable opentelemetry if INSTANA_DISABLE_USE_OPENTELEMETRY is set', () => { - process.env.INSTANA_DISABLE_USE_OPENTELEMETRY = 'false'; - const config = coreConfig.normalize(); - expect(config.tracing.useOpentelemetry).to.equal(true); - }); + coreConfig.update({ + externalConfig: { + serviceName: 'agent-service' + }, + source: CONFIG_SOURCES.AGENT + }); - it('should accept custom secrets config', () => { - const config = coreConfig.normalize({ - secrets: { - matcherMode: 'equals', - keywords: ['custom-secret', 'sheesh'] - } + expect(config.serviceName).to.equal('agent-service'); }); - expect(config.secrets.matcherMode).to.equal('equals'); - expect(config.secrets.keywords).to.deep.equal(['custom-secret', 'sheesh']); - }); - it("should set keywords to empty array for matcher mode 'none'", () => { - const config = coreConfig.normalize({ - secrets: { - matcherMode: 'none' - } - }); - expect(config.secrets.matcherMode).to.equal('none'); - expect(config.secrets.keywords).to.deep.equal([]); - }); + it('should respect precedence: ENV > IN_CODE > AGENT > DEFAULT', () => { + process.env.INSTANA_SERVICE_NAME = 'env-service'; - it('should reject non-string matcher mode', () => { - const config = coreConfig.normalize({ secrets: { matcherMode: 43 } }); - expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); - expect(config.secrets.keywords).to.deep.equal(['key', 'pass', 'secret']); - }); + const config = coreConfig.normalize({ + userConfig: { + packageJsonPath: '/custom/path' + } + }); - it('should reject unknown matcher mode from config', () => { - const config = coreConfig.normalize({ secrets: { matcherMode: 'whatever' } }); - expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); - expect(config.secrets.keywords).to.deep.equal(['key', 'pass', 'secret']); - }); + expect(config.serviceName).to.equal('env-service'); + expect(config.packageJsonPath).to.equal('/custom/path'); - it('should reject non-array keywords', () => { - const config = coreConfig.normalize({ secrets: { keywords: 'yes' } }); - expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); - expect(config.secrets.keywords).to.deep.equal(['key', 'pass', 'secret']); - }); + coreConfig.update({ + externalConfig: { + serviceName: 'agent-service', + packageJsonPath: '/agent/path', + preloadOpentelemetry: true + }, + source: CONFIG_SOURCES.AGENT + }); - it('should parse secrets from env var', () => { - process.env.INSTANA_SECRETS = ' eQuaLs-igNore-case : concealed , hush '; - const config = coreConfig.normalize(); - expect(config.secrets.matcherMode).to.equal('equals-ignore-case'); - expect(config.secrets.keywords).to.deep.equal(['concealed', 'hush']); - }); + expect(config.serviceName).to.equal('env-service'); - it('must use default secrets when INSTANA_SECRETS is invalid', () => { - process.env.INSTANA_SECRETS = 'whatever'; - const config = coreConfig.normalize(); - expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); - expect(config.secrets.keywords).to.deep.equal(['key', 'pass', 'secret']); - }); + expect(config.packageJsonPath).to.equal('/custom/path'); - it("must accept INSTANA_SECRETS without secrets list if matcher mode is 'none'", () => { - process.env.INSTANA_SECRETS = 'NONE'; - const config = coreConfig.normalize(); - expect(config.secrets.matcherMode).to.equal('none'); - expect(config.secrets.keywords).to.deep.equal([]); - }); + expect(config.preloadOpentelemetry).to.be.true; + }); - it('should reject unknown matcher mode from INSTANA_SECRETS', () => { - process.env.INSTANA_SECRETS = 'unknown-matcher:nope,never'; - const config = coreConfig.normalize(); - expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); - expect(config.secrets.keywords).to.deep.equal(['nope', 'never']); - }); + it('should preserve existing tracing config when external source updates tracing partially', () => { + const config = coreConfig.normalize(); - it('should accept packageJsonPath', () => { - const config = coreConfig.normalize({ packageJsonPath: './something' }); - expect(config.packageJsonPath).to.equal('./something'); - }); + expect(config.tracing.kafka.traceCorrelation).to.be.true; + expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal([]); + + coreConfig.update({ + externalConfig: { + tracing: { + http: { + extraHttpHeadersToCapture: ['x-instana-test'] + } + } + }, + source: CONFIG_SOURCES.AGENT + }); - it('should not accept packageJsonPath', () => { - const config = coreConfig.normalize({ packageJsonPath: 1234 }); - expect(config.packageJsonPath).to.not.exist; - }); + expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal(['x-instana-test']); + expect(config.tracing.kafka).to.deep.equal({ + traceCorrelation: true + }); + expect(config.tracing.kafka.traceCorrelation).to.be.true; + expect(config.tracing.enabled).to.be.true; + expect(config.tracing.spanBatchingEnabled).to.be.false; + }); - it('should accept INSTANA_PACKAGE_JSON_PATH', () => { - process.env.INSTANA_PACKAGE_JSON_PATH = '/my/path'; - const config = coreConfig.normalize({}); - expect(config.packageJsonPath).to.equal('/my/path'); - }); + it('should handle external source updating tracing.disable configuration', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + disable: { + 'http-client': true + } + } + } + }); - it('should disable allow root exit span if config is set to false', () => { - const config = coreConfig.normalize({ - tracing: { allowRootExitSpan: false } - }); - expect(config.tracing.allowRootExitSpan).to.equal(false); - }); + expect(config.tracing.disable).to.deep.equal({ + 'http-client': true + }); + + coreConfig.update({ + externalConfig: { + tracing: { + disable: { + 'http-server': true, + mongodb: true + } + } + }, + source: CONFIG_SOURCES.AGENT + }); - it('should enable allow root exit span if config is set to true', () => { - const config = coreConfig.normalize({ - tracing: { allowRootExitSpan: true } + expect(config.tracing.disable).to.deep.equal({ + 'http-client': true + }); }); - expect(config.tracing.allowRootExitSpan).to.equal(true); - }); - it('should disable allow root exit span if INSTANA_ALLOW_ROOT_EXIT_SPAN is not set', () => { - process.env.INSTANA_ALLOW_ROOT_EXIT_SPAN = false; - const config = coreConfig.normalize(); - expect(config.tracing.allowRootExitSpan).to.equal(false); - }); + it('should handle external source updating tracing.http.extraHttpHeadersToCapture', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + http: { + extraHttpHeadersToCapture: ['x-custom-header'] + } + } + } + }); - it('should enable allow root exit span if INSTANA_ALLOW_ROOT_EXIT_SPAN is set', () => { - process.env.INSTANA_ALLOW_ROOT_EXIT_SPAN = true; - const config = coreConfig.normalize(); - expect(config.tracing.allowRootExitSpan).to.equal(true); - }); - it('should not set ignore endpoints tracers by default', () => { - const config = coreConfig.normalize(); - expect(config.tracing.ignoreEndpoints).to.deep.equal({}); - }); + expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal(['x-custom-header']); + + coreConfig.update({ + externalConfig: { + tracing: { + http: { + extraHttpHeadersToCapture: ['x-agent-header', 'x-trace-id'] + } + } + }, + source: CONFIG_SOURCES.AGENT + }); - it('should apply ignore endpoints if the INSTANA_IGNORE_ENDPOINTS is set and valid', () => { - process.env.INSTANA_IGNORE_ENDPOINTS = 'redis:get,set;'; - const config = coreConfig.normalize(); + expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal(['x-custom-header']); + }); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ redis: [{ methods: ['get', 'set'] }] }); - }); + it('should handle external source updating secrets configuration', () => { + const config = coreConfig.normalize({ + userConfig: { + secrets: { + matcherMode: 'contains-ignore-case', + keywords: ['password', 'secret'] + } + } + }); - it('should correctly parse INSTANA_IGNORE_ENDPOINTS containing multiple services and endpoints', () => { - process.env.INSTANA_IGNORE_ENDPOINTS = 'redis:get,set; dynamodb:query'; - const config = coreConfig.normalize(); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ - redis: [{ methods: ['get', 'set'] }], - dynamodb: [{ methods: ['query'] }] - }); - }); + expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); + expect(config.secrets.keywords).to.deep.equal(['password', 'secret']); - it('should fallback to default if INSTANA_IGNORE_ENDPOINTS is set but has an invalid format', () => { - process.env.INSTANA_IGNORE_ENDPOINTS = '"redis=get,set"'; - const config = coreConfig.normalize(); - expect(config.tracing.ignoreEndpoints).to.deep.equal({}); - }); + coreConfig.update({ + externalConfig: { + secrets: { + keywords: ['token', 'apikey', 'auth'] + } + }, + source: CONFIG_SOURCES.AGENT + }); - it('should apply ignore endpoints via config', () => { - const config = coreConfig.normalize({ - tracing: { - ignoreEndpoints: { redis: ['get'] } - } - }); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ redis: [{ methods: ['get'] }] }); - }); - it('should apply multiple ignore endpoints via config', () => { - const config = coreConfig.normalize({ - tracing: { - ignoreEndpoints: { redis: ['GET', 'TYPE'] } - } - }); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ redis: [{ methods: ['get', 'type'] }] }); - }); - it('should apply ignore endpoints via config for multiple packages', () => { - const config = coreConfig.normalize({ - tracing: { - ignoreEndpoints: { redis: ['get'], dynamodb: ['querey'] } - } + expect(config.secrets.matcherMode).to.equal('contains-ignore-case'); + expect(config.secrets.keywords).to.deep.equal(['password', 'secret']); }); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ - redis: [{ methods: ['get'] }], - dynamodb: [{ methods: ['querey'] }] - }); - }); - it('should normalize case and trim spaces in method names and endpoint paths', () => { - const config = coreConfig.normalize({ - tracing: { - ignoreEndpoints: { - redis: [' GET ', 'TyPe'], - kafka: [{ methods: [' PUBLISH '], endpoints: [' Topic1 ', 'TOPIC2 '] }] + it('should ignore external source updating tracing.ignoreEndpoints configuration when already set', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { + redis: ['get', 'set'] + } + } } - } - }); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ - redis: [{ methods: ['get', 'type'] }], - kafka: [{ methods: ['publish'], endpoints: ['topic1', 'topic2'] }] - }); - }); + }); - it('should return an empty list if all configurations are invalid', () => { - const config = coreConfig.normalize({ - tracing: { - ignoreEndpoints: { redis: {}, kafka: true, mysql: null } - } - }); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ - redis: [], - kafka: [], - mysql: [] - }); - }); + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + redis: [{ methods: ['get', 'set'] }] + }); - it('should normalize objects when unsupported additional fields applied', () => { - const config = coreConfig.normalize({ - tracing: { - ignoreEndpoints: { - redis: [{ extra: 'data' }], - kafka: [{ methods: ['publish'], extra: 'info' }] - } - } - }); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ - redis: [], - kafka: [{ methods: ['publish'] }] - }); - }); + coreConfig.update({ + externalConfig: { + tracing: { + ignoreEndpoints: { + dynamodb: [{ methods: ['query'] }], + mongodb: [{ methods: ['find'] }] + } + } + }, + source: CONFIG_SOURCES.AGENT + }); - it('should normalize objects with only methods and no endpoints', () => { - const config = coreConfig.normalize({ - tracing: { - ignoreEndpoints: { - kafka: [{ methods: ['PUBLISH'] }] - } - } - }); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ - kafka: [{ methods: ['publish'] }] + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + redis: [{ methods: ['get', 'set'] }] + }); }); - }); - it('should normalize objects with only endpoints and no methods', () => { - const config = coreConfig.normalize({ - tracing: { - ignoreEndpoints: { - kafka: [{ endpoints: ['Topic1'] }] + it('should use external config value dor tracing.ignoreEndpoints configuration when not set', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: {} } - } - }); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ - kafka: [{ endpoints: ['topic1'] }] - }); - }); + }); - it('should normalize objects where methods or endpoints are invalid types', () => { - const config = coreConfig.normalize({ - tracing: { - ignoreEndpoints: { - kafka: [{ methods: 123, endpoints: 'invalid' }] - } - } + coreConfig.update({ + externalConfig: { + tracing: { + ignoreEndpoints: { + dynamodb: [{ methods: ['query'] }], + mongodb: [{ methods: ['find'] }] + } + } + }, + source: CONFIG_SOURCES.AGENT + }); + + expect(config.tracing.ignoreEndpoints).to.deep.equal({ + dynamodb: [{ methods: ['query'] }], + mongodb: [{ methods: ['find'] }] + }); }); - expect(config.tracing.ignoreEndpoints).to.deep.equal({}); - }); - it('preloadOpentelemetry should default to false', () => { - const config = coreConfig.normalize({}); - expect(config.preloadOpentelemetry).to.be.false; - }); + it('should handle external source updating stackTrace configuration when not set in-code', () => { + const config = coreConfig.normalize(); + + expect(config.tracing.stackTrace).to.equal('all'); + expect(config.tracing.stackTraceLength).to.equal(10); + + coreConfig.update({ + externalConfig: { + tracing: { + stackTrace: 'on-error', + stackTraceLength: 15 + } + }, + source: CONFIG_SOURCES.AGENT + }); - it('preloadOpentelemetry should accept true value', () => { - const config = coreConfig.normalize({ - preloadOpentelemetry: true + expect(config.tracing.stackTrace).to.equal('on-error'); + expect(config.tracing.stackTraceLength).to.equal(15); }); - expect(config.preloadOpentelemetry).to.be.true; - }); - it('preloadOpentelemetry should work with custom defaults', () => { - const customDefaults = { - preloadOpentelemetry: true, - tracing: { - forceTransmissionStartingAt: 25 - } - }; - const config = coreConfig.normalize({}, customDefaults); - expect(config.preloadOpentelemetry).to.be.true; - expect(config.tracing.forceTransmissionStartingAt).to.equal(25); - }); + it('should not override in-code config with external config when in-code has higher priority', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + enabled: false, + stackTrace: 'never' + } + } + }); - describe('when testing ignore endpoints reading from INSTANA_IGNORE_ENDPOINTS_PATH env variable', () => { - let filePaths; + expect(config.tracing.enabled).to.be.false; + expect(config.tracing.stackTrace).to.equal('all'); + + coreConfig.update({ + externalConfig: { + tracing: { + enabled: true, + stackTrace: 'all' + } + }, + source: CONFIG_SOURCES.AGENT + }); - before(() => { - filePaths = setupTestYamlFiles(__dirname); + expect(config.tracing.enabled).to.be.false; + expect(config.tracing.stackTrace).to.equal('all'); }); - after(() => { - cleanupTestYamlFiles(filePaths); - }); + it('should handle complex nested config updates from external source', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + http: { + extraHttpHeadersToCapture: ['x-custom'] + }, + kafka: { + traceCorrelation: false + } + } + } + }); - it('should normalize YAML with "tracing" key', () => { - process.env.INSTANA_IGNORE_ENDPOINTS_PATH = filePaths.tracingYamlPath; - const config = coreConfig.normalize(); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ - kafka: [{ methods: ['consume', 'publish'], endpoints: ['topic1', 'topic2'] }] + expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal(['x-custom']); + expect(config.tracing.kafka.traceCorrelation).to.be.false; + + coreConfig.update({ + externalConfig: { + tracing: { + http: { + extraHttpHeadersToCapture: ['x-agent-header'] + }, + stackTrace: 'on-error', + stackTraceLength: 25, + disable: { + redis: true + } + } + }, + source: CONFIG_SOURCES.AGENT }); + + expect(config.tracing.http.extraHttpHeadersToCapture).to.deep.equal(['x-custom']); + expect(config.tracing.stackTrace).to.equal('on-error'); + expect(config.tracing.stackTraceLength).to.equal(25); + expect(config.tracing.disable.redis).to.be.true; + + expect(config.tracing.kafka.traceCorrelation).to.be.false; }); - it('should normalize YAML with "com.instana.tracing" key', () => { - process.env.INSTANA_IGNORE_ENDPOINTS_PATH = filePaths.comInstanaTracingYamlPath; + it('should handle external source disabling tracing when not set in-code', () => { const config = coreConfig.normalize(); - expect(config.tracing.ignoreEndpoints).to.deep.equal({ - kafka: [{ methods: ['consume', 'publish'], endpoints: ['topic1', 'topic2'] }] + + expect(config.tracing.enabled).to.be.true; + + coreConfig.update({ + externalConfig: { + tracing: { + enabled: false + } + }, + source: CONFIG_SOURCES.AGENT }); - }); - it('should return an empty object for invalid YAML content', () => { - process.env.INSTANA_IGNORE_ENDPOINTS_PATH = filePaths.invalidYamlPath; - const config = coreConfig.normalize(); - expect(config.tracing.ignoreEndpoints).to.deep.equal({}); + expect(config.tracing.enabled).to.be.false; }); - it('should return an empty object for YAML with missing root keys', () => { - process.env.INSTANA_IGNORE_ENDPOINTS_PATH = filePaths.missingRootKeyYamlPath; + it('should handle external source updating tracing.ignoreEndpointsDisableSuppression', () => { const config = coreConfig.normalize(); - expect(config.tracing.ignoreEndpoints).to.deep.equal({}); - }); - it('should return false when INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION is not set', () => { - const config = coreConfig.normalize(); - expect(config.tracing.ignoreEndpointsDisableSuppression).to.equal(false); - }); + expect(config.tracing.ignoreEndpointsDisableSuppression).to.be.false; - it('should return true when INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION is set', () => { - process.env.INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION = true; - const config = coreConfig.normalize(); - expect(config.tracing.ignoreEndpointsDisableSuppression).to.equal(true); - }); + coreConfig.update({ + externalConfig: { + tracing: { + ignoreEndpointsDisableSuppression: true + } + }, + source: CONFIG_SOURCES.AGENT + }); - it('should return false when INSTANA_TRACING_DISABLE_EOL_EVENTS is not set', () => { - const config = coreConfig.normalize(); - expect(config.tracing.disableEOLEvents).to.equal(false); + expect(config.tracing.ignoreEndpointsDisableSuppression).to.be.true; }); - it('should return true when INSTANA_TRACING_DISABLE_EOL_EVENTS is set to true', () => { - process.env.INSTANA_TRACING_DISABLE_EOL_EVENTS = 'true'; - const config = coreConfig.normalize(); - expect(config.tracing.disableEOLEvents).to.equal(true); - }); + it('should preserve existing tracing.ignoreEndpoints when external source updates disableSuppression', () => { + const config = coreConfig.normalize({ + userConfig: { + tracing: { + ignoreEndpoints: { + redis: ['get'], + mongodb: ['find'] + } + } + } + }); - it('should return false when INSTANA_TRACING_DISABLE_EOL_EVENTS is set to false', () => { - process.env.INSTANA_TRACING_DISABLE_EOL_EVENTS = 'false'; - const config = coreConfig.normalize(); - expect(config.tracing.disableEOLEvents).to.equal(false); - }); + expect(config.tracing.ignoreEndpoints.redis).to.deep.equal([{ methods: ['get'] }]); + expect(config.tracing.ignoreEndpoints.mongodb).to.deep.equal([{ methods: ['find'] }]); - it('should return false when INSTANA_TRACING_DISABLE_EOL_EVENTS is set to any other value', () => { - process.env.INSTANA_TRACING_DISABLE_EOL_EVENTS = 'test'; - const config = coreConfig.normalize(); - expect(config.tracing.disableEOLEvents).to.equal(false); + coreConfig.update({ + externalConfig: { + tracing: { + ignoreEndpointsDisableSuppression: true + } + }, + source: CONFIG_SOURCES.AGENT + }); + + expect(config.tracing.ignoreEndpoints.redis).to.deep.equal([{ methods: ['get'] }]); + expect(config.tracing.ignoreEndpoints.mongodb).to.deep.equal([{ methods: ['find'] }]); + expect(config.tracing.ignoreEndpointsDisableSuppression).to.be.true; }); }); diff --git a/packages/core/test/config/util_test.js b/packages/core/test/config/util_test.js new file mode 100644 index 0000000000..313b4f2dbd --- /dev/null +++ b/packages/core/test/config/util_test.js @@ -0,0 +1,617 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +const expect = require('chai').expect; +const { createFakeLogger } = require('../test_util'); +const util = require('../../src/config/util'); +const validate = require('../../src/config/validator'); +const { CONFIG_SOURCES } = require('../../src/util/constants'); + +describe('config.util', () => { + let logger; + + before(() => { + logger = createFakeLogger(); + util.init(logger); + }); + + beforeEach(resetEnv); + afterEach(resetEnv); + + function resetEnv() { + delete process.env.TEST_ENV_VAR; + delete process.env.TEST_BOOL_VAR; + delete process.env.TEST_STRING_VAR; + delete process.env.TEST_TRUTHY_VAR; + } + + describe('resolve - priority order (env > inCode > agent > default)', () => { + it('should prioritize env over inCode, agent, and default', () => { + process.env.TEST_ENV_VAR = '100'; + + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: 200, + agentValue: 300, + defaultValue: 400 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 100, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should prioritize inCode over agent and default when env is not set', () => { + const result = util.resolve( + { + inCodeValue: 200, + agentValue: 300, + defaultValue: 400 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 200, + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should prioritize agent over default when env and inCode are not set', () => { + const result = util.resolve( + { + agentValue: 300, + defaultValue: 400 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 300, + source: CONFIG_SOURCES.AGENT + }); + }); + + it('should use default when env, inCode, and agent are not set', () => { + const result = util.resolve( + { + defaultValue: 400 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 400, + source: CONFIG_SOURCES.DEFAULT + }); + }); + }); + + describe('resolve with numberValidator', () => { + it('should parse numeric string from env var', () => { + process.env.TEST_ENV_VAR = '7500'; + + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: undefined, + defaultValue: 1000 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 7500, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should parse numeric string from inCode value', () => { + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: '5000', + defaultValue: 1000 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 5000, + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should handle zero as valid value', () => { + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: 0, + defaultValue: 1000 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 0, + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should handle negative numbers', () => { + process.env.TEST_ENV_VAR = '-500'; + + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: undefined, + defaultValue: 1000 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: -500, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should handle floating point numbers', () => { + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: 123.45, + defaultValue: 1000 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 123.45, + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should handle empty string as 0', () => { + process.env.TEST_ENV_VAR = ''; + + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: undefined, + defaultValue: 1000 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 0, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should fall back to next priority when env var is invalid', () => { + process.env.TEST_ENV_VAR = 'not-a-number'; + + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: 3000, + defaultValue: 1000 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 3000, + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should fall back to default when all values are invalid', () => { + process.env.TEST_ENV_VAR = 'not-a-number'; + + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: 'invalid', + defaultValue: 1000 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 1000, + source: CONFIG_SOURCES.DEFAULT + }); + }); + + it('should treat null as undefined', () => { + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: null, + defaultValue: 1000 + }, + validate.numberValidator + ); + + expect(result).to.deep.equal({ + value: 1000, + source: CONFIG_SOURCES.DEFAULT + }); + }); + }); + + describe('resolve with booleanValidator', () => { + it('should parse "true" from env var', () => { + process.env.TEST_BOOL_VAR = 'true'; + + const result = util.resolve( + { + envValue: 'TEST_BOOL_VAR', + inCodeValue: undefined, + defaultValue: false + }, + validate.booleanValidator + ); + + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should parse "false" from env var', () => { + process.env.TEST_BOOL_VAR = 'false'; + + const result = util.resolve( + { + envValue: 'TEST_BOOL_VAR', + inCodeValue: undefined, + defaultValue: true + }, + validate.booleanValidator + ); + + expect(result).to.deep.equal({ + value: false, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should parse "1" as true', () => { + process.env.TEST_BOOL_VAR = '1'; + + const result = util.resolve( + { + envValue: 'TEST_BOOL_VAR', + inCodeValue: undefined, + defaultValue: false + }, + validate.booleanValidator + ); + + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should parse "0" as false', () => { + process.env.TEST_BOOL_VAR = '0'; + + const result = util.resolve( + { + envValue: 'TEST_BOOL_VAR', + inCodeValue: undefined, + defaultValue: true + }, + validate.booleanValidator + ); + + expect(result).to.deep.equal({ + value: false, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should handle case-insensitive values', () => { + process.env.TEST_BOOL_VAR = 'TRUE'; + + const result = util.resolve( + { + envValue: 'TEST_BOOL_VAR', + inCodeValue: undefined, + defaultValue: false + }, + validate.booleanValidator + ); + + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should handle boolean inCode values', () => { + const result = util.resolve( + { + envValue: 'TEST_BOOL_VAR', + inCodeValue: true, + defaultValue: false + }, + validate.booleanValidator + ); + + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should fall back to next priority when env var is invalid', () => { + process.env.TEST_BOOL_VAR = 'invalid'; + + const result = util.resolve( + { + envValue: 'TEST_BOOL_VAR', + inCodeValue: true, + defaultValue: false + }, + validate.booleanValidator + ); + + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should fall back to default when all values are invalid', () => { + process.env.TEST_BOOL_VAR = 'invalid'; + + const result = util.resolve( + { + envValue: 'TEST_BOOL_VAR', + inCodeValue: 'not-a-boolean', + defaultValue: true + }, + validate.booleanValidator + ); + + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.DEFAULT + }); + }); + }); + + describe('resolve with validateTruthyBoolean', () => { + it('should return true for any truthy env var value', () => { + process.env.TEST_TRUTHY_VAR = 'any-value'; + + const result = util.resolve( + { + envValue: 'TEST_TRUTHY_VAR', + inCodeValue: undefined, + defaultValue: false + }, + validate.validateTruthyBoolean + ); + + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.ENV + }); + }); + + it('should return true for truthy inCode value', () => { + const result = util.resolve( + { + envValue: 'TEST_TRUTHY_VAR', + inCodeValue: true, + defaultValue: false + }, + validate.validateTruthyBoolean + ); + + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should fall back to default for empty string env var', () => { + process.env.TEST_TRUTHY_VAR = ''; + + const result = util.resolve( + { + envValue: 'TEST_TRUTHY_VAR', + inCodeValue: undefined, + defaultValue: false + }, + validate.validateTruthyBoolean + ); + + expect(result).to.deep.equal({ + value: false, + source: CONFIG_SOURCES.DEFAULT + }); + }); + + it('should fall back to default for falsy inCode value', () => { + const result = util.resolve( + { + envValue: 'TEST_TRUTHY_VAR', + inCodeValue: false, + defaultValue: true + }, + validate.validateTruthyBoolean + ); + + expect(result).to.deep.equal({ + value: true, + source: CONFIG_SOURCES.DEFAULT + }); + }); + }); + + describe('resolve with stringValidator', () => { + it('should use string from env var', () => { + process.env.TEST_STRING_VAR = 'env-value'; + + const result = util.resolve( + { + envValue: 'TEST_STRING_VAR', + inCodeValue: undefined, + defaultValue: 'default-value' + }, + validate.stringValidator + ); + + expect(result).to.deep.equal({ + value: 'env-value', + source: CONFIG_SOURCES.ENV + }); + }); + + it('should use string from inCode value', () => { + const result = util.resolve( + { + envValue: 'TEST_STRING_VAR', + inCodeValue: 'config-value', + defaultValue: 'default-value' + }, + validate.stringValidator + ); + + expect(result).to.deep.equal({ + value: 'config-value', + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should handle empty string as valid value', () => { + process.env.TEST_STRING_VAR = ''; + + const result = util.resolve( + { + envValue: 'TEST_STRING_VAR', + inCodeValue: undefined, + defaultValue: 'default-value' + }, + validate.stringValidator + ); + + expect(result).to.deep.equal({ + value: '', + source: CONFIG_SOURCES.ENV + }); + }); + + it('should handle multiline strings', () => { + const multilineValue = 'line1\nline2\nline3'; + const result = util.resolve( + { + envValue: 'TEST_STRING_VAR', + inCodeValue: multilineValue, + defaultValue: 'default-value' + }, + validate.stringValidator + ); + + expect(result).to.deep.equal({ + value: multilineValue, + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should reject non-string values and fall back', () => { + const result = util.resolve( + { + envValue: 'TEST_STRING_VAR', + inCodeValue: 123, + defaultValue: 'default-value' + }, + validate.stringValidator + ); + + expect(result).to.deep.equal({ + value: 'default-value', + source: CONFIG_SOURCES.DEFAULT + }); + }); + + it('should treat null as undefined', () => { + const result = util.resolve( + { + envValue: 'TEST_STRING_VAR', + inCodeValue: null, + defaultValue: 'default-value' + }, + validate.stringValidator + ); + + expect(result).to.deep.equal({ + value: 'default-value', + source: CONFIG_SOURCES.DEFAULT + }); + }); + }); + + describe('resolve with multiple validators', () => { + it('should apply validators in sequence', () => { + const customValidator = value => { + if (typeof value === 'number' && value > 100) { + return value; + } + return undefined; + }; + + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: 150, + defaultValue: 50 + }, + [validate.numberValidator, customValidator] + ); + + expect(result).to.deep.equal({ + value: 150, + source: CONFIG_SOURCES.INCODE + }); + }); + + it('should fall back when any validator in chain fails', () => { + const customValidator = value => { + if (typeof value === 'number' && value > 100) { + return value; + } + return undefined; + }; + + const result = util.resolve( + { + envValue: 'TEST_ENV_VAR', + inCodeValue: 50, + defaultValue: 200 + }, + [validate.numberValidator, customValidator] + ); + + expect(result).to.deep.equal({ + value: 200, + source: CONFIG_SOURCES.DEFAULT + }); + }); + }); +}); diff --git a/packages/core/test/config/validator_test.js b/packages/core/test/config/validator_test.js new file mode 100644 index 0000000000..6a5204b94e --- /dev/null +++ b/packages/core/test/config/validator_test.js @@ -0,0 +1,175 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +const expect = require('chai').expect; +const validator = require('../../src/config/validator'); + +describe('config.validator', () => { + describe('numberValidator', () => { + it('should return number for valid numeric input', () => { + expect(validator.numberValidator(123)).to.equal(123); + expect(validator.numberValidator(0)).to.equal(0); + expect(validator.numberValidator(-456)).to.equal(-456); + expect(validator.numberValidator(123.45)).to.equal(123.45); + }); + + it('should parse numeric strings', () => { + expect(validator.numberValidator('123')).to.equal(123); + expect(validator.numberValidator('0')).to.equal(0); + expect(validator.numberValidator('-456')).to.equal(-456); + expect(validator.numberValidator('123.45')).to.equal(123.45); + }); + + it('should handle empty string as 0', () => { + expect(validator.numberValidator('')).to.equal(0); + }); + + it('should return undefined for null', () => { + expect(validator.numberValidator(null)).to.be.undefined; + }); + + it('should return undefined for undefined', () => { + expect(validator.numberValidator(undefined)).to.be.undefined; + }); + + it('should return undefined for non-numeric strings', () => { + expect(validator.numberValidator('abc')).to.be.undefined; + expect(validator.numberValidator('12abc')).to.be.undefined; + expect(validator.numberValidator('not-a-number')).to.be.undefined; + }); + + it('should return undefined for NaN', () => { + expect(validator.numberValidator(NaN)).to.be.undefined; + }); + + it('should handle Infinity', () => { + expect(validator.numberValidator(Infinity)).to.equal(Infinity); + expect(validator.numberValidator(-Infinity)).to.equal(-Infinity); + }); + }); + + describe('booleanValidator', () => { + it('should return boolean for valid boolean input', () => { + expect(validator.booleanValidator(true)).to.equal(true); + expect(validator.booleanValidator(false)).to.equal(false); + }); + + it('should parse "true" string as true', () => { + expect(validator.booleanValidator('true')).to.equal(true); + expect(validator.booleanValidator('TRUE')).to.equal(true); + expect(validator.booleanValidator('True')).to.equal(true); + }); + + it('should parse "false" string as false', () => { + expect(validator.booleanValidator('false')).to.equal(false); + expect(validator.booleanValidator('FALSE')).to.equal(false); + expect(validator.booleanValidator('False')).to.equal(false); + }); + + it('should parse "1" as true', () => { + expect(validator.booleanValidator('1')).to.equal(true); + }); + + it('should parse "0" as false', () => { + expect(validator.booleanValidator('0')).to.equal(false); + }); + + it('should return undefined for null', () => { + expect(validator.booleanValidator(null)).to.be.undefined; + }); + + it('should return undefined for undefined', () => { + expect(validator.booleanValidator(undefined)).to.be.undefined; + }); + + it('should return undefined for invalid strings', () => { + expect(validator.booleanValidator('yes')).to.be.undefined; + expect(validator.booleanValidator('no')).to.be.undefined; + expect(validator.booleanValidator('invalid')).to.be.undefined; + expect(validator.booleanValidator('')).to.be.undefined; + }); + + it('should return undefined for numbers other than 0 and 1', () => { + expect(validator.booleanValidator(2)).to.be.undefined; + expect(validator.booleanValidator(-1)).to.be.undefined; + expect(validator.booleanValidator(123)).to.be.undefined; + }); + + it('should return undefined for objects', () => { + expect(validator.booleanValidator({})).to.be.undefined; + expect(validator.booleanValidator([])).to.be.undefined; + }); + }); + + describe('stringValidator', () => { + it('should return string for valid string input', () => { + expect(validator.stringValidator('hello')).to.equal('hello'); + expect(validator.stringValidator('world')).to.equal('world'); + expect(validator.stringValidator('')).to.equal(''); + }); + + it('should handle empty string', () => { + expect(validator.stringValidator('')).to.equal(''); + }); + + it('should handle multiline strings', () => { + const multiline = 'line1\nline2\nline3'; + expect(validator.stringValidator(multiline)).to.equal(multiline); + }); + + it('should handle strings with special characters', () => { + expect(validator.stringValidator('hello@world.com')).to.equal('hello@world.com'); + expect(validator.stringValidator('path/to/file')).to.equal('path/to/file'); + expect(validator.stringValidator('key=value')).to.equal('key=value'); + }); + + it('should return undefined for null', () => { + expect(validator.stringValidator(null)).to.be.undefined; + }); + + it('should return undefined for undefined', () => { + expect(validator.stringValidator(undefined)).to.be.undefined; + }); + + it('should return undefined for numbers', () => { + expect(validator.stringValidator(123)).to.be.undefined; + expect(validator.stringValidator(0)).to.be.undefined; + expect(validator.stringValidator(-456)).to.be.undefined; + }); + + it('should return undefined for booleans', () => { + expect(validator.stringValidator(true)).to.be.undefined; + expect(validator.stringValidator(false)).to.be.undefined; + }); + + it('should return undefined for objects', () => { + expect(validator.stringValidator({})).to.be.undefined; + expect(validator.stringValidator([])).to.be.undefined; + }); + }); + + describe('validateTruthyBoolean', () => { + it('should return true for truthy values', () => { + expect(validator.validateTruthyBoolean(true)).to.equal(true); + expect(validator.validateTruthyBoolean(1)).to.equal(true); + expect(validator.validateTruthyBoolean('any-string')).to.equal(true); + expect(validator.validateTruthyBoolean('true')).to.equal(true); + expect(validator.validateTruthyBoolean('false')).to.equal(true); + expect(validator.validateTruthyBoolean({})).to.equal(true); + expect(validator.validateTruthyBoolean([])).to.equal(true); + expect(validator.validateTruthyBoolean(123)).to.equal(true); + }); + + it('should return undefined for falsy values', () => { + expect(validator.validateTruthyBoolean(false)).to.be.undefined; + expect(validator.validateTruthyBoolean(0)).to.be.undefined; + expect(validator.validateTruthyBoolean('')).to.be.undefined; + expect(validator.validateTruthyBoolean(null)).to.be.undefined; + expect(validator.validateTruthyBoolean(undefined)).to.be.undefined; + expect(validator.validateTruthyBoolean(NaN)).to.be.undefined; + }); + }); +}); diff --git a/packages/core/test/tracing/index_test.js b/packages/core/test/tracing/index_test.js index 515a616bbd..14a39885f8 100644 --- a/packages/core/test/tracing/index_test.js +++ b/packages/core/test/tracing/index_test.js @@ -155,8 +155,8 @@ mochaSuiteFn('[UNIT] tracing/index', function () { } }; initAndActivate({}, extraConfigFromAgent); - expect(activateStubKafkaJs).to.have.been.calledWith(extraConfigFromAgent); - expect(activateStubRdKafka).to.have.been.calledWith(extraConfigFromAgent); + expect(activateStubKafkaJs).to.have.been.called; + expect(activateStubRdKafka).to.have.been.called; }); it('should disable aws-sdk/v3 via config', () => { @@ -193,18 +193,18 @@ mochaSuiteFn('[UNIT] tracing/index', function () { expect(activateStubRdKafka).to.have.been.called; }); - it('should prefer config.tracing.disable over env vars', () => { + it('should prefer env vars over config.tracing.disable', () => { process.env.INSTANA_TRACING_DISABLE_INSTRUMENTATIONS = 'grpc,kafkajs'; initAndActivate({ tracing: { disable: { instrumentations: ['aws-sdk/v2'] } } }); - expect(initAwsSdkv2).not.to.have.been.called; - expect(activateAwsSdkv2).not.to.have.been.called; + expect(initAwsSdkv2).to.have.been.called; + expect(activateAwsSdkv2).to.have.been.called; expect(initStubGrpcJs).to.have.been.called; expect(activateStubGrpcJs).to.have.been.called; - expect(initStubKafkaJs).to.have.been.called; - expect(activateStubKafkaJs).to.have.been.called; + expect(initStubKafkaJs).not.to.have.been.called; + expect(activateStubKafkaJs).not.to.have.been.called; }); it('should disable all instrumentations in specified groups', () => { @@ -273,10 +273,12 @@ mochaSuiteFn('[UNIT] tracing/index', function () { function initAndActivate(initConfig, extraConfigForActivate) { const logger = testUtils.createFakeLogger(); coreConfig.init(logger); - const config = coreConfig.normalize(initConfig); + let config = coreConfig.normalize({ userConfig: initConfig }); util.init(config); tracing.init(config); - tracing.activate(extraConfigForActivate); + + config = coreConfig.update(extraConfigForActivate, 3); + tracing.activate(config); } }); }); diff --git a/packages/core/test/tracing/spanBuffer_test.js b/packages/core/test/tracing/spanBuffer_test.js index aa629b8e30..695cb77424 100644 --- a/packages/core/test/tracing/spanBuffer_test.js +++ b/packages/core/test/tracing/spanBuffer_test.js @@ -46,7 +46,13 @@ describe('tracing/spanBuffer', () => { }); beforeEach(() => { - spanBuffer.activate(); + downstreamConnectionStub.sendSpans.resetHistory(); + spanBuffer.setTransmitImmediate(false); + spanBuffer.activate({ + tracing: { + spanBatchingEnabled: false + } + }); expect(global.setTimeout.called).to.be.false; global.setTimeout.resetHistory(); @@ -124,7 +130,13 @@ describe('tracing/spanBuffer', () => { }); beforeEach(() => { - spanBuffer.activate(); + downstreamConnectionStub.sendSpans.resetHistory(); + spanBuffer.setTransmitImmediate(false); + spanBuffer.activate({ + tracing: { + spanBatchingEnabled: false + } + }); expect(global.setTimeout.called).to.be.true; global.setTimeout.resetHistory(); }); @@ -217,7 +229,14 @@ describe('tracing/spanBuffer', () => { spanBuffer.addBatchableSpanName('batchable'); }); - beforeEach(() => spanBuffer.activate()); + beforeEach(() => { + spanBuffer.setTransmitImmediate(false); + spanBuffer.activate({ + tracing: { + spanBatchingEnabled: true + } + }); + }); afterEach(() => spanBuffer.deactivate()); @@ -563,7 +582,14 @@ describe('tracing/spanBuffer', () => { spanBuffer.addBatchableSpanName('batchable'); }); - beforeEach(() => spanBuffer.activate()); + beforeEach(() => { + spanBuffer.setTransmitImmediate(false); + spanBuffer.activate({ + tracing: { + spanBatchingEnabled: false + } + }); + }); afterEach(() => spanBuffer.deactivate()); @@ -574,7 +600,14 @@ describe('tracing/spanBuffer', () => { }); describe('when applying span transformations', () => { - beforeEach(() => spanBuffer.activate()); + beforeEach(() => { + spanBuffer.setTransmitImmediate(false); + spanBuffer.activate({ + tracing: { + spanBatchingEnabled: false + } + }); + }); afterEach(() => spanBuffer.deactivate()); const span = { diff --git a/packages/core/test/util/disableInstrumentation_test.js b/packages/core/test/util/disableInstrumentation_test.js index 32bb15b09d..4204fd70ca 100644 --- a/packages/core/test/util/disableInstrumentation_test.js +++ b/packages/core/test/util/disableInstrumentation_test.js @@ -387,31 +387,6 @@ describe('util.disableInstrumentation', () => { expect(consoleResult).to.be.true; }); - it('should accept service configuration and agent configuration when both are present', () => { - disableInstrumentation.init({ - tracing: { - disable: { instrumentations: ['console'] } - } - }); - disableInstrumentation.activate({ - tracing: { - disable: { instrumentations: ['bunyan'] } - } - }); - - const consoleResult = disableInstrumentation.isInstrumentationDisabled({ - instrumentationKey: './instrumentation/logging/console', - instrumentationModules: testInstrumentationModules - }); - const bunyanResult = disableInstrumentation.isInstrumentationDisabled({ - instrumentationKey: './instrumentation/logging/bunyan', - instrumentationModules: testInstrumentationModules - }); - - expect(consoleResult).to.be.true; - expect(bunyanResult).to.be.true; - }); - it('should use agent configuration when service configuration is empty', () => { disableInstrumentation.init({}); disableInstrumentation.activate({