diff --git a/package.json b/package.json index ca48492fe66..635be483c81 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "node": ">=18" }, "dependencies": { - "@datadog/libdatadog": "^0.4.0", + "@datadog/libdatadog": "^0.5.0", "@datadog/native-appsec": "8.4.0", "@datadog/native-iast-rewriter": "2.8.0", "@datadog/native-iast-taint-tracking": "3.2.0", diff --git a/packages/dd-trace/src/config.js b/packages/dd-trace/src/config.js index 7e4299a0d74..0cb0d3ff3b5 100644 --- a/packages/dd-trace/src/config.js +++ b/packages/dd-trace/src/config.js @@ -19,6 +19,7 @@ const telemetryMetrics = require('./telemetry/metrics') const { getIsGCPFunction, getIsAzureFunction } = require('./serverless') const { ORIGIN_KEY, GRPC_CLIENT_ERROR_STATUSES, GRPC_SERVER_ERROR_STATUSES } = require('./constants') const { appendRules } = require('./payload-tagging/config') +const libdatadog = require('@datadog/libdatadog') const tracerMetrics = telemetryMetrics.manager.namespace('tracers') @@ -236,6 +237,8 @@ function reformatSpanSamplingRules (rules) { class Config { constructor (options = {}) { + const { localFileConfigEntries, fleetFileConfigEntries } = this.getStableConfig() + options = { ...options, appsec: options.appsec != null ? options.appsec : options.experimental?.appsec, @@ -244,9 +247,22 @@ class Config { // Configure the logger first so it can be used to warn about other configs const logConfig = log.getConfig() - this.debug = logConfig.enabled + this.debug = isTrue(coalesce( + fleetFileConfigEntries.DD_TRACE_DEBUG, + process.env.DD_TRACE_DEBUG, + process.env.OTEL_LOG_LEVEL === 'debug' || undefined, + localFileConfigEntries.DD_TRACE_DEBUG, + logConfig.enabled + )) this.logger = coalesce(options.logger, logConfig.logger) - this.logLevel = coalesce(options.logLevel, logConfig.logLevel) + this.logLevel = coalesce( + options.logLevel, + fleetFileConfigEntries.DD_TRACE_LOG_LEVEL, + process.env.DD_TRACE_LOG_LEVEL, + process.env.OTEL_LOG_LEVEL, + localFileConfigEntries.DD_TRACE_LOG_LEVEL, + logConfig.logLevel + ) log.use(this.logger) log.toggle(this.debug, this.logLevel) @@ -337,7 +353,9 @@ class Config { } this._applyDefaults() + this._applyLocalStableConfig(localFileConfigEntries) this._applyEnvironment() + this._applyFleetStableConfig(fleetFileConfigEntries) this._applyOptions(options) this._applyCalculated() this._applyRemote({}) @@ -390,6 +408,44 @@ class Config { } } + getStableConfig () { + // Note: we use maybeLoad because there may be cases where the library is not available and we + // want to avoid breaking the application. In those cases, we will not have the file-based configuration. + const libconfig = libdatadog.maybeLoad('library_config') + const localFileConfigEntries = {} + const fleetFileConfigEntries = {} + if (libconfig != null) { + const configurator = new libconfig.JsConfigurator() + + const localConfigPath = process.env.DD_TEST_LOCAL_CONFIG_PATH ?? + configurator.get_config_local_path(process.platform) + const fleetConfigPath = process.env.DD_TEST_FLEET_CONFIG_PATH ?? + configurator.get_config_managed_path(process.platform) + + let localConfig = '' + try { + localConfig = fs.readFileSync(localConfigPath, 'utf8') + } catch (err) {} + let fleetConfig = '' + try { + fleetConfig = fs.readFileSync(fleetConfigPath, 'utf8') + } catch (err) {} + + if (localConfig || fleetConfig) { + configurator.set_envp(Object.entries(process.env).map(([key, value]) => `${key}=${value}`)) + configurator.set_args(process.argv) + configurator.get_configuration(localConfig.toString(), fleetConfig.toString()).forEach((entry) => { + if (entry.source === 'local_stable_config') { + localFileConfigEntries[entry.name] = entry.value + } else if (entry.source === 'fleet_stable_config') { + fleetFileConfigEntries[entry.name] = entry.value + } + }) + } + } + return { localFileConfigEntries, fleetFileConfigEntries } + } + // Supports only a subset of options for now. configure (options, remote) { if (remote) { @@ -575,6 +631,50 @@ class Config { this._setValue(defaults, 'aws.dynamoDb.tablePrimaryKeys', undefined) } + _applyLocalStableConfig (localFileConfigEntries) { + const obj = setHiddenProperty(this, '_localStableConfig', {}) + this._applyStableConfig(localFileConfigEntries, obj) + } + + _applyFleetStableConfig (fleetFileConfigEntries) { + const obj = setHiddenProperty(this, '_fleetStableConfig', {}) + this._applyStableConfig(fleetFileConfigEntries, obj) + } + + _applyStableConfig (config, obj) { + const { + DD_APPSEC_ENABLED, + DD_APPSEC_SCA_ENABLED, + DD_DATA_STREAMS_ENABLED, + DD_DYNAMIC_INSTRUMENTATION_ENABLED, + DD_ENV, + DD_IAST_ENABLED, + DD_LOGS_INJECTION, + DD_PROFILING_ENABLED, + DD_RUNTIME_METRICS_ENABLED, + DD_SERVICE, + DD_VERSION + } = config + + this._setBoolean(obj, 'appsec.enabled', DD_APPSEC_ENABLED) + this._setBoolean(obj, 'appsec.sca.enabled', DD_APPSEC_SCA_ENABLED) + this._setBoolean(obj, 'dsmEnabled', DD_DATA_STREAMS_ENABLED) + this._setBoolean(obj, 'dynamicInstrumentation.enabled', DD_DYNAMIC_INSTRUMENTATION_ENABLED) + this._setString(obj, 'env', DD_ENV) + this._setBoolean(obj, 'iast.enabled', DD_IAST_ENABLED) + this._setBoolean(obj, 'logInjection', DD_LOGS_INJECTION) + const profilingEnabledEnv = DD_PROFILING_ENABLED + const profilingEnabled = isTrue(profilingEnabledEnv) + ? 'true' + : isFalse(profilingEnabledEnv) + ? 'false' + : profilingEnabledEnv === 'auto' ? 'auto' : undefined + this._setString(obj, 'profiling.enabled', profilingEnabled) + this._setBoolean(obj, 'runtimeMetrics', DD_RUNTIME_METRICS_ENABLED) + this._setString(obj, 'service', DD_SERVICE) + this._setString(obj, 'version', DD_VERSION) + } + _applyEnvironment () { const { AWS_LAMBDA_FUNCTION_NAME, @@ -1317,9 +1417,33 @@ class Config { // eslint-disable-next-line @stylistic/js/max-len // https://github.com/DataDog/dd-go/blob/prod/trace/apps/tracer-telemetry-intake/telemetry-payload/static/config_norm_rules.json _merge () { - const containers = [this._remote, this._options, this._env, this._calculated, this._defaults] - const origins = ['remote_config', 'code', 'env_var', 'calculated', 'default'] - const unprocessedValues = [this._remoteUnprocessed, this._optsUnprocessed, this._envUnprocessed, {}, {}] + const containers = [ + this._remote, + this._options, + this._fleetStableConfig, + this._env, + this._localStableConfig, + this._calculated, + this._defaults + ] + const origins = [ + 'remote_config', + 'code', + 'fleet_stable_config', + 'env_var', + 'local_stable_config', + 'calculated', + 'default' + ] + const unprocessedValues = [ + this._remoteUnprocessed, + this._optsUnprocessed, + {}, + this._envUnprocessed, + {}, + {}, + {} + ] const changes = [] for (const name in this._defaults) { diff --git a/packages/dd-trace/test/config.spec.js b/packages/dd-trace/test/config.spec.js index 56cda03cf4e..7dc6b8b425e 100644 --- a/packages/dd-trace/test/config.spec.js +++ b/packages/dd-trace/test/config.spec.js @@ -6,6 +6,7 @@ const { expect } = require('chai') const { readFileSync } = require('fs') const sinon = require('sinon') const { GRPC_CLIENT_ERROR_STATUSES, GRPC_SERVER_ERROR_STATUSES } = require('../src/constants') +const path = require('path') describe('Config', () => { let Config @@ -2342,4 +2343,84 @@ describe('Config', () => { expect(taggingConfig).to.have.property('maxDepth', 7) }) }) + + context('library config', () => { + let env + let tempDir + beforeEach(() => { + env = process.env + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'config-test-')) + process.env.DD_TEST_LOCAL_CONFIG_PATH = path.join(tempDir, 'local.yaml') + process.env.DD_TEST_FLEET_CONFIG_PATH = path.join(tempDir, 'fleet.yaml') + }) + + afterEach(() => { + process.env = env + fs.rmdirSync(tempDir, { recursive: true }) + }) + + it('should apply host wide config', () => { + fs.writeFileSync( + process.env.DD_TEST_LOCAL_CONFIG_PATH, + ` +apm_configuration_default: + DD_RUNTIME_METRICS_ENABLED: true +`) + const config = new Config() + expect(config).to.have.property('runtimeMetrics', true) + }) + + it('should apply service specific config', () => { + fs.writeFileSync( + process.env.DD_TEST_LOCAL_CONFIG_PATH, + ` +rules: + - selectors: + - origin: language + matches: + - nodejs + operator: equals + configuration: + DD_SERVICE: my-service +`) + const config = new Config() + expect(config).to.have.property('service', 'my-service') + }) + + it('should respect the priority orders', () => { + fs.writeFileSync( + process.env.DD_TEST_LOCAL_CONFIG_PATH, + ` +rules: + - selectors: + - origin: language + matches: + - nodejs + operator: equals + configuration: + DD_SERVICE: a +`) + const configA = new Config() + expect(configA).to.have.property('service', 'a') + + process.env.DD_SERVICE = 'b' + const configB = new Config() + expect(configB).to.have.property('service', 'b', 'local stable config < env var') + + fs.writeFileSync( + process.env.DD_TEST_FLEET_CONFIG_PATH, + ` +rules: + - selectors: + - origin: language + matches: + - nodejs + operator: equals + configuration: + DD_SERVICE: c + `) + const configC = new Config() + expect(configC).to.have.property('service', 'c', 'local stable config < fleet config < env var') + }) + }) }) diff --git a/yarn.lock b/yarn.lock index 0511effddf6..412e025cc30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -401,10 +401,10 @@ resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" integrity "sha1-u1BFecHK6SPmV2pPXaQ9Jfl729k= sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==" -"@datadog/libdatadog@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.4.0.tgz#aeeea02973f663b555ad9ac30c4015a31d561598" - integrity sha512-kGZfFVmQInzt6J4FFGrqMbrDvOxqwk3WqhAreS6n9b/De+iMVy/NMu3V7uKsY5zAvz+uQw0liDJm3ZDVH/MVVw== +"@datadog/libdatadog@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.5.0.tgz#0ef2a2a76bb9505a0e7e5bc9be1415b467dbf368" + integrity sha512-YvLUVOhYVjJssm0f22/RnDQMc7ZZt/w1bA0nty1vvjyaDz5EWaHfWaaV4GYpCt5MRvnGjCBxIwwbRivmGseKeQ== "@datadog/native-appsec@8.4.0": version "8.4.0"