From 2c36e1a4b818e0126ce74948302ec2c1b0aa3dce Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 5 Jun 2026 17:22:12 -0400 Subject: [PATCH 1/6] Redact OTLP header and Datadog key configurations from configuration telemetry Add the OTLP exporter header configurations and the Datadog API key and application key configurations to the telemetry configuration filter list so their values are reported as "" in the configuration telemetry: - OTEL_EXPORTER_OTLP_HEADERS - OTEL_EXPORTER_OTLP_TRACES_HEADERS - OTEL_EXPORTER_OTLP_METRICS_HEADERS - OTEL_EXPORTER_OTLP_LOGS_HEADERS - DD_API_KEY - DD_APPLICATION_KEY (and its DD_APP_KEY alias) For each configuration, every form that can reach ConfigSetting is covered: the dotted configuration names (otlp.traces.headers, otlp.metrics.headers, otlp.logs.headers, application-key, app-key) and the environment-variable names. Mark these configurations, DD_API_KEY, and DD_APPLICATION_KEY with "sensitive: true" in metadata/supported-configurations.json. Migrate ConfigSettingTest to JUnit 5 and extend it to cover the OTLP header and application key configurations, including an assertion that the configured value is not present in the reported telemetry value. Update ConfigCollectorTest so the application key collected through the ConfigCollector pipeline is asserted to render as "". Co-Authored-By: Claude Opus 4.8 --- .../trace/api/ConfigCollectorTest.groovy | 42 ++++++++++-------- metadata/supported-configurations.json | 27 ++++++++---- .../java/datadog/trace/api/ConfigSetting.java | 21 ++++++++- .../datadog/trace/api/ConfigSettingTest.java | 43 ++++++++++++++++--- 4 files changed, 98 insertions(+), 35 deletions(-) diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy index 7ac922c028c..7e25eeb5985 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy @@ -27,43 +27,45 @@ class ConfigCollectorTest extends DDSpecification { expect: def envConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.ENV) def config = envConfigByKey.get(configKey) - config.stringValue() == configValue + config.stringValue() == expectedValue config.origin == ConfigOrigin.ENV where: - configKey | configValue + // expectedValue equals configValue for every setting except those redacted from configuration + // telemetry (e.g. the application key), where the collected value is rendered as "". + configKey | configValue | expectedValue // ConfigProvider.getEnum - IastConfig.IAST_TELEMETRY_VERBOSITY | Verbosity.DEBUG.toString() + IastConfig.IAST_TELEMETRY_VERBOSITY | Verbosity.DEBUG.toString() | configValue // ConfigProvider.getString - TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA | "v1" + TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA | "v1" | configValue // ConfigProvider.getStringNotEmpty - AppSecConfig.APPSEC_AUTOMATED_USER_EVENTS_TRACKING | UserEventTrackingMode.EXTENDED.toString() + AppSecConfig.APPSEC_AUTOMATED_USER_EVENTS_TRACKING | UserEventTrackingMode.EXTENDED.toString() | configValue // ConfigProvider.getStringExcludingSource - GeneralConfig.APPLICATION_KEY | "app-key" + GeneralConfig.APPLICATION_KEY | "app-key" | "" // ConfigProvider.getBoolean - TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES | "true" + TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES | "true" | configValue // ConfigProvider.getInteger - JmxFetchConfig.JMX_FETCH_CHECK_PERIOD | "60" + JmxFetchConfig.JMX_FETCH_CHECK_PERIOD | "60" | configValue // ConfigProvider.getLong - CiVisibilityConfig.CIVISIBILITY_GIT_COMMAND_TIMEOUT_MILLIS | "450273" + CiVisibilityConfig.CIVISIBILITY_GIT_COMMAND_TIMEOUT_MILLIS | "450273" | configValue // ConfigProvider.getFloat - GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL | "1.5" + GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL | "1.5" | configValue // ConfigProvider.getDouble - TracerConfig.TRACE_SAMPLE_RATE | "2.2" + TracerConfig.TRACE_SAMPLE_RATE | "2.2" | configValue // ConfigProvider.getList - TraceInstrumentationConfig.JMS_PROPAGATION_DISABLED_TOPICS | "someTopic,otherTopic" + TraceInstrumentationConfig.JMS_PROPAGATION_DISABLED_TOPICS | "someTopic,otherTopic" | configValue // ConfigProvider.getSet - IastConfig.IAST_WEAK_HASH_ALGORITHMS | "SHA1,SHA-1" + IastConfig.IAST_WEAK_HASH_ALGORITHMS | "SHA1,SHA-1" | configValue // ConfigProvider.getSpacedList - TracerConfig.PROXY_NO_PROXY | "a b c" + TracerConfig.PROXY_NO_PROXY | "a b c" | configValue // ConfigProvider.getMergedMap - TracerConfig.TRACE_PEER_SERVICE_MAPPING | "service1:best_service,userService:my_service" + TracerConfig.TRACE_PEER_SERVICE_MAPPING | "service1:best_service,userService:my_service" | configValue // ConfigProvider.getOrderedMap - TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING | "/asdf/*:/test" + TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING | "/asdf/*:/test" | configValue // ConfigProvider.getMergedMapWithOptionalMappings - TracerConfig.HEADER_TAGS | "e:five" + TracerConfig.HEADER_TAGS | "e:five" | configValue // ConfigProvider.getIntegerRange - TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES | "400-402" + TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES | "400-402" | configValue } def "should collect merged data from multiple sources"() { @@ -131,8 +133,10 @@ class ConfigCollectorTest extends DDSpecification { cs.origin == ConfigOrigin.DEFAULT where: + // GeneralConfig.APPLICATION_KEY is redacted from configuration telemetry, so its collected + // value is rendered as "" rather than null; that redaction is verified in the + // "non-default config settings get collected" feature above. configKey << [ - GeneralConfig.APPLICATION_KEY, TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES, JmxFetchConfig.JMX_FETCH_CHECK_PERIOD, CiVisibilityConfig.CIVISIBILITY_DEBUG_PORT, diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index a42ba33a31b..a8588354206 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -118,7 +118,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_API_KEY_FILE": [ @@ -198,7 +199,8 @@ "version": "A", "type": "string", "default": null, - "aliases": ["DD_APP_KEY"] + "aliases": ["DD_APP_KEY"], + "sensitive": true } ], "DD_APPLICATION_KEY_FILE": [ @@ -2318,7 +2320,8 @@ "version": "A", "type": "map", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_OTLP_LOGS_PROTOCOL": [ @@ -2462,7 +2465,8 @@ "version": "B", "type": "map", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_OTLP_METRICS_PROTOCOL": [ @@ -2510,7 +2514,8 @@ "version": "B", "type": "map", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_OTLP_TRACES_PROTOCOL": [ @@ -11654,7 +11659,8 @@ "version": "B", "type": "map", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "OTEL_EXPORTER_OTLP_PROTOCOL": [ @@ -11694,7 +11700,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": [ @@ -11734,7 +11741,8 @@ "version": "B", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": [ @@ -11790,7 +11798,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": [ diff --git a/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java b/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java index cf77e3bfb35..f6160db5804 100644 --- a/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java +++ b/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java @@ -23,9 +23,28 @@ public final class ConfigSetting { /** The config ID associated with this setting, or {@code null} if not applicable. */ public final String configId; + // Configuration keys whose values are excluded from configuration telemetry by replacing them + // with "". Keys are listed in every form that may reach this constructor: the dotted + // configuration name (used by ConfigProvider) and the environment-variable name. private static final Set CONFIG_FILTER_LIST = new HashSet<>( - Arrays.asList("DD_API_KEY", "dd.api-key", "dd.profiling.api-key", "dd.profiling.apikey")); + Arrays.asList( + "DD_API_KEY", + "dd.api-key", + "dd.profiling.api-key", + "dd.profiling.apikey", + "application-key", + "dd.application-key", + "DD_APPLICATION_KEY", + "app-key", + "dd.app-key", + "otlp.traces.headers", + "otlp.metrics.headers", + "otlp.logs.headers", + "OTEL_EXPORTER_OTLP_HEADERS", + "OTEL_EXPORTER_OTLP_TRACES_HEADERS", + "OTEL_EXPORTER_OTLP_METRICS_HEADERS", + "OTEL_EXPORTER_OTLP_LOGS_HEADERS")); public static ConfigSetting of(String key, Object value, ConfigOrigin origin) { return new ConfigSetting(key, value, origin, ABSENT_SEQ_ID, null); diff --git a/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java b/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java index ad086241a6d..3ed0c933f0c 100644 --- a/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java +++ b/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java @@ -1,6 +1,7 @@ package datadog.trace.api; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import datadog.trace.junit.utils.tabletest.BoxedValueConverter; @@ -42,17 +43,47 @@ void supportsEqualityCheck( } @TableTest({ - "scenario | key | value | filteredValue", - "DD_API_KEY | DD_API_KEY | somevalue | ", - "dd.api-key | dd.api-key | somevalue | ", - "dd.profiling.api-key | dd.profiling.api-key | somevalue | ", - "dd.profiling.apikey | dd.profiling.apikey | somevalue | ", - "some.other.key | some.other.key | somevalue | somevalue " + "scenario | key | value | filteredValue", + "dd api key env | DD_API_KEY | somevalue | ", + "dd api key prop | dd.api-key | somevalue | ", + "profiling api key | dd.profiling.api-key | somevalue | ", + "profiling apikey | dd.profiling.apikey | somevalue | ", + "application key name | application-key | somevalue | ", + "application key prop | dd.application-key | somevalue | ", + "application key env | DD_APPLICATION_KEY | somevalue | ", + "app key alias name | app-key | somevalue | ", + "app key alias prop | dd.app-key | somevalue | ", + "otlp traces headers | otlp.traces.headers | somevalue | ", + "otlp metrics headers | otlp.metrics.headers | somevalue | ", + "otlp logs headers | otlp.logs.headers | somevalue | ", + "otel otlp headers | OTEL_EXPORTER_OTLP_HEADERS | somevalue | ", + "otel traces headers | OTEL_EXPORTER_OTLP_TRACES_HEADERS | somevalue | ", + "otel metrics headers | OTEL_EXPORTER_OTLP_METRICS_HEADERS | somevalue | ", + "otel logs headers | OTEL_EXPORTER_OTLP_LOGS_HEADERS | somevalue | ", + "other key | some.other.key | somevalue | somevalue " }) void filtersKeyValues(String key, String value, String filteredValue) { assertEquals(filteredValue, ConfigSetting.of(key, value, ConfigOrigin.DEFAULT).stringValue()); } + @TableTest({ + "scenario | key | value ", + "otlp traces | otlp.traces.headers | dd-api-key=secret-traces ", + "otlp metrics | otlp.metrics.headers | dd-api-key=secret-metrics", + "otlp logs | otlp.logs.headers | dd-api-key=secret-logs ", + "otel base | OTEL_EXPORTER_OTLP_HEADERS | dd-api-key=secret-base ", + "otel traces | OTEL_EXPORTER_OTLP_TRACES_HEADERS | dd-api-key=secret-traces ", + "otel metrics | OTEL_EXPORTER_OTLP_METRICS_HEADERS | dd-api-key=secret-metrics", + "otel logs | OTEL_EXPORTER_OTLP_LOGS_HEADERS | dd-api-key=secret-logs ", + "dd api key | DD_API_KEY | secret-api-key " + }) + void doesNotExposeSensitiveValues(String key, String value) { + String rendered = ConfigSetting.of(key, value, ConfigOrigin.ENV).stringValue(); + assertEquals("", rendered); + assertFalse( + rendered.contains(value), "rendered telemetry value must not contain the configured value"); + } + @TableTest({ "scenario | value | rendered", "null | | ", From ede87b95fb13223f4c75ca8c51920f47a753ee51 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Wed, 10 Jun 2026 16:15:43 -0400 Subject: [PATCH 2/6] test(telemetry): guard that sensitive-marked configs are actually redacted Co-Authored-By: Claude Opus 4.8 (1M context) --- .../api/SensitiveConfigRedactionTest.java | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java diff --git a/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java b/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java new file mode 100644 index 00000000000..647f727e76b --- /dev/null +++ b/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java @@ -0,0 +1,178 @@ +package datadog.trace.api; + +import static datadog.trace.util.ConfigStrings.toEnvVar; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.snakeyaml.engine.v2.api.Load; +import org.snakeyaml.engine.v2.api.LoadSettings; + +/** + * Drift-guard test that keeps the {@code "sensitive": true} attribute in {@code + * metadata/supported-configurations.json} consistent with the redaction actually performed by + * {@link ConfigSetting}. + * + *

Telemetry redaction is driven by {@code ConfigSetting.CONFIG_FILTER_LIST}; the registry + * attribute is otherwise not read at runtime. Without this guard, marking a configuration {@code + * sensitive: true} in the registry without adding it to the filter list (or vice-versa) would + * silently leave that configuration unredacted in configuration telemetry. This test fails CI when + * the two drift apart. + */ +public class SensitiveConfigRedactionTest { + + private static final String REGISTRY_RELATIVE_PATH = "metadata/supported-configurations.json"; + + /** + * Normalizes any config name form -- env-var ({@code DD_API_KEY}), dotted system property ({@code + * dd.api-key}), or bare dotted name ({@code otlp.traces.headers}) -- to a single canonical token + * so the registry keys and the filter-list entries can be compared. + * + *

{@link datadog.trace.util.ConfigStrings#toEnvVar(String)} upper-cases and replaces {@code .} + * / {@code -} with {@code _}, but it does not unify the {@code DD_} prefix: a registry env name + * such as {@code DD_OTLP_TRACES_HEADERS} and the filter's dotted {@code otlp.traces.headers} + * (which {@code toEnvVar} turns into {@code OTLP_TRACES_HEADERS}) would otherwise not match. We + * strip a leading {@code DD_} after {@code toEnvVar} so both collapse onto {@code + * OTLP_TRACES_HEADERS}. {@code OTEL_*} names have no {@code DD_} prefix and are unaffected. + */ + private static String canonical(String name) { + String env = toEnvVar(name); + if (env.startsWith("DD_")) { + env = env.substring("DD_".length()); + } + return env; + } + + @Test + void everySensitiveConfigIsRedacted() { + Set sensitiveRegistryKeys = sensitiveRegistryKeys(); + assertTrue( + !sensitiveRegistryKeys.isEmpty(), + "expected at least one config marked \"sensitive\": true in " + REGISTRY_RELATIVE_PATH); + + Set filterCanonical = + configFilterList().stream() + .map(SensitiveConfigRedactionTest::canonical) + .collect(toTreeSet()); + + Set notRedacted = new TreeSet<>(); + for (String key : sensitiveRegistryKeys) { + if (!filterCanonical.contains(canonical(key))) { + notRedacted.add(key); + } + } + + if (!notRedacted.isEmpty()) { + fail( + "These configurations are marked \"sensitive\": true in " + + REGISTRY_RELATIVE_PATH + + " but are NOT redacted by ConfigSetting.CONFIG_FILTER_LIST. Add them (in env-var " + + "and/or dotted form) to CONFIG_FILTER_LIST in ConfigSetting.java, or drop the " + + "\"sensitive\": true marker:\n " + + String.join("\n ", notRedacted)); + } + } + + /** + * Advisory only: surfaces filter-list entries that have no {@code "sensitive": true} counterpart + * in the registry. This does not fail the build -- some entries (e.g. profiling api keys) are + * legitimately redacted without being registry-sensitive -- but it makes intentional asymmetry + * visible in the logs. + */ + @Test + void reportsFilterEntriesNotMarkedSensitive() { + Set sensitiveCanonical = + sensitiveRegistryKeys().stream() + .map(SensitiveConfigRedactionTest::canonical) + .collect(toTreeSet()); + + Set filterOnly = new TreeSet<>(); + for (String entry : configFilterList()) { + if (!sensitiveCanonical.contains(canonical(entry))) { + filterOnly.add(entry); + } + } + + if (!filterOnly.isEmpty()) { + System.out.println( + "[advisory] CONFIG_FILTER_LIST entries with no \"sensitive\": true marker in " + + REGISTRY_RELATIVE_PATH + + " (not a failure): " + + filterOnly); + } + } + + @SuppressWarnings("unchecked") + private static Set sensitiveRegistryKeys() { + Path registry = locateRegistry(); + String content; + try { + content = new String(Files.readAllBytes(registry), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read " + registry, e); + } + + Object parsed = new Load(LoadSettings.builder().build()).loadFromString(content); + Map root = (Map) parsed; + Map supported = (Map) root.get("supportedConfigurations"); + + Set sensitive = new TreeSet<>(); + for (Map.Entry entry : supported.entrySet()) { + // Each value is a list of versioned definitions; the config is sensitive if any marks it so. + for (Object def : (List) entry.getValue()) { + Object flag = ((Map) def).get("sensitive"); + if (Boolean.TRUE.equals(flag)) { + sensitive.add(entry.getKey()); + break; + } + } + } + return sensitive; + } + + /** Reads {@code CONFIG_FILTER_LIST} from {@link ConfigSetting} via reflection. */ + @SuppressWarnings("unchecked") + private static Set configFilterList() { + try { + Field field = ConfigSetting.class.getDeclaredField("CONFIG_FILTER_LIST"); + field.setAccessible(true); + return (Set) field.get(null); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new IllegalStateException("Could not read ConfigSetting.CONFIG_FILTER_LIST", e); + } + } + + /** Walks up from the working directory until {@code metadata/supported-configurations.json}. */ + private static Path locateRegistry() { + Path dir = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + for (Path current = dir; current != null; current = current.getParent()) { + Path candidate = current.resolve(REGISTRY_RELATIVE_PATH); + if (Files.isRegularFile(candidate)) { + return candidate; + } + } + throw new IllegalStateException( + "Could not locate " + + REGISTRY_RELATIVE_PATH + + " by walking up from " + + dir + + ". Adjust the resolution logic in " + + SensitiveConfigRedactionTest.class.getName()); + } + + private static java.util.stream.Collector> toTreeSet() { + return Collectors.toCollection(TreeSet::new); + } +} From bae31f84fa06a8c76e9a79422a669a0b5403a571 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 11 Jun 2026 17:22:03 -0400 Subject: [PATCH 3/6] Address review: alphabetize filter list, migrate ConfigCollectorTest to JUnit, redact api-key/profiling collect-path forms, tighten drift guard Co-Authored-By: Claude Opus 4.8 (1M context) --- .../trace/api/ConfigCollectorTest.groovy | 329 --------------- .../trace/api/ConfigCollectorTest.java | 374 ++++++++++++++++++ metadata/supported-configurations.json | 6 +- .../java/datadog/trace/api/ConfigSetting.java | 28 +- .../datadog/trace/api/ConfigSettingTest.java | 58 +-- .../api/SensitiveConfigRedactionTest.java | 119 ++---- 6 files changed, 447 insertions(+), 467 deletions(-) delete mode 100644 internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy create mode 100644 internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy deleted file mode 100644 index 7e25eeb5985..00000000000 --- a/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy +++ /dev/null @@ -1,329 +0,0 @@ -package datadog.trace.api - -import datadog.trace.api.config.AppSecConfig -import datadog.trace.api.config.CiVisibilityConfig -import datadog.trace.api.config.GeneralConfig -import datadog.trace.api.config.IastConfig -import datadog.trace.api.config.JmxFetchConfig -import datadog.trace.api.config.TraceInstrumentationConfig -import datadog.trace.api.config.TracerConfig -import datadog.trace.api.iast.telemetry.Verbosity -import datadog.trace.api.naming.SpanNaming -import datadog.trace.bootstrap.config.provider.ConfigProvider -import datadog.trace.config.inversion.ConfigHelper -import datadog.trace.test.util.DDSpecification -import datadog.trace.util.ConfigStrings - -import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_WEAK_HASH_ALGORITHMS -import static datadog.trace.api.ConfigDefaults.DEFAULT_TELEMETRY_HEARTBEAT_INTERVAL -import static datadog.trace.api.ConfigSetting.ABSENT_SEQ_ID - -class ConfigCollectorTest extends DDSpecification { - - def "non-default config settings get collected"() { - setup: - injectEnvConfig(ConfigStrings.toEnvVar(configKey), configValue) - - expect: - def envConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.ENV) - def config = envConfigByKey.get(configKey) - config.stringValue() == expectedValue - config.origin == ConfigOrigin.ENV - - where: - // expectedValue equals configValue for every setting except those redacted from configuration - // telemetry (e.g. the application key), where the collected value is rendered as "". - configKey | configValue | expectedValue - // ConfigProvider.getEnum - IastConfig.IAST_TELEMETRY_VERBOSITY | Verbosity.DEBUG.toString() | configValue - // ConfigProvider.getString - TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA | "v1" | configValue - // ConfigProvider.getStringNotEmpty - AppSecConfig.APPSEC_AUTOMATED_USER_EVENTS_TRACKING | UserEventTrackingMode.EXTENDED.toString() | configValue - // ConfigProvider.getStringExcludingSource - GeneralConfig.APPLICATION_KEY | "app-key" | "" - // ConfigProvider.getBoolean - TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES | "true" | configValue - // ConfigProvider.getInteger - JmxFetchConfig.JMX_FETCH_CHECK_PERIOD | "60" | configValue - // ConfigProvider.getLong - CiVisibilityConfig.CIVISIBILITY_GIT_COMMAND_TIMEOUT_MILLIS | "450273" | configValue - // ConfigProvider.getFloat - GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL | "1.5" | configValue - // ConfigProvider.getDouble - TracerConfig.TRACE_SAMPLE_RATE | "2.2" | configValue - // ConfigProvider.getList - TraceInstrumentationConfig.JMS_PROPAGATION_DISABLED_TOPICS | "someTopic,otherTopic" | configValue - // ConfigProvider.getSet - IastConfig.IAST_WEAK_HASH_ALGORITHMS | "SHA1,SHA-1" | configValue - // ConfigProvider.getSpacedList - TracerConfig.PROXY_NO_PROXY | "a b c" | configValue - // ConfigProvider.getMergedMap - TracerConfig.TRACE_PEER_SERVICE_MAPPING | "service1:best_service,userService:my_service" | configValue - // ConfigProvider.getOrderedMap - TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING | "/asdf/*:/test" | configValue - // ConfigProvider.getMergedMapWithOptionalMappings - TracerConfig.HEADER_TAGS | "e:five" | configValue - // ConfigProvider.getIntegerRange - TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES | "400-402" | configValue - } - - def "should collect merged data from multiple sources"() { - setup: - injectEnvConfig(ConfigStrings.toEnvVar(configKey), envConfigValue) - if (jvmConfigValue != null) { - injectSysConfig(configKey, jvmConfigValue) - } - - when: - def collected = ConfigCollector.get().collect() - - then: - def envSetting = collected.get(ConfigOrigin.ENV) - def envConfig = envSetting.get(configKey) - envConfig.stringValue() == envConfigValue - envConfig.origin == ConfigOrigin.ENV - if (jvmConfigValue != null ) { - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP) - def jvmConfig = jvmSetting.get(configKey) - jvmConfig.stringValue().split(',') as Set == jvmConfigValue.split(',') as Set - jvmConfig.origin == ConfigOrigin.JVM_PROP - } - - - // TODO: Add a check for which setting the collector recognizes as highest precedence - - where: - configKey | envConfigValue | jvmConfigValue | expectedValue | expectedOrigin - // ConfigProvider.getMergedMap - TracerConfig.TRACE_PEER_SERVICE_MAPPING | "service1:best_service,userService:my_service" | "service2:backup_service" | "service2:backup_service,service1:best_service,userService:my_service" | ConfigOrigin.CALCULATED - // ConfigProvider.getOrderedMap - TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING | "/asdf/*:/test,/b:some" | "/a:prop" | "/asdf/*:/test,/b:some,/a:prop" | ConfigOrigin.CALCULATED - // ConfigProvider.getMergedMapWithOptionalMappings - TracerConfig.HEADER_TAGS | "j:ten" | "e:five,b:six" | "e:five,j:ten,b:six" | ConfigOrigin.CALCULATED - // ConfigProvider.getMergedMap, but only one source - TracerConfig.TRACE_PEER_SERVICE_MAPPING | "service1:best_service,userService:my_service" | null | "service1:best_service,userService:my_service" | ConfigOrigin.ENV - } - - def "default not-null config settings are collected"() { - expect: - def defaultConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT) - def setting = defaultConfigByKey.get(configKey) - setting.origin == ConfigOrigin.DEFAULT - setting.stringValue() == defaultValue - - where: - configKey | defaultValue - IastConfig.IAST_TELEMETRY_VERBOSITY | Verbosity.INFORMATION.toString() - TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA | "v" + SpanNaming.SCHEMA_MIN_VERSION - GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL | new Float(DEFAULT_TELEMETRY_HEARTBEAT_INTERVAL).toString() - CiVisibilityConfig.CIVISIBILITY_GRADLE_SOURCE_SETS | "main,test" - IastConfig.IAST_WEAK_HASH_ALGORITHMS | DEFAULT_IAST_WEAK_HASH_ALGORITHMS.join(",") - TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES | "400-500" - } - - def "default null config settings are also collected"() { - when: - def defaultConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT) - ConfigSetting cs = defaultConfigByKey.get(configKey) - - then: - cs.key == configKey - cs.stringValue() == null - cs.origin == ConfigOrigin.DEFAULT - - where: - // GeneralConfig.APPLICATION_KEY is redacted from configuration telemetry, so its collected - // value is rendered as "" rather than null; that redaction is verified in the - // "non-default config settings get collected" feature above. - configKey << [ - TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES, - JmxFetchConfig.JMX_FETCH_CHECK_PERIOD, - CiVisibilityConfig.CIVISIBILITY_DEBUG_PORT, - TracerConfig.TRACE_SAMPLE_RATE, - TraceInstrumentationConfig.JMS_PROPAGATION_DISABLED_TOPICS, - TracerConfig.PROXY_NO_PROXY, - ] - } - - def "default empty maps and list config settings are collected as empty strings"() { - when: - def defaultConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT) - ConfigSetting cs = defaultConfigByKey.get(configKey) - - then: - cs.key == configKey - cs.stringValue() == "" - cs.origin == ConfigOrigin.DEFAULT - - where: - configKey << [ - TracerConfig.TRACE_PEER_SERVICE_MAPPING, - TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING, - TracerConfig.HEADER_TAGS, - ] - } - - def "put-get configurations"() { - setup: - ConfigCollector.get().collect() - - when: - ConfigCollector.get().put('key1', 'value1', ConfigOrigin.DEFAULT, ABSENT_SEQ_ID) - ConfigCollector.get().put('key2', 'value2', ConfigOrigin.ENV, ABSENT_SEQ_ID) - ConfigCollector.get().put('key1', 'value4', ConfigOrigin.REMOTE, ABSENT_SEQ_ID) - ConfigCollector.get().put('key3', 'value3', ConfigOrigin.JVM_PROP, ABSENT_SEQ_ID) - - then: - def collected = ConfigCollector.get().collect() - collected.get(ConfigOrigin.REMOTE).get('key1') == ConfigSetting.of('key1', 'value4', ConfigOrigin.REMOTE) - collected.get(ConfigOrigin.ENV).get('key2') == ConfigSetting.of('key2', 'value2', ConfigOrigin.ENV) - collected.get(ConfigOrigin.JVM_PROP).get('key3') == ConfigSetting.of('key3', 'value3', ConfigOrigin.JVM_PROP) - collected.get(ConfigOrigin.DEFAULT).get('key1') == ConfigSetting.of('key1', 'value1', ConfigOrigin.DEFAULT) - } - - - def "hide pii configuration data"() { - setup: - ConfigCollector.get().collect() - - when: - ConfigCollector.get().put('DD_API_KEY', 'sensitive data', ConfigOrigin.ENV, ABSENT_SEQ_ID) - - then: - def collected = ConfigCollector.get().collect() - collected.get(ConfigOrigin.ENV).get('DD_API_KEY').stringValue() == '' - } - - def "collects common setting default values"() { - when: - def defaultConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT) - - then: - def setting = defaultConfigByKey.get(key) - - setting.key == key - setting.stringValue() == value - setting.origin == ConfigOrigin.DEFAULT - - where: - key | value - "trace.enabled" | "true" - "profiling.enabled" | "false" - "appsec.enabled" | "inactive" - "data.streams.enabled" | "false" - "trace.tags" | "" - "trace.header.tags" | "" - "logs.injection.enabled" | "true" - // defaults to null meaning sample everything but not exactly the same as when explicitly set to 1.0 - "trace.sample.rate" | null - } - - def "collects common setting overridden values"() { - setup: - injectEnvConfig("DD_TRACE_ENABLED", "false") - injectEnvConfig("DD_PROFILING_ENABLED", "true") - injectEnvConfig("DD_APPSEC_ENABLED", "false") - injectEnvConfig("DD_DATA_STREAMS_ENABLED", "true") - injectEnvConfig("DD_TAGS", "team:apm,component:web") - injectEnvConfig("DD_TRACE_HEADER_TAGS", "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2") - injectEnvConfig("DD_LOGS_INJECTION", "false") - injectEnvConfig("DD_TRACE_SAMPLE_RATE", "0.3") - - when: - def envConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.ENV) - - then: - def setting = envConfigByKey.get(key) - - setting.key == key - setting.stringValue() == value - setting.origin == ConfigOrigin.ENV - - where: - key | value - "trace.enabled" | "false" - "profiling.enabled" | "true" - "appsec.enabled" | "false" - "data.streams.enabled" | "true" - // doesn't preserve ordering for some maps - "trace.tags" | "component:web,team:apm" - // lowercase keys for some maps merged from different sources - "trace.header.tags" | "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2".toLowerCase() - "logs.injection.enabled" | "false" - "trace.sample.rate" | "0.3" - } - - def "config collector creates ConfigSettings with correct seqId"() { - setup: - ConfigCollector.get().collect() // clear previous state - - when: - // Simulate sources with increasing precedence and a default - ConfigCollector.get().put("test.key", "default", ConfigOrigin.DEFAULT, ConfigSetting.DEFAULT_SEQ_ID) - ConfigCollector.get().put("test.key", "env", ConfigOrigin.ENV, 2) - ConfigCollector.get().put("test.key", "jvm", ConfigOrigin.JVM_PROP, 3) - ConfigCollector.get().put("test.key", "remote", ConfigOrigin.REMOTE, 4) - - then: - def collected = ConfigCollector.get().collect() - def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.key") - def envSetting = collected.get(ConfigOrigin.ENV).get("test.key") - def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.key") - def remoteSetting = collected.get(ConfigOrigin.REMOTE).get("test.key") - - defaultSetting.seqId == ConfigSetting.DEFAULT_SEQ_ID - // Higher precedence = higher seqId - defaultSetting.seqId < envSetting.seqId - envSetting.seqId < jvmSetting.seqId - jvmSetting.seqId < remoteSetting.seqId - } - - def "config id is null for non-StableConfigSource"() { - setup: - def strictness = ConfigHelper.get().configInversionStrictFlag() - ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.TEST) - - def key = "test.key" - def value = "test-value" - injectSysConfig(key, value) - - when: - // Trigger config collection by getting a value - ConfigProvider.getInstance().getString(key) - def settings = ConfigCollector.get().collect() - - then: - // Verify the config was collected but without a config ID - def setting = settings.get(ConfigOrigin.JVM_PROP).get(key) - setting != null - setting.configId == null - setting.value == value - setting.origin == ConfigOrigin.JVM_PROP - - cleanup: - ConfigHelper.get().setConfigInversionStrict(strictness) - } - - def "default sources cannot be overridden"() { - setup: - def key = "test.key" - def value = "test-value" - def overrideVal = "override-value" - def defaultConfigByKey - ConfigSetting cs - - when: - // Need to make 2 calls in a row because collect() will empty the map - ConfigCollector.get().putDefault(key, value) - ConfigCollector.get().putDefault(key, overrideVal) - defaultConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT) - cs = defaultConfigByKey.get(key) - - then: - cs.key == key - cs.stringValue() == value - cs.origin == ConfigOrigin.DEFAULT - cs.seqId == ConfigSetting.DEFAULT_SEQ_ID - } -} diff --git a/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java b/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java new file mode 100644 index 00000000000..c9ce9f0fa85 --- /dev/null +++ b/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java @@ -0,0 +1,374 @@ +package datadog.trace.api; + +import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_WEAK_HASH_ALGORITHMS; +import static datadog.trace.api.ConfigDefaults.DEFAULT_TELEMETRY_HEARTBEAT_INTERVAL; +import static datadog.trace.api.ConfigSetting.ABSENT_SEQ_ID; +import static datadog.trace.junit.utils.config.WithConfigExtension.injectEnvConfig; +import static datadog.trace.junit.utils.config.WithConfigExtension.injectSysConfig; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.api.config.AppSecConfig; +import datadog.trace.api.config.CiVisibilityConfig; +import datadog.trace.api.config.GeneralConfig; +import datadog.trace.api.config.IastConfig; +import datadog.trace.api.config.JmxFetchConfig; +import datadog.trace.api.config.TraceInstrumentationConfig; +import datadog.trace.api.config.TracerConfig; +import datadog.trace.api.iast.telemetry.Verbosity; +import datadog.trace.api.naming.SpanNaming; +import datadog.trace.bootstrap.config.provider.ConfigProvider; +import datadog.trace.config.inversion.ConfigHelper; +import datadog.trace.test.util.DDJavaSpecification; +import datadog.trace.util.ConfigStrings; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; + +public class ConfigCollectorTest extends DDJavaSpecification { + + static Stream nonDefaultConfigSettingsGetCollectedArguments() { + // expectedValue equals configValue for every setting except those redacted from configuration + // telemetry (e.g. the application key), where the collected value is rendered as "". + return Stream.of( + // ConfigProvider.getEnum + arguments(IastConfig.IAST_TELEMETRY_VERBOSITY, Verbosity.DEBUG.toString(), null), + // ConfigProvider.getString + arguments(TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA, "v1", null), + // ConfigProvider.getStringNotEmpty + arguments( + AppSecConfig.APPSEC_AUTOMATED_USER_EVENTS_TRACKING, + UserEventTrackingMode.EXTENDED.toString(), + null), + // ConfigProvider.getStringExcludingSource + arguments(DDTags.SERVICE, "my-service", null), + // ConfigProvider.getStringExcludingSource, redacted from configuration telemetry + arguments(GeneralConfig.APPLICATION_KEY, "app-key", ""), + arguments(GeneralConfig.API_KEY, "some-api-key", ""), + // ConfigProvider.getBoolean + arguments(TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES, "true", null), + // ConfigProvider.getInteger + arguments(JmxFetchConfig.JMX_FETCH_CHECK_PERIOD, "60", null), + // ConfigProvider.getLong + arguments(CiVisibilityConfig.CIVISIBILITY_GIT_COMMAND_TIMEOUT_MILLIS, "450273", null), + // ConfigProvider.getFloat + arguments(GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL, "1.5", null), + // ConfigProvider.getDouble + arguments(TracerConfig.TRACE_SAMPLE_RATE, "2.2", null), + // ConfigProvider.getList + arguments( + TraceInstrumentationConfig.JMS_PROPAGATION_DISABLED_TOPICS, + "someTopic,otherTopic", + null), + // ConfigProvider.getSet + arguments(IastConfig.IAST_WEAK_HASH_ALGORITHMS, "SHA1,SHA-1", null), + // ConfigProvider.getSpacedList + arguments(TracerConfig.PROXY_NO_PROXY, "a b c", null), + // ConfigProvider.getMergedMap + arguments( + TracerConfig.TRACE_PEER_SERVICE_MAPPING, + "service1:best_service,userService:my_service", + null), + // ConfigProvider.getOrderedMap + arguments(TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING, "/asdf/*:/test", null), + // ConfigProvider.getMergedMapWithOptionalMappings + arguments(TracerConfig.HEADER_TAGS, "e:five", null), + // ConfigProvider.getIntegerRange + arguments(TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES, "400-402", null)); + } + + @ParameterizedTest + @MethodSource("nonDefaultConfigSettingsGetCollectedArguments") + void nonDefaultConfigSettingsGetCollected( + String configKey, String configValue, String expectedOverride) { + // expectedValue equals configValue unless an explicit override is provided (used for redacted + // settings rendered as ""). + String expectedValue = expectedOverride != null ? expectedOverride : configValue; + injectEnvConfig(ConfigStrings.toEnvVar(configKey), configValue); + + Map envConfigByKey = + ConfigCollector.get().collect().get(ConfigOrigin.ENV); + ConfigSetting config = envConfigByKey.get(configKey); + assertEquals(expectedValue, config.stringValue()); + assertEquals(ConfigOrigin.ENV, config.origin); + } + + static Stream shouldCollectMergedDataFromMultipleSourcesArguments() { + return Stream.of( + // ConfigProvider.getMergedMap + arguments( + TracerConfig.TRACE_PEER_SERVICE_MAPPING, + "service1:best_service,userService:my_service", + "service2:backup_service"), + // ConfigProvider.getOrderedMap + arguments( + TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING, + "/asdf/*:/test,/b:some", + "/a:prop"), + // ConfigProvider.getMergedMapWithOptionalMappings + arguments(TracerConfig.HEADER_TAGS, "j:ten", "e:five,b:six"), + // ConfigProvider.getMergedMap, but only one source + arguments( + TracerConfig.TRACE_PEER_SERVICE_MAPPING, + "service1:best_service,userService:my_service", + null)); + } + + @ParameterizedTest + @MethodSource("shouldCollectMergedDataFromMultipleSourcesArguments") + void shouldCollectMergedDataFromMultipleSources( + String configKey, String envConfigValue, String jvmConfigValue) { + injectEnvConfig(ConfigStrings.toEnvVar(configKey), envConfigValue); + if (jvmConfigValue != null) { + injectSysConfig(configKey, jvmConfigValue); + } + + Map> collected = ConfigCollector.get().collect(); + + Map envSetting = collected.get(ConfigOrigin.ENV); + ConfigSetting envConfig = envSetting.get(configKey); + assertEquals(envConfigValue, envConfig.stringValue()); + assertEquals(ConfigOrigin.ENV, envConfig.origin); + if (jvmConfigValue != null) { + Map jvmSetting = collected.get(ConfigOrigin.JVM_PROP); + ConfigSetting jvmConfig = jvmSetting.get(configKey); + assertEquals( + new HashSet<>(Arrays.asList(jvmConfigValue.split(","))), + new HashSet<>(Arrays.asList(jvmConfig.stringValue().split(",")))); + assertEquals(ConfigOrigin.JVM_PROP, jvmConfig.origin); + } + + // TODO: Add a check for which setting the collector recognizes as highest precedence + } + + static Stream defaultNotNullConfigSettingsAreCollectedArguments() { + return Stream.of( + arguments(IastConfig.IAST_TELEMETRY_VERBOSITY, Verbosity.INFORMATION.toString()), + arguments(TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA, "v" + SpanNaming.SCHEMA_MIN_VERSION), + arguments( + GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL, + Float.toString(DEFAULT_TELEMETRY_HEARTBEAT_INTERVAL)), + arguments(CiVisibilityConfig.CIVISIBILITY_GRADLE_SOURCE_SETS, "main,test"), + arguments( + IastConfig.IAST_WEAK_HASH_ALGORITHMS, + String.join(",", DEFAULT_IAST_WEAK_HASH_ALGORITHMS)), + arguments(TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES, "400-500")); + } + + @ParameterizedTest + @MethodSource("defaultNotNullConfigSettingsAreCollectedArguments") + void defaultNotNullConfigSettingsAreCollected(String configKey, String defaultValue) { + Map defaultConfigByKey = + ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT); + ConfigSetting setting = defaultConfigByKey.get(configKey); + assertEquals(ConfigOrigin.DEFAULT, setting.origin); + assertEquals(defaultValue, setting.stringValue()); + } + + static Stream defaultNullConfigSettingsAreAlsoCollectedArguments() { + // GeneralConfig.APPLICATION_KEY is redacted from configuration telemetry, so its collected + // value is rendered as "" rather than null; that redaction is verified in the + // nonDefaultConfigSettingsGetCollected test above. + return Stream.of( + arguments(TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES), + arguments(JmxFetchConfig.JMX_FETCH_CHECK_PERIOD), + arguments(CiVisibilityConfig.CIVISIBILITY_DEBUG_PORT), + arguments(TracerConfig.TRACE_SAMPLE_RATE), + arguments(TraceInstrumentationConfig.JMS_PROPAGATION_DISABLED_TOPICS), + arguments(TracerConfig.PROXY_NO_PROXY)); + } + + @ParameterizedTest + @MethodSource("defaultNullConfigSettingsAreAlsoCollectedArguments") + void defaultNullConfigSettingsAreAlsoCollected(String configKey) { + Map defaultConfigByKey = + ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT); + ConfigSetting cs = defaultConfigByKey.get(configKey); + + assertEquals(configKey, cs.key); + assertNull(cs.stringValue()); + assertEquals(ConfigOrigin.DEFAULT, cs.origin); + } + + static Stream defaultEmptyMapsAndListConfigSettingsArguments() { + return Stream.of( + arguments(TracerConfig.TRACE_PEER_SERVICE_MAPPING), + arguments(TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING), + arguments(TracerConfig.HEADER_TAGS)); + } + + @ParameterizedTest + @MethodSource("defaultEmptyMapsAndListConfigSettingsArguments") + void defaultEmptyMapsAndListConfigSettingsAreCollectedAsEmptyStrings(String configKey) { + Map defaultConfigByKey = + ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT); + ConfigSetting cs = defaultConfigByKey.get(configKey); + + assertEquals(configKey, cs.key); + assertEquals("", cs.stringValue()); + assertEquals(ConfigOrigin.DEFAULT, cs.origin); + } + + @Test + void putGetConfigurations() { + ConfigCollector.get().collect(); + + ConfigCollector.get().put("key1", "value1", ConfigOrigin.DEFAULT, ABSENT_SEQ_ID); + ConfigCollector.get().put("key2", "value2", ConfigOrigin.ENV, ABSENT_SEQ_ID); + ConfigCollector.get().put("key1", "value4", ConfigOrigin.REMOTE, ABSENT_SEQ_ID); + ConfigCollector.get().put("key3", "value3", ConfigOrigin.JVM_PROP, ABSENT_SEQ_ID); + + Map> collected = ConfigCollector.get().collect(); + assertEquals( + ConfigSetting.of("key1", "value4", ConfigOrigin.REMOTE), + collected.get(ConfigOrigin.REMOTE).get("key1")); + assertEquals( + ConfigSetting.of("key2", "value2", ConfigOrigin.ENV), + collected.get(ConfigOrigin.ENV).get("key2")); + assertEquals( + ConfigSetting.of("key3", "value3", ConfigOrigin.JVM_PROP), + collected.get(ConfigOrigin.JVM_PROP).get("key3")); + assertEquals( + ConfigSetting.of("key1", "value1", ConfigOrigin.DEFAULT), + collected.get(ConfigOrigin.DEFAULT).get("key1")); + } + + @Test + void hidePiiConfigurationData() { + ConfigCollector.get().collect(); + + ConfigCollector.get().put("DD_API_KEY", "sensitive data", ConfigOrigin.ENV, ABSENT_SEQ_ID); + + Map> collected = ConfigCollector.get().collect(); + assertEquals("", collected.get(ConfigOrigin.ENV).get("DD_API_KEY").stringValue()); + } + + @TableTest({ + "scenario | key | value ", + "trace enabled | trace.enabled | true ", + "profiling enabled | profiling.enabled | false ", + "appsec enabled | appsec.enabled | inactive", + "data streams | data.streams.enabled | false ", + "trace tags | trace.tags | '' ", + "trace header tags | trace.header.tags | '' ", + "logs injection | logs.injection.enabled | true ", + "trace sample rate | trace.sample.rate | " + }) + void collectsCommonSettingDefaultValues(String key, String value) { + Map defaultConfigByKey = + ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT); + + ConfigSetting setting = defaultConfigByKey.get(key); + assertEquals(key, setting.key); + assertEquals(value, setting.stringValue()); + assertEquals(ConfigOrigin.DEFAULT, setting.origin); + } + + @TableTest({ + "scenario | key | value ", + "trace enabled | trace.enabled | false ", + "profiling enabled | profiling.enabled | true ", + "appsec enabled | appsec.enabled | false ", + "data streams | data.streams.enabled | true ", + "trace tags | trace.tags | component:web,team:apm ", + "trace header tags | trace.header.tags | x-header-tag-1:header_tag_1,x-header-tag-2:header_tag_2", + "logs injection | logs.injection.enabled | false ", + "trace sample rate | trace.sample.rate | 0.3 " + }) + void collectsCommonSettingOverriddenValues(String key, String value) { + injectEnvConfig("DD_TRACE_ENABLED", "false"); + injectEnvConfig("DD_PROFILING_ENABLED", "true"); + injectEnvConfig("DD_APPSEC_ENABLED", "false"); + injectEnvConfig("DD_DATA_STREAMS_ENABLED", "true"); + injectEnvConfig("DD_TAGS", "team:apm,component:web"); + injectEnvConfig( + "DD_TRACE_HEADER_TAGS", "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2"); + injectEnvConfig("DD_LOGS_INJECTION", "false"); + injectEnvConfig("DD_TRACE_SAMPLE_RATE", "0.3"); + + Map envConfigByKey = + ConfigCollector.get().collect().get(ConfigOrigin.ENV); + + ConfigSetting setting = envConfigByKey.get(key); + assertEquals(key, setting.key); + assertEquals(value, setting.stringValue()); + assertEquals(ConfigOrigin.ENV, setting.origin); + } + + @Test + void configCollectorCreatesConfigSettingsWithCorrectSeqId() { + ConfigCollector.get().collect(); // clear previous state + + // Simulate sources with increasing precedence and a default + ConfigCollector.get() + .put("test.key", "default", ConfigOrigin.DEFAULT, ConfigSetting.DEFAULT_SEQ_ID); + ConfigCollector.get().put("test.key", "env", ConfigOrigin.ENV, 2); + ConfigCollector.get().put("test.key", "jvm", ConfigOrigin.JVM_PROP, 3); + ConfigCollector.get().put("test.key", "remote", ConfigOrigin.REMOTE, 4); + + Map> collected = ConfigCollector.get().collect(); + ConfigSetting defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.key"); + ConfigSetting envSetting = collected.get(ConfigOrigin.ENV).get("test.key"); + ConfigSetting jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.key"); + ConfigSetting remoteSetting = collected.get(ConfigOrigin.REMOTE).get("test.key"); + + assertEquals(ConfigSetting.DEFAULT_SEQ_ID, defaultSetting.seqId); + // Higher precedence = higher seqId + assertTrue(defaultSetting.seqId < envSetting.seqId); + assertTrue(envSetting.seqId < jvmSetting.seqId); + assertTrue(jvmSetting.seqId < remoteSetting.seqId); + } + + @Test + void configIdIsNullForNonStableConfigSource() { + ConfigHelper.StrictnessPolicy strictness = ConfigHelper.get().configInversionStrictFlag(); + ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.TEST); + + String key = "test.key"; + String value = "test-value"; + injectSysConfig(key, value); + + try { + // Trigger config collection by getting a value + ConfigProvider.getInstance().getString(key); + Map> settings = ConfigCollector.get().collect(); + + // Verify the config was collected but without a config ID + ConfigSetting setting = settings.get(ConfigOrigin.JVM_PROP).get(key); + assertNotNull(setting); + assertNull(setting.configId); + assertEquals(value, setting.value); + assertEquals(ConfigOrigin.JVM_PROP, setting.origin); + } finally { + ConfigHelper.get().setConfigInversionStrict(strictness); + } + } + + @Test + void defaultSourcesCannotBeOverridden() { + String key = "test.key"; + String value = "test-value"; + String overrideVal = "override-value"; + + // Need to make 2 calls in a row because collect() will empty the map + ConfigCollector.get().putDefault(key, value); + ConfigCollector.get().putDefault(key, overrideVal); + Map defaultConfigByKey = + ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT); + ConfigSetting cs = defaultConfigByKey.get(key); + + assertEquals(key, cs.key); + assertEquals(value, cs.stringValue()); + assertEquals(ConfigOrigin.DEFAULT, cs.origin); + assertEquals(ConfigSetting.DEFAULT_SEQ_ID, cs.seqId); + } +} diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index a8588354206..a756b3dfb80 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2611,7 +2611,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_PROFILING_APIKEY_FILE": [ @@ -2627,7 +2628,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_PROFILING_API_KEY_FILE": [ diff --git a/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java b/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java index f6160db5804..d1aaa88ac17 100644 --- a/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java +++ b/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java @@ -25,26 +25,30 @@ public final class ConfigSetting { // Configuration keys whose values are excluded from configuration telemetry by replacing them // with "". Keys are listed in every form that may reach this constructor: the dotted - // configuration name (used by ConfigProvider) and the environment-variable name. + // configuration name (used by ConfigProvider) and the environment-variable name. Keep this list + // in sync with the "sensitive": true entries in metadata/supported-configurations.json. private static final Set CONFIG_FILTER_LIST = new HashSet<>( Arrays.asList( "DD_API_KEY", - "dd.api-key", - "dd.profiling.api-key", - "dd.profiling.apikey", - "application-key", - "dd.application-key", "DD_APPLICATION_KEY", + "DD_PROFILING_API_KEY", + "DD_PROFILING_APIKEY", + "OTEL_EXPORTER_OTLP_HEADERS", + "OTEL_EXPORTER_OTLP_LOGS_HEADERS", + "OTEL_EXPORTER_OTLP_METRICS_HEADERS", + "OTEL_EXPORTER_OTLP_TRACES_HEADERS", + "api-key", "app-key", + "application-key", + "dd.api-key", "dd.app-key", - "otlp.traces.headers", - "otlp.metrics.headers", + "dd.application-key", + "dd.profiling.api-key", + "dd.profiling.apikey", "otlp.logs.headers", - "OTEL_EXPORTER_OTLP_HEADERS", - "OTEL_EXPORTER_OTLP_TRACES_HEADERS", - "OTEL_EXPORTER_OTLP_METRICS_HEADERS", - "OTEL_EXPORTER_OTLP_LOGS_HEADERS")); + "otlp.metrics.headers", + "otlp.traces.headers")); public static ConfigSetting of(String key, Object value, ConfigOrigin origin) { return new ConfigSetting(key, value, origin, ABSENT_SEQ_ID, null); diff --git a/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java b/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java index 3ed0c933f0c..2ceedc96dec 100644 --- a/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java +++ b/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java @@ -1,7 +1,6 @@ package datadog.trace.api; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import datadog.trace.junit.utils.tabletest.BoxedValueConverter; @@ -43,47 +42,32 @@ void supportsEqualityCheck( } @TableTest({ - "scenario | key | value | filteredValue", - "dd api key env | DD_API_KEY | somevalue | ", - "dd api key prop | dd.api-key | somevalue | ", - "profiling api key | dd.profiling.api-key | somevalue | ", - "profiling apikey | dd.profiling.apikey | somevalue | ", - "application key name | application-key | somevalue | ", - "application key prop | dd.application-key | somevalue | ", - "application key env | DD_APPLICATION_KEY | somevalue | ", - "app key alias name | app-key | somevalue | ", - "app key alias prop | dd.app-key | somevalue | ", - "otlp traces headers | otlp.traces.headers | somevalue | ", - "otlp metrics headers | otlp.metrics.headers | somevalue | ", - "otlp logs headers | otlp.logs.headers | somevalue | ", - "otel otlp headers | OTEL_EXPORTER_OTLP_HEADERS | somevalue | ", - "otel traces headers | OTEL_EXPORTER_OTLP_TRACES_HEADERS | somevalue | ", - "otel metrics headers | OTEL_EXPORTER_OTLP_METRICS_HEADERS | somevalue | ", - "otel logs headers | OTEL_EXPORTER_OTLP_LOGS_HEADERS | somevalue | ", - "other key | some.other.key | somevalue | somevalue " + "scenario | key | value | filteredValue", + "dd api key env | DD_API_KEY | somevalue | ", + "dd api key prop | dd.api-key | somevalue | ", + "api key name | api-key | somevalue | ", + "profiling api key | dd.profiling.api-key | somevalue | ", + "profiling apikey | dd.profiling.apikey | somevalue | ", + "profiling api key env | DD_PROFILING_API_KEY | somevalue | ", + "profiling apikey env | DD_PROFILING_APIKEY | somevalue | ", + "application key name | application-key | somevalue | ", + "application key prop | dd.application-key | somevalue | ", + "application key env | DD_APPLICATION_KEY | somevalue | ", + "app key alias name | app-key | somevalue | ", + "app key alias prop | dd.app-key | somevalue | ", + "otlp traces headers | otlp.traces.headers | somevalue | ", + "otlp metrics headers | otlp.metrics.headers | somevalue | ", + "otlp logs headers | otlp.logs.headers | somevalue | ", + "otel otlp headers | OTEL_EXPORTER_OTLP_HEADERS | somevalue | ", + "otel traces headers | OTEL_EXPORTER_OTLP_TRACES_HEADERS | somevalue | ", + "otel metrics headers | OTEL_EXPORTER_OTLP_METRICS_HEADERS | somevalue | ", + "otel logs headers | OTEL_EXPORTER_OTLP_LOGS_HEADERS | somevalue | ", + "other key | some.other.key | somevalue | somevalue " }) void filtersKeyValues(String key, String value, String filteredValue) { assertEquals(filteredValue, ConfigSetting.of(key, value, ConfigOrigin.DEFAULT).stringValue()); } - @TableTest({ - "scenario | key | value ", - "otlp traces | otlp.traces.headers | dd-api-key=secret-traces ", - "otlp metrics | otlp.metrics.headers | dd-api-key=secret-metrics", - "otlp logs | otlp.logs.headers | dd-api-key=secret-logs ", - "otel base | OTEL_EXPORTER_OTLP_HEADERS | dd-api-key=secret-base ", - "otel traces | OTEL_EXPORTER_OTLP_TRACES_HEADERS | dd-api-key=secret-traces ", - "otel metrics | OTEL_EXPORTER_OTLP_METRICS_HEADERS | dd-api-key=secret-metrics", - "otel logs | OTEL_EXPORTER_OTLP_LOGS_HEADERS | dd-api-key=secret-logs ", - "dd api key | DD_API_KEY | secret-api-key " - }) - void doesNotExposeSensitiveValues(String key, String value) { - String rendered = ConfigSetting.of(key, value, ConfigOrigin.ENV).stringValue(); - assertEquals("", rendered); - assertFalse( - rendered.contains(value), "rendered telemetry value must not contain the configured value"); - } - @TableTest({ "scenario | value | rendered", "null | | ", diff --git a/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java b/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java index 647f727e76b..8a1ad77e042 100644 --- a/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java +++ b/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java @@ -1,8 +1,8 @@ package datadog.trace.api; import static datadog.trace.util.ConfigStrings.toEnvVar; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import java.io.IOException; import java.io.UncheckedIOException; @@ -21,32 +21,17 @@ import org.snakeyaml.engine.v2.api.LoadSettings; /** - * Drift-guard test that keeps the {@code "sensitive": true} attribute in {@code - * metadata/supported-configurations.json} consistent with the redaction actually performed by - * {@link ConfigSetting}. - * - *

Telemetry redaction is driven by {@code ConfigSetting.CONFIG_FILTER_LIST}; the registry - * attribute is otherwise not read at runtime. Without this guard, marking a configuration {@code - * sensitive: true} in the registry without adding it to the filter list (or vice-versa) would - * silently leave that configuration unredacted in configuration telemetry. This test fails CI when - * the two drift apart. + * Drift-guard test keeping the {@code "sensitive": true} entries in {@code + * metadata/supported-configurations.json} in sync with {@code ConfigSetting.CONFIG_FILTER_LIST}. + * The registry attribute is not read at runtime, so this test is what keeps the two from drifting. */ public class SensitiveConfigRedactionTest { private static final String REGISTRY_RELATIVE_PATH = "metadata/supported-configurations.json"; - /** - * Normalizes any config name form -- env-var ({@code DD_API_KEY}), dotted system property ({@code - * dd.api-key}), or bare dotted name ({@code otlp.traces.headers}) -- to a single canonical token - * so the registry keys and the filter-list entries can be compared. - * - *

{@link datadog.trace.util.ConfigStrings#toEnvVar(String)} upper-cases and replaces {@code .} - * / {@code -} with {@code _}, but it does not unify the {@code DD_} prefix: a registry env name - * such as {@code DD_OTLP_TRACES_HEADERS} and the filter's dotted {@code otlp.traces.headers} - * (which {@code toEnvVar} turns into {@code OTLP_TRACES_HEADERS}) would otherwise not match. We - * strip a leading {@code DD_} after {@code toEnvVar} so both collapse onto {@code - * OTLP_TRACES_HEADERS}. {@code OTEL_*} names have no {@code DD_} prefix and are unaffected. - */ + // Normalizes any config name form (env-var, dotted system property, or bare dotted name) to a + // single canonical token. toEnvVar upper-cases and replaces "." / "-" with "_"; we then strip a + // leading "DD_" so dotted and env-var forms of the same config collapse onto the same token. private static String canonical(String name) { String env = toEnvVar(name); if (env.startsWith("DD_")) { @@ -56,64 +41,26 @@ private static String canonical(String name) { } @Test - void everySensitiveConfigIsRedacted() { - Set sensitiveRegistryKeys = sensitiveRegistryKeys(); - assertTrue( - !sensitiveRegistryKeys.isEmpty(), - "expected at least one config marked \"sensitive\": true in " + REGISTRY_RELATIVE_PATH); - - Set filterCanonical = - configFilterList().stream() + void sensitiveRegistryEntriesAndFilterListStayInSync() { + Set registryCanonical = + sensitiveRegistryKeys().stream() .map(SensitiveConfigRedactionTest::canonical) .collect(toTreeSet()); - - Set notRedacted = new TreeSet<>(); - for (String key : sensitiveRegistryKeys) { - if (!filterCanonical.contains(canonical(key))) { - notRedacted.add(key); - } - } - - if (!notRedacted.isEmpty()) { - fail( - "These configurations are marked \"sensitive\": true in " - + REGISTRY_RELATIVE_PATH - + " but are NOT redacted by ConfigSetting.CONFIG_FILTER_LIST. Add them (in env-var " - + "and/or dotted form) to CONFIG_FILTER_LIST in ConfigSetting.java, or drop the " - + "\"sensitive\": true marker:\n " - + String.join("\n ", notRedacted)); - } - } - - /** - * Advisory only: surfaces filter-list entries that have no {@code "sensitive": true} counterpart - * in the registry. This does not fail the build -- some entries (e.g. profiling api keys) are - * legitimately redacted without being registry-sensitive -- but it makes intentional asymmetry - * visible in the logs. - */ - @Test - void reportsFilterEntriesNotMarkedSensitive() { - Set sensitiveCanonical = - sensitiveRegistryKeys().stream() + Set filterCanonical = + configFilterList().stream() .map(SensitiveConfigRedactionTest::canonical) .collect(toTreeSet()); - Set filterOnly = new TreeSet<>(); - for (String entry : configFilterList()) { - if (!sensitiveCanonical.contains(canonical(entry))) { - filterOnly.add(entry); - } - } - - if (!filterOnly.isEmpty()) { - System.out.println( - "[advisory] CONFIG_FILTER_LIST entries with no \"sensitive\": true marker in " - + REGISTRY_RELATIVE_PATH - + " (not a failure): " - + filterOnly); - } + assertFalse(registryCanonical.isEmpty(), "expected at least one \"sensitive\": true config"); + assertEquals( + registryCanonical, + filterCanonical, + "Registry \"sensitive\": true entries (with aliases) and ConfigSetting.CONFIG_FILTER_LIST " + + "must match after canonicalization. Reconcile metadata/supported-configurations.json " + + "and CONFIG_FILTER_LIST in ConfigSetting.java."); } + // Registry keys plus their aliases for every entry marked "sensitive": true. @SuppressWarnings("unchecked") private static Set sensitiveRegistryKeys() { Path registry = locateRegistry(); @@ -130,19 +77,23 @@ private static Set sensitiveRegistryKeys() { Set sensitive = new TreeSet<>(); for (Map.Entry entry : supported.entrySet()) { - // Each value is a list of versioned definitions; the config is sensitive if any marks it so. for (Object def : (List) entry.getValue()) { - Object flag = ((Map) def).get("sensitive"); - if (Boolean.TRUE.equals(flag)) { + Map definition = (Map) def; + if (Boolean.TRUE.equals(definition.get("sensitive"))) { sensitive.add(entry.getKey()); - break; + Object aliases = definition.get("aliases"); + if (aliases instanceof List) { + for (Object alias : (List) aliases) { + sensitive.add((String) alias); + } + } } } } return sensitive; } - /** Reads {@code CONFIG_FILTER_LIST} from {@link ConfigSetting} via reflection. */ + // Reads CONFIG_FILTER_LIST from ConfigSetting via reflection. @SuppressWarnings("unchecked") private static Set configFilterList() { try { @@ -154,7 +105,7 @@ private static Set configFilterList() { } } - /** Walks up from the working directory until {@code metadata/supported-configurations.json}. */ + // Walks up from the working directory until metadata/supported-configurations.json is found. private static Path locateRegistry() { Path dir = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); for (Path current = dir; current != null; current = current.getParent()) { @@ -163,13 +114,7 @@ private static Path locateRegistry() { return candidate; } } - throw new IllegalStateException( - "Could not locate " - + REGISTRY_RELATIVE_PATH - + " by walking up from " - + dir - + ". Adjust the resolution logic in " - + SensitiveConfigRedactionTest.class.getName()); + throw new IllegalStateException("Could not locate " + REGISTRY_RELATIVE_PATH + " from " + dir); } private static java.util.stream.Collector> toTreeSet() { From ae0d111b5bcf6336e18ae61e393aba45cadefba6 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 11 Jun 2026 19:19:29 -0400 Subject: [PATCH 4/6] Simplify config redaction and extend it to all collected credential configs Collect the profiling api key under its property name (single DD_ telemetry name), reduce CONFIG_FILTER_LIST to the property-name forms values are actually collected under, and map OTEL headers to their OTLP collected form in the drift guard. Also mark and redact the remaining collected credential configs: the profiling and crash-tracking proxy passwords and the RUM client token. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../main/java/datadog/trace/api/Config.java | 20 +++++++++--- .../trace/api/ConfigCollectorTest.java | 9 ++++-- metadata/supported-configurations.json | 9 ++++-- .../java/datadog/trace/api/ConfigSetting.java | 30 +++++++---------- .../datadog/trace/api/ConfigSettingTest.java | 32 +++++++------------ .../api/SensitiveConfigRedactionTest.java | 32 +++++++++++-------- 6 files changed, 69 insertions(+), 63 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 6418bab301e..5e9ff56ce34 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -2311,7 +2311,9 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) if (tmpApiKey == null) { final String oldProfilingApiKeyFile = configProvider.getString(PROFILING_API_KEY_FILE_OLD); - tmpApiKey = getEnv(propertyNameToEnvironmentVariableName(PROFILING_API_KEY_OLD)); + tmpApiKey = + getEnvCollectedAs( + propertyNameToEnvironmentVariableName(PROFILING_API_KEY_OLD), PROFILING_API_KEY_OLD); if (oldProfilingApiKeyFile != null) { try { tmpApiKey = @@ -2326,7 +2328,10 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) if (tmpApiKey == null) { final String veryOldProfilingApiKeyFile = configProvider.getString(PROFILING_API_KEY_FILE_VERY_OLD); - tmpApiKey = getEnv(propertyNameToEnvironmentVariableName(PROFILING_API_KEY_VERY_OLD)); + tmpApiKey = + getEnvCollectedAs( + propertyNameToEnvironmentVariableName(PROFILING_API_KEY_VERY_OLD), + PROFILING_API_KEY_VERY_OLD); if (veryOldProfilingApiKeyFile != null) { try { tmpApiKey = @@ -6117,10 +6122,17 @@ private static boolean isWindowsOS() { } private static String getEnv(String name) { - String value = ConfigHelper.env(name); + return getEnvCollectedAs(name, name); + } + + // Reads an environment variable and, when set, records it in configuration telemetry under the + // given configuration key. Pass a property-name collectKey (rather than the raw env-var name) so + // the value normalizes to a single DD_ telemetry name like every other setting. + private static String getEnvCollectedAs(String envName, String collectKey) { + String value = ConfigHelper.env(envName); if (value != null) { // Report non-default sequence id for consistency - ConfigCollector.get().put(name, value, ConfigOrigin.ENV, NON_DEFAULT_SEQ_ID); + ConfigCollector.get().put(collectKey, value, ConfigOrigin.ENV, NON_DEFAULT_SEQ_ID); } return value; } diff --git a/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java b/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java index c9ce9f0fa85..11514199833 100644 --- a/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java +++ b/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java @@ -16,6 +16,7 @@ import datadog.trace.api.config.GeneralConfig; import datadog.trace.api.config.IastConfig; import datadog.trace.api.config.JmxFetchConfig; +import datadog.trace.api.config.ProfilingConfig; import datadog.trace.api.config.TraceInstrumentationConfig; import datadog.trace.api.config.TracerConfig; import datadog.trace.api.iast.telemetry.Verbosity; @@ -54,6 +55,8 @@ static Stream nonDefaultConfigSettingsGetCollectedArguments() { // ConfigProvider.getStringExcludingSource, redacted from configuration telemetry arguments(GeneralConfig.APPLICATION_KEY, "app-key", ""), arguments(GeneralConfig.API_KEY, "some-api-key", ""), + // ConfigProvider.getString, redacted from configuration telemetry + arguments(ProfilingConfig.PROFILING_PROXY_PASSWORD, "some-proxy-password", ""), // ConfigProvider.getBoolean arguments(TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES, "true", null), // ConfigProvider.getInteger @@ -243,13 +246,13 @@ void putGetConfigurations() { } @Test - void hidePiiConfigurationData() { + void redactsSensitiveConfigurationValues() { ConfigCollector.get().collect(); - ConfigCollector.get().put("DD_API_KEY", "sensitive data", ConfigOrigin.ENV, ABSENT_SEQ_ID); + ConfigCollector.get().put("api-key", "somevalue", ConfigOrigin.ENV, ABSENT_SEQ_ID); Map> collected = ConfigCollector.get().collect(); - assertEquals("", collected.get(ConfigOrigin.ENV).get("DD_API_KEY").stringValue()); + assertEquals("", collected.get(ConfigOrigin.ENV).get("api-key").stringValue()); } @TableTest({ diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index a756b3dfb80..18109ab2b43 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -1008,7 +1008,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_CRASHTRACKING_PROXY_PORT": [ @@ -3261,7 +3262,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_PROFILING_PROXY_PORT": [ @@ -3645,7 +3647,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_RUM_DEFAULT_PRIVACY_LEVEL": [ diff --git a/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java b/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java index d1aaa88ac17..3cdeadcaff1 100644 --- a/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java +++ b/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java @@ -23,32 +23,24 @@ public final class ConfigSetting { /** The config ID associated with this setting, or {@code null} if not applicable. */ public final String configId; - // Configuration keys whose values are excluded from configuration telemetry by replacing them - // with "". Keys are listed in every form that may reach this constructor: the dotted - // configuration name (used by ConfigProvider) and the environment-variable name. Keep this list - // in sync with the "sensitive": true entries in metadata/supported-configurations.json. + // Configuration property names whose values are excluded from configuration telemetry by + // replacing them with "". These are the keys under which the values are collected (the + // property-name form used by ConfigProvider); every sensitive setting is collected under one of + // these regardless of which env-var/alias the user set. Keep in sync with the "sensitive": true + // entries in metadata/supported-configurations.json. private static final Set CONFIG_FILTER_LIST = new HashSet<>( Arrays.asList( - "DD_API_KEY", - "DD_APPLICATION_KEY", - "DD_PROFILING_API_KEY", - "DD_PROFILING_APIKEY", - "OTEL_EXPORTER_OTLP_HEADERS", - "OTEL_EXPORTER_OTLP_LOGS_HEADERS", - "OTEL_EXPORTER_OTLP_METRICS_HEADERS", - "OTEL_EXPORTER_OTLP_TRACES_HEADERS", "api-key", - "app-key", "application-key", - "dd.api-key", - "dd.app-key", - "dd.application-key", - "dd.profiling.api-key", - "dd.profiling.apikey", + "crashtracking.proxy.password", "otlp.logs.headers", "otlp.metrics.headers", - "otlp.traces.headers")); + "otlp.traces.headers", + "profiling.api-key", + "profiling.apikey", + "profiling.proxy.password", + "rum.client.token")); public static ConfigSetting of(String key, Object value, ConfigOrigin origin) { return new ConfigSetting(key, value, origin, ABSENT_SEQ_ID, null); diff --git a/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java b/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java index 2ceedc96dec..f70761786b0 100644 --- a/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java +++ b/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java @@ -41,28 +41,18 @@ void supportsEqualityCheck( } } + // Sensitive values are redacted by the property-name key under which they are collected. A couple + // of representative sensitive keys plus a non-sensitive control; the full filter list is kept in + // sync with the registry by SensitiveConfigRedactionTest. @TableTest({ - "scenario | key | value | filteredValue", - "dd api key env | DD_API_KEY | somevalue | ", - "dd api key prop | dd.api-key | somevalue | ", - "api key name | api-key | somevalue | ", - "profiling api key | dd.profiling.api-key | somevalue | ", - "profiling apikey | dd.profiling.apikey | somevalue | ", - "profiling api key env | DD_PROFILING_API_KEY | somevalue | ", - "profiling apikey env | DD_PROFILING_APIKEY | somevalue | ", - "application key name | application-key | somevalue | ", - "application key prop | dd.application-key | somevalue | ", - "application key env | DD_APPLICATION_KEY | somevalue | ", - "app key alias name | app-key | somevalue | ", - "app key alias prop | dd.app-key | somevalue | ", - "otlp traces headers | otlp.traces.headers | somevalue | ", - "otlp metrics headers | otlp.metrics.headers | somevalue | ", - "otlp logs headers | otlp.logs.headers | somevalue | ", - "otel otlp headers | OTEL_EXPORTER_OTLP_HEADERS | somevalue | ", - "otel traces headers | OTEL_EXPORTER_OTLP_TRACES_HEADERS | somevalue | ", - "otel metrics headers | OTEL_EXPORTER_OTLP_METRICS_HEADERS | somevalue | ", - "otel logs headers | OTEL_EXPORTER_OTLP_LOGS_HEADERS | somevalue | ", - "other key | some.other.key | somevalue | somevalue " + "scenario | key | value | filteredValue", + "api key | api-key | somevalue | ", + "application key | application-key | somevalue | ", + "otlp traces headers | otlp.traces.headers | somevalue | ", + "profiling api key | profiling.api-key | somevalue | ", + "proxy password | crashtracking.proxy.password | somevalue | ", + "rum client token | rum.client.token | somevalue | ", + "non-sensitive key | some.other.key | somevalue | somevalue " }) void filtersKeyValues(String key, String value, String filteredValue) { assertEquals(filteredValue, ConfigSetting.of(key, value, ConfigOrigin.DEFAULT).stringValue()); diff --git a/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java b/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java index 8a1ad77e042..711a620d99d 100644 --- a/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java +++ b/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java @@ -29,14 +29,24 @@ public class SensitiveConfigRedactionTest { private static final String REGISTRY_RELATIVE_PATH = "metadata/supported-configurations.json"; - // Normalizes any config name form (env-var, dotted system property, or bare dotted name) to a - // single canonical token. toEnvVar upper-cases and replaces "." / "-" with "_"; we then strip a - // leading "DD_" so dotted and env-var forms of the same config collapse onto the same token. + // Normalizes a config name to the canonical token under which its value is COLLECTED, so the + // registry's public names line up with the property-name forms in CONFIG_FILTER_LIST. toEnvVar + // upper-cases and replaces "." / "-" with "_"; we strip a leading "DD_" so the dotted property + // name and the DD_ env-var form of the same config collapse together. OTLP exporter headers set + // via the OpenTelemetry env vars are collected under the Datadog otlp..headers keys, so + // the OTEL_ names map onto that collected form. private static String canonical(String name) { String env = toEnvVar(name); if (env.startsWith("DD_")) { env = env.substring("DD_".length()); } + if (env.equals("OTEL_EXPORTER_OTLP_HEADERS")) { + // The generic OTEL header env var funnels into every otlp..headers; traces stands in. + return "OTLP_TRACES_HEADERS"; + } + if (env.startsWith("OTEL_EXPORTER_OTLP_") && env.endsWith("_HEADERS")) { + return "OTLP_" + env.substring("OTEL_EXPORTER_OTLP_".length()); + } return env; } @@ -55,12 +65,14 @@ void sensitiveRegistryEntriesAndFilterListStayInSync() { assertEquals( registryCanonical, filterCanonical, - "Registry \"sensitive\": true entries (with aliases) and ConfigSetting.CONFIG_FILTER_LIST " - + "must match after canonicalization. Reconcile metadata/supported-configurations.json " - + "and CONFIG_FILTER_LIST in ConfigSetting.java."); + "Registry \"sensitive\": true entries and ConfigSetting.CONFIG_FILTER_LIST must match after " + + "canonicalization. Reconcile metadata/supported-configurations.json and " + + "CONFIG_FILTER_LIST in ConfigSetting.java."); } - // Registry keys plus their aliases for every entry marked "sensitive": true. + // Registry keys for every entry marked "sensitive": true. Aliases are not collected separately -- + // a value is always collected under its primary key's property name -- so they are not needed + // here. @SuppressWarnings("unchecked") private static Set sensitiveRegistryKeys() { Path registry = locateRegistry(); @@ -81,12 +93,6 @@ private static Set sensitiveRegistryKeys() { Map definition = (Map) def; if (Boolean.TRUE.equals(definition.get("sensitive"))) { sensitive.add(entry.getKey()); - Object aliases = definition.get("aliases"); - if (aliases instanceof List) { - for (Object alias : (List) aliases) { - sensitive.add((String) alias); - } - } } } } From 31f95fc82a9b4b43754df62cac57ada41c4d5abb Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 11 Jun 2026 19:59:07 -0400 Subject: [PATCH 5/6] Drop deprecated profiling api-key fallbacks; keep ConfigCollectorTest in Groovy Remove redaction of the deprecated profiling.api-key/profiling.apikey fallback env vars (and the getEnvCollectedAs helper they needed). Only redact non-null values, so an unset sensitive config still reports null rather than . Revert ConfigCollectorTest from JUnit back to its original Groovy form with minimal redaction edits, and trim the ConfigSettingTest table to a few representative cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../main/java/datadog/trace/api/Config.java | 20 +- .../trace/api/ConfigCollectorTest.groovy | 325 +++++++++++++++ .../trace/api/ConfigCollectorTest.java | 377 ------------------ metadata/supported-configurations.json | 6 +- .../java/datadog/trace/api/ConfigSetting.java | 6 +- .../datadog/trace/api/ConfigSettingTest.java | 13 +- 6 files changed, 339 insertions(+), 408 deletions(-) create mode 100644 internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy delete mode 100644 internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 5e9ff56ce34..6418bab301e 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -2311,9 +2311,7 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) if (tmpApiKey == null) { final String oldProfilingApiKeyFile = configProvider.getString(PROFILING_API_KEY_FILE_OLD); - tmpApiKey = - getEnvCollectedAs( - propertyNameToEnvironmentVariableName(PROFILING_API_KEY_OLD), PROFILING_API_KEY_OLD); + tmpApiKey = getEnv(propertyNameToEnvironmentVariableName(PROFILING_API_KEY_OLD)); if (oldProfilingApiKeyFile != null) { try { tmpApiKey = @@ -2328,10 +2326,7 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) if (tmpApiKey == null) { final String veryOldProfilingApiKeyFile = configProvider.getString(PROFILING_API_KEY_FILE_VERY_OLD); - tmpApiKey = - getEnvCollectedAs( - propertyNameToEnvironmentVariableName(PROFILING_API_KEY_VERY_OLD), - PROFILING_API_KEY_VERY_OLD); + tmpApiKey = getEnv(propertyNameToEnvironmentVariableName(PROFILING_API_KEY_VERY_OLD)); if (veryOldProfilingApiKeyFile != null) { try { tmpApiKey = @@ -6122,17 +6117,10 @@ private static boolean isWindowsOS() { } private static String getEnv(String name) { - return getEnvCollectedAs(name, name); - } - - // Reads an environment variable and, when set, records it in configuration telemetry under the - // given configuration key. Pass a property-name collectKey (rather than the raw env-var name) so - // the value normalizes to a single DD_ telemetry name like every other setting. - private static String getEnvCollectedAs(String envName, String collectKey) { - String value = ConfigHelper.env(envName); + String value = ConfigHelper.env(name); if (value != null) { // Report non-default sequence id for consistency - ConfigCollector.get().put(collectKey, value, ConfigOrigin.ENV, NON_DEFAULT_SEQ_ID); + ConfigCollector.get().put(name, value, ConfigOrigin.ENV, NON_DEFAULT_SEQ_ID); } return value; } diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy new file mode 100644 index 00000000000..bbb88b6ef96 --- /dev/null +++ b/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy @@ -0,0 +1,325 @@ +package datadog.trace.api + +import datadog.trace.api.config.AppSecConfig +import datadog.trace.api.config.CiVisibilityConfig +import datadog.trace.api.config.GeneralConfig +import datadog.trace.api.config.IastConfig +import datadog.trace.api.config.JmxFetchConfig +import datadog.trace.api.config.TraceInstrumentationConfig +import datadog.trace.api.config.TracerConfig +import datadog.trace.api.iast.telemetry.Verbosity +import datadog.trace.api.naming.SpanNaming +import datadog.trace.bootstrap.config.provider.ConfigProvider +import datadog.trace.config.inversion.ConfigHelper +import datadog.trace.test.util.DDSpecification +import datadog.trace.util.ConfigStrings + +import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_WEAK_HASH_ALGORITHMS +import static datadog.trace.api.ConfigDefaults.DEFAULT_TELEMETRY_HEARTBEAT_INTERVAL +import static datadog.trace.api.ConfigSetting.ABSENT_SEQ_ID + +class ConfigCollectorTest extends DDSpecification { + + def "non-default config settings get collected"() { + setup: + injectEnvConfig(ConfigStrings.toEnvVar(configKey), configValue) + + expect: + def envConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.ENV) + def config = envConfigByKey.get(configKey) + config.stringValue() == configValue + config.origin == ConfigOrigin.ENV + + where: + configKey | configValue + // ConfigProvider.getEnum + IastConfig.IAST_TELEMETRY_VERBOSITY | Verbosity.DEBUG.toString() + // ConfigProvider.getString + TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA | "v1" + // ConfigProvider.getStringNotEmpty + AppSecConfig.APPSEC_AUTOMATED_USER_EVENTS_TRACKING | UserEventTrackingMode.EXTENDED.toString() + // ConfigProvider.getStringExcludingSource + DDTags.SERVICE | "my-service" + // ConfigProvider.getBoolean + TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES | "true" + // ConfigProvider.getInteger + JmxFetchConfig.JMX_FETCH_CHECK_PERIOD | "60" + // ConfigProvider.getLong + CiVisibilityConfig.CIVISIBILITY_GIT_COMMAND_TIMEOUT_MILLIS | "450273" + // ConfigProvider.getFloat + GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL | "1.5" + // ConfigProvider.getDouble + TracerConfig.TRACE_SAMPLE_RATE | "2.2" + // ConfigProvider.getList + TraceInstrumentationConfig.JMS_PROPAGATION_DISABLED_TOPICS | "someTopic,otherTopic" + // ConfigProvider.getSet + IastConfig.IAST_WEAK_HASH_ALGORITHMS | "SHA1,SHA-1" + // ConfigProvider.getSpacedList + TracerConfig.PROXY_NO_PROXY | "a b c" + // ConfigProvider.getMergedMap + TracerConfig.TRACE_PEER_SERVICE_MAPPING | "service1:best_service,userService:my_service" + // ConfigProvider.getOrderedMap + TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING | "/asdf/*:/test" + // ConfigProvider.getMergedMapWithOptionalMappings + TracerConfig.HEADER_TAGS | "e:five" + // ConfigProvider.getIntegerRange + TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES | "400-402" + } + + def "should collect merged data from multiple sources"() { + setup: + injectEnvConfig(ConfigStrings.toEnvVar(configKey), envConfigValue) + if (jvmConfigValue != null) { + injectSysConfig(configKey, jvmConfigValue) + } + + when: + def collected = ConfigCollector.get().collect() + + then: + def envSetting = collected.get(ConfigOrigin.ENV) + def envConfig = envSetting.get(configKey) + envConfig.stringValue() == envConfigValue + envConfig.origin == ConfigOrigin.ENV + if (jvmConfigValue != null ) { + def jvmSetting = collected.get(ConfigOrigin.JVM_PROP) + def jvmConfig = jvmSetting.get(configKey) + jvmConfig.stringValue().split(',') as Set == jvmConfigValue.split(',') as Set + jvmConfig.origin == ConfigOrigin.JVM_PROP + } + + + // TODO: Add a check for which setting the collector recognizes as highest precedence + + where: + configKey | envConfigValue | jvmConfigValue | expectedValue | expectedOrigin + // ConfigProvider.getMergedMap + TracerConfig.TRACE_PEER_SERVICE_MAPPING | "service1:best_service,userService:my_service" | "service2:backup_service" | "service2:backup_service,service1:best_service,userService:my_service" | ConfigOrigin.CALCULATED + // ConfigProvider.getOrderedMap + TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING | "/asdf/*:/test,/b:some" | "/a:prop" | "/asdf/*:/test,/b:some,/a:prop" | ConfigOrigin.CALCULATED + // ConfigProvider.getMergedMapWithOptionalMappings + TracerConfig.HEADER_TAGS | "j:ten" | "e:five,b:six" | "e:five,j:ten,b:six" | ConfigOrigin.CALCULATED + // ConfigProvider.getMergedMap, but only one source + TracerConfig.TRACE_PEER_SERVICE_MAPPING | "service1:best_service,userService:my_service" | null | "service1:best_service,userService:my_service" | ConfigOrigin.ENV + } + + def "default not-null config settings are collected"() { + expect: + def defaultConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT) + def setting = defaultConfigByKey.get(configKey) + setting.origin == ConfigOrigin.DEFAULT + setting.stringValue() == defaultValue + + where: + configKey | defaultValue + IastConfig.IAST_TELEMETRY_VERBOSITY | Verbosity.INFORMATION.toString() + TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA | "v" + SpanNaming.SCHEMA_MIN_VERSION + GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL | new Float(DEFAULT_TELEMETRY_HEARTBEAT_INTERVAL).toString() + CiVisibilityConfig.CIVISIBILITY_GRADLE_SOURCE_SETS | "main,test" + IastConfig.IAST_WEAK_HASH_ALGORITHMS | DEFAULT_IAST_WEAK_HASH_ALGORITHMS.join(",") + TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES | "400-500" + } + + def "default null config settings are also collected"() { + when: + def defaultConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT) + ConfigSetting cs = defaultConfigByKey.get(configKey) + + then: + cs.key == configKey + cs.stringValue() == null + cs.origin == ConfigOrigin.DEFAULT + + where: + configKey << [ + GeneralConfig.APPLICATION_KEY, + TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES, + JmxFetchConfig.JMX_FETCH_CHECK_PERIOD, + CiVisibilityConfig.CIVISIBILITY_DEBUG_PORT, + TracerConfig.TRACE_SAMPLE_RATE, + TraceInstrumentationConfig.JMS_PROPAGATION_DISABLED_TOPICS, + TracerConfig.PROXY_NO_PROXY, + ] + } + + def "default empty maps and list config settings are collected as empty strings"() { + when: + def defaultConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT) + ConfigSetting cs = defaultConfigByKey.get(configKey) + + then: + cs.key == configKey + cs.stringValue() == "" + cs.origin == ConfigOrigin.DEFAULT + + where: + configKey << [ + TracerConfig.TRACE_PEER_SERVICE_MAPPING, + TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING, + TracerConfig.HEADER_TAGS, + ] + } + + def "put-get configurations"() { + setup: + ConfigCollector.get().collect() + + when: + ConfigCollector.get().put('key1', 'value1', ConfigOrigin.DEFAULT, ABSENT_SEQ_ID) + ConfigCollector.get().put('key2', 'value2', ConfigOrigin.ENV, ABSENT_SEQ_ID) + ConfigCollector.get().put('key1', 'value4', ConfigOrigin.REMOTE, ABSENT_SEQ_ID) + ConfigCollector.get().put('key3', 'value3', ConfigOrigin.JVM_PROP, ABSENT_SEQ_ID) + + then: + def collected = ConfigCollector.get().collect() + collected.get(ConfigOrigin.REMOTE).get('key1') == ConfigSetting.of('key1', 'value4', ConfigOrigin.REMOTE) + collected.get(ConfigOrigin.ENV).get('key2') == ConfigSetting.of('key2', 'value2', ConfigOrigin.ENV) + collected.get(ConfigOrigin.JVM_PROP).get('key3') == ConfigSetting.of('key3', 'value3', ConfigOrigin.JVM_PROP) + collected.get(ConfigOrigin.DEFAULT).get('key1') == ConfigSetting.of('key1', 'value1', ConfigOrigin.DEFAULT) + } + + + def "hide pii configuration data"() { + setup: + ConfigCollector.get().collect() + + when: + ConfigCollector.get().put('api-key', 'sensitive data', ConfigOrigin.ENV, ABSENT_SEQ_ID) + + then: + def collected = ConfigCollector.get().collect() + collected.get(ConfigOrigin.ENV).get('api-key').stringValue() == '' + } + + def "collects common setting default values"() { + when: + def defaultConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT) + + then: + def setting = defaultConfigByKey.get(key) + + setting.key == key + setting.stringValue() == value + setting.origin == ConfigOrigin.DEFAULT + + where: + key | value + "trace.enabled" | "true" + "profiling.enabled" | "false" + "appsec.enabled" | "inactive" + "data.streams.enabled" | "false" + "trace.tags" | "" + "trace.header.tags" | "" + "logs.injection.enabled" | "true" + // defaults to null meaning sample everything but not exactly the same as when explicitly set to 1.0 + "trace.sample.rate" | null + } + + def "collects common setting overridden values"() { + setup: + injectEnvConfig("DD_TRACE_ENABLED", "false") + injectEnvConfig("DD_PROFILING_ENABLED", "true") + injectEnvConfig("DD_APPSEC_ENABLED", "false") + injectEnvConfig("DD_DATA_STREAMS_ENABLED", "true") + injectEnvConfig("DD_TAGS", "team:apm,component:web") + injectEnvConfig("DD_TRACE_HEADER_TAGS", "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2") + injectEnvConfig("DD_LOGS_INJECTION", "false") + injectEnvConfig("DD_TRACE_SAMPLE_RATE", "0.3") + + when: + def envConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.ENV) + + then: + def setting = envConfigByKey.get(key) + + setting.key == key + setting.stringValue() == value + setting.origin == ConfigOrigin.ENV + + where: + key | value + "trace.enabled" | "false" + "profiling.enabled" | "true" + "appsec.enabled" | "false" + "data.streams.enabled" | "true" + // doesn't preserve ordering for some maps + "trace.tags" | "component:web,team:apm" + // lowercase keys for some maps merged from different sources + "trace.header.tags" | "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2".toLowerCase() + "logs.injection.enabled" | "false" + "trace.sample.rate" | "0.3" + } + + def "config collector creates ConfigSettings with correct seqId"() { + setup: + ConfigCollector.get().collect() // clear previous state + + when: + // Simulate sources with increasing precedence and a default + ConfigCollector.get().put("test.key", "default", ConfigOrigin.DEFAULT, ConfigSetting.DEFAULT_SEQ_ID) + ConfigCollector.get().put("test.key", "env", ConfigOrigin.ENV, 2) + ConfigCollector.get().put("test.key", "jvm", ConfigOrigin.JVM_PROP, 3) + ConfigCollector.get().put("test.key", "remote", ConfigOrigin.REMOTE, 4) + + then: + def collected = ConfigCollector.get().collect() + def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.key") + def envSetting = collected.get(ConfigOrigin.ENV).get("test.key") + def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.key") + def remoteSetting = collected.get(ConfigOrigin.REMOTE).get("test.key") + + defaultSetting.seqId == ConfigSetting.DEFAULT_SEQ_ID + // Higher precedence = higher seqId + defaultSetting.seqId < envSetting.seqId + envSetting.seqId < jvmSetting.seqId + jvmSetting.seqId < remoteSetting.seqId + } + + def "config id is null for non-StableConfigSource"() { + setup: + def strictness = ConfigHelper.get().configInversionStrictFlag() + ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.TEST) + + def key = "test.key" + def value = "test-value" + injectSysConfig(key, value) + + when: + // Trigger config collection by getting a value + ConfigProvider.getInstance().getString(key) + def settings = ConfigCollector.get().collect() + + then: + // Verify the config was collected but without a config ID + def setting = settings.get(ConfigOrigin.JVM_PROP).get(key) + setting != null + setting.configId == null + setting.value == value + setting.origin == ConfigOrigin.JVM_PROP + + cleanup: + ConfigHelper.get().setConfigInversionStrict(strictness) + } + + def "default sources cannot be overridden"() { + setup: + def key = "test.key" + def value = "test-value" + def overrideVal = "override-value" + def defaultConfigByKey + ConfigSetting cs + + when: + // Need to make 2 calls in a row because collect() will empty the map + ConfigCollector.get().putDefault(key, value) + ConfigCollector.get().putDefault(key, overrideVal) + defaultConfigByKey = ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT) + cs = defaultConfigByKey.get(key) + + then: + cs.key == key + cs.stringValue() == value + cs.origin == ConfigOrigin.DEFAULT + cs.seqId == ConfigSetting.DEFAULT_SEQ_ID + } +} diff --git a/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java b/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java deleted file mode 100644 index 11514199833..00000000000 --- a/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.java +++ /dev/null @@ -1,377 +0,0 @@ -package datadog.trace.api; - -import static datadog.trace.api.ConfigDefaults.DEFAULT_IAST_WEAK_HASH_ALGORITHMS; -import static datadog.trace.api.ConfigDefaults.DEFAULT_TELEMETRY_HEARTBEAT_INTERVAL; -import static datadog.trace.api.ConfigSetting.ABSENT_SEQ_ID; -import static datadog.trace.junit.utils.config.WithConfigExtension.injectEnvConfig; -import static datadog.trace.junit.utils.config.WithConfigExtension.injectSysConfig; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -import datadog.trace.api.config.AppSecConfig; -import datadog.trace.api.config.CiVisibilityConfig; -import datadog.trace.api.config.GeneralConfig; -import datadog.trace.api.config.IastConfig; -import datadog.trace.api.config.JmxFetchConfig; -import datadog.trace.api.config.ProfilingConfig; -import datadog.trace.api.config.TraceInstrumentationConfig; -import datadog.trace.api.config.TracerConfig; -import datadog.trace.api.iast.telemetry.Verbosity; -import datadog.trace.api.naming.SpanNaming; -import datadog.trace.bootstrap.config.provider.ConfigProvider; -import datadog.trace.config.inversion.ConfigHelper; -import datadog.trace.test.util.DDJavaSpecification; -import datadog.trace.util.ConfigStrings; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Map; -import java.util.stream.Stream; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.tabletest.junit.TableTest; - -public class ConfigCollectorTest extends DDJavaSpecification { - - static Stream nonDefaultConfigSettingsGetCollectedArguments() { - // expectedValue equals configValue for every setting except those redacted from configuration - // telemetry (e.g. the application key), where the collected value is rendered as "". - return Stream.of( - // ConfigProvider.getEnum - arguments(IastConfig.IAST_TELEMETRY_VERBOSITY, Verbosity.DEBUG.toString(), null), - // ConfigProvider.getString - arguments(TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA, "v1", null), - // ConfigProvider.getStringNotEmpty - arguments( - AppSecConfig.APPSEC_AUTOMATED_USER_EVENTS_TRACKING, - UserEventTrackingMode.EXTENDED.toString(), - null), - // ConfigProvider.getStringExcludingSource - arguments(DDTags.SERVICE, "my-service", null), - // ConfigProvider.getStringExcludingSource, redacted from configuration telemetry - arguments(GeneralConfig.APPLICATION_KEY, "app-key", ""), - arguments(GeneralConfig.API_KEY, "some-api-key", ""), - // ConfigProvider.getString, redacted from configuration telemetry - arguments(ProfilingConfig.PROFILING_PROXY_PASSWORD, "some-proxy-password", ""), - // ConfigProvider.getBoolean - arguments(TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES, "true", null), - // ConfigProvider.getInteger - arguments(JmxFetchConfig.JMX_FETCH_CHECK_PERIOD, "60", null), - // ConfigProvider.getLong - arguments(CiVisibilityConfig.CIVISIBILITY_GIT_COMMAND_TIMEOUT_MILLIS, "450273", null), - // ConfigProvider.getFloat - arguments(GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL, "1.5", null), - // ConfigProvider.getDouble - arguments(TracerConfig.TRACE_SAMPLE_RATE, "2.2", null), - // ConfigProvider.getList - arguments( - TraceInstrumentationConfig.JMS_PROPAGATION_DISABLED_TOPICS, - "someTopic,otherTopic", - null), - // ConfigProvider.getSet - arguments(IastConfig.IAST_WEAK_HASH_ALGORITHMS, "SHA1,SHA-1", null), - // ConfigProvider.getSpacedList - arguments(TracerConfig.PROXY_NO_PROXY, "a b c", null), - // ConfigProvider.getMergedMap - arguments( - TracerConfig.TRACE_PEER_SERVICE_MAPPING, - "service1:best_service,userService:my_service", - null), - // ConfigProvider.getOrderedMap - arguments(TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING, "/asdf/*:/test", null), - // ConfigProvider.getMergedMapWithOptionalMappings - arguments(TracerConfig.HEADER_TAGS, "e:five", null), - // ConfigProvider.getIntegerRange - arguments(TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES, "400-402", null)); - } - - @ParameterizedTest - @MethodSource("nonDefaultConfigSettingsGetCollectedArguments") - void nonDefaultConfigSettingsGetCollected( - String configKey, String configValue, String expectedOverride) { - // expectedValue equals configValue unless an explicit override is provided (used for redacted - // settings rendered as ""). - String expectedValue = expectedOverride != null ? expectedOverride : configValue; - injectEnvConfig(ConfigStrings.toEnvVar(configKey), configValue); - - Map envConfigByKey = - ConfigCollector.get().collect().get(ConfigOrigin.ENV); - ConfigSetting config = envConfigByKey.get(configKey); - assertEquals(expectedValue, config.stringValue()); - assertEquals(ConfigOrigin.ENV, config.origin); - } - - static Stream shouldCollectMergedDataFromMultipleSourcesArguments() { - return Stream.of( - // ConfigProvider.getMergedMap - arguments( - TracerConfig.TRACE_PEER_SERVICE_MAPPING, - "service1:best_service,userService:my_service", - "service2:backup_service"), - // ConfigProvider.getOrderedMap - arguments( - TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING, - "/asdf/*:/test,/b:some", - "/a:prop"), - // ConfigProvider.getMergedMapWithOptionalMappings - arguments(TracerConfig.HEADER_TAGS, "j:ten", "e:five,b:six"), - // ConfigProvider.getMergedMap, but only one source - arguments( - TracerConfig.TRACE_PEER_SERVICE_MAPPING, - "service1:best_service,userService:my_service", - null)); - } - - @ParameterizedTest - @MethodSource("shouldCollectMergedDataFromMultipleSourcesArguments") - void shouldCollectMergedDataFromMultipleSources( - String configKey, String envConfigValue, String jvmConfigValue) { - injectEnvConfig(ConfigStrings.toEnvVar(configKey), envConfigValue); - if (jvmConfigValue != null) { - injectSysConfig(configKey, jvmConfigValue); - } - - Map> collected = ConfigCollector.get().collect(); - - Map envSetting = collected.get(ConfigOrigin.ENV); - ConfigSetting envConfig = envSetting.get(configKey); - assertEquals(envConfigValue, envConfig.stringValue()); - assertEquals(ConfigOrigin.ENV, envConfig.origin); - if (jvmConfigValue != null) { - Map jvmSetting = collected.get(ConfigOrigin.JVM_PROP); - ConfigSetting jvmConfig = jvmSetting.get(configKey); - assertEquals( - new HashSet<>(Arrays.asList(jvmConfigValue.split(","))), - new HashSet<>(Arrays.asList(jvmConfig.stringValue().split(",")))); - assertEquals(ConfigOrigin.JVM_PROP, jvmConfig.origin); - } - - // TODO: Add a check for which setting the collector recognizes as highest precedence - } - - static Stream defaultNotNullConfigSettingsAreCollectedArguments() { - return Stream.of( - arguments(IastConfig.IAST_TELEMETRY_VERBOSITY, Verbosity.INFORMATION.toString()), - arguments(TracerConfig.TRACE_SPAN_ATTRIBUTE_SCHEMA, "v" + SpanNaming.SCHEMA_MIN_VERSION), - arguments( - GeneralConfig.TELEMETRY_HEARTBEAT_INTERVAL, - Float.toString(DEFAULT_TELEMETRY_HEARTBEAT_INTERVAL)), - arguments(CiVisibilityConfig.CIVISIBILITY_GRADLE_SOURCE_SETS, "main,test"), - arguments( - IastConfig.IAST_WEAK_HASH_ALGORITHMS, - String.join(",", DEFAULT_IAST_WEAK_HASH_ALGORITHMS)), - arguments(TracerConfig.TRACE_HTTP_CLIENT_ERROR_STATUSES, "400-500")); - } - - @ParameterizedTest - @MethodSource("defaultNotNullConfigSettingsAreCollectedArguments") - void defaultNotNullConfigSettingsAreCollected(String configKey, String defaultValue) { - Map defaultConfigByKey = - ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT); - ConfigSetting setting = defaultConfigByKey.get(configKey); - assertEquals(ConfigOrigin.DEFAULT, setting.origin); - assertEquals(defaultValue, setting.stringValue()); - } - - static Stream defaultNullConfigSettingsAreAlsoCollectedArguments() { - // GeneralConfig.APPLICATION_KEY is redacted from configuration telemetry, so its collected - // value is rendered as "" rather than null; that redaction is verified in the - // nonDefaultConfigSettingsGetCollected test above. - return Stream.of( - arguments(TraceInstrumentationConfig.RESOLVER_USE_URL_CACHES), - arguments(JmxFetchConfig.JMX_FETCH_CHECK_PERIOD), - arguments(CiVisibilityConfig.CIVISIBILITY_DEBUG_PORT), - arguments(TracerConfig.TRACE_SAMPLE_RATE), - arguments(TraceInstrumentationConfig.JMS_PROPAGATION_DISABLED_TOPICS), - arguments(TracerConfig.PROXY_NO_PROXY)); - } - - @ParameterizedTest - @MethodSource("defaultNullConfigSettingsAreAlsoCollectedArguments") - void defaultNullConfigSettingsAreAlsoCollected(String configKey) { - Map defaultConfigByKey = - ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT); - ConfigSetting cs = defaultConfigByKey.get(configKey); - - assertEquals(configKey, cs.key); - assertNull(cs.stringValue()); - assertEquals(ConfigOrigin.DEFAULT, cs.origin); - } - - static Stream defaultEmptyMapsAndListConfigSettingsArguments() { - return Stream.of( - arguments(TracerConfig.TRACE_PEER_SERVICE_MAPPING), - arguments(TracerConfig.TRACE_HTTP_SERVER_PATH_RESOURCE_NAME_MAPPING), - arguments(TracerConfig.HEADER_TAGS)); - } - - @ParameterizedTest - @MethodSource("defaultEmptyMapsAndListConfigSettingsArguments") - void defaultEmptyMapsAndListConfigSettingsAreCollectedAsEmptyStrings(String configKey) { - Map defaultConfigByKey = - ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT); - ConfigSetting cs = defaultConfigByKey.get(configKey); - - assertEquals(configKey, cs.key); - assertEquals("", cs.stringValue()); - assertEquals(ConfigOrigin.DEFAULT, cs.origin); - } - - @Test - void putGetConfigurations() { - ConfigCollector.get().collect(); - - ConfigCollector.get().put("key1", "value1", ConfigOrigin.DEFAULT, ABSENT_SEQ_ID); - ConfigCollector.get().put("key2", "value2", ConfigOrigin.ENV, ABSENT_SEQ_ID); - ConfigCollector.get().put("key1", "value4", ConfigOrigin.REMOTE, ABSENT_SEQ_ID); - ConfigCollector.get().put("key3", "value3", ConfigOrigin.JVM_PROP, ABSENT_SEQ_ID); - - Map> collected = ConfigCollector.get().collect(); - assertEquals( - ConfigSetting.of("key1", "value4", ConfigOrigin.REMOTE), - collected.get(ConfigOrigin.REMOTE).get("key1")); - assertEquals( - ConfigSetting.of("key2", "value2", ConfigOrigin.ENV), - collected.get(ConfigOrigin.ENV).get("key2")); - assertEquals( - ConfigSetting.of("key3", "value3", ConfigOrigin.JVM_PROP), - collected.get(ConfigOrigin.JVM_PROP).get("key3")); - assertEquals( - ConfigSetting.of("key1", "value1", ConfigOrigin.DEFAULT), - collected.get(ConfigOrigin.DEFAULT).get("key1")); - } - - @Test - void redactsSensitiveConfigurationValues() { - ConfigCollector.get().collect(); - - ConfigCollector.get().put("api-key", "somevalue", ConfigOrigin.ENV, ABSENT_SEQ_ID); - - Map> collected = ConfigCollector.get().collect(); - assertEquals("", collected.get(ConfigOrigin.ENV).get("api-key").stringValue()); - } - - @TableTest({ - "scenario | key | value ", - "trace enabled | trace.enabled | true ", - "profiling enabled | profiling.enabled | false ", - "appsec enabled | appsec.enabled | inactive", - "data streams | data.streams.enabled | false ", - "trace tags | trace.tags | '' ", - "trace header tags | trace.header.tags | '' ", - "logs injection | logs.injection.enabled | true ", - "trace sample rate | trace.sample.rate | " - }) - void collectsCommonSettingDefaultValues(String key, String value) { - Map defaultConfigByKey = - ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT); - - ConfigSetting setting = defaultConfigByKey.get(key); - assertEquals(key, setting.key); - assertEquals(value, setting.stringValue()); - assertEquals(ConfigOrigin.DEFAULT, setting.origin); - } - - @TableTest({ - "scenario | key | value ", - "trace enabled | trace.enabled | false ", - "profiling enabled | profiling.enabled | true ", - "appsec enabled | appsec.enabled | false ", - "data streams | data.streams.enabled | true ", - "trace tags | trace.tags | component:web,team:apm ", - "trace header tags | trace.header.tags | x-header-tag-1:header_tag_1,x-header-tag-2:header_tag_2", - "logs injection | logs.injection.enabled | false ", - "trace sample rate | trace.sample.rate | 0.3 " - }) - void collectsCommonSettingOverriddenValues(String key, String value) { - injectEnvConfig("DD_TRACE_ENABLED", "false"); - injectEnvConfig("DD_PROFILING_ENABLED", "true"); - injectEnvConfig("DD_APPSEC_ENABLED", "false"); - injectEnvConfig("DD_DATA_STREAMS_ENABLED", "true"); - injectEnvConfig("DD_TAGS", "team:apm,component:web"); - injectEnvConfig( - "DD_TRACE_HEADER_TAGS", "X-Header-Tag-1:header_tag_1,X-Header-Tag-2:header_tag_2"); - injectEnvConfig("DD_LOGS_INJECTION", "false"); - injectEnvConfig("DD_TRACE_SAMPLE_RATE", "0.3"); - - Map envConfigByKey = - ConfigCollector.get().collect().get(ConfigOrigin.ENV); - - ConfigSetting setting = envConfigByKey.get(key); - assertEquals(key, setting.key); - assertEquals(value, setting.stringValue()); - assertEquals(ConfigOrigin.ENV, setting.origin); - } - - @Test - void configCollectorCreatesConfigSettingsWithCorrectSeqId() { - ConfigCollector.get().collect(); // clear previous state - - // Simulate sources with increasing precedence and a default - ConfigCollector.get() - .put("test.key", "default", ConfigOrigin.DEFAULT, ConfigSetting.DEFAULT_SEQ_ID); - ConfigCollector.get().put("test.key", "env", ConfigOrigin.ENV, 2); - ConfigCollector.get().put("test.key", "jvm", ConfigOrigin.JVM_PROP, 3); - ConfigCollector.get().put("test.key", "remote", ConfigOrigin.REMOTE, 4); - - Map> collected = ConfigCollector.get().collect(); - ConfigSetting defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.key"); - ConfigSetting envSetting = collected.get(ConfigOrigin.ENV).get("test.key"); - ConfigSetting jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.key"); - ConfigSetting remoteSetting = collected.get(ConfigOrigin.REMOTE).get("test.key"); - - assertEquals(ConfigSetting.DEFAULT_SEQ_ID, defaultSetting.seqId); - // Higher precedence = higher seqId - assertTrue(defaultSetting.seqId < envSetting.seqId); - assertTrue(envSetting.seqId < jvmSetting.seqId); - assertTrue(jvmSetting.seqId < remoteSetting.seqId); - } - - @Test - void configIdIsNullForNonStableConfigSource() { - ConfigHelper.StrictnessPolicy strictness = ConfigHelper.get().configInversionStrictFlag(); - ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.TEST); - - String key = "test.key"; - String value = "test-value"; - injectSysConfig(key, value); - - try { - // Trigger config collection by getting a value - ConfigProvider.getInstance().getString(key); - Map> settings = ConfigCollector.get().collect(); - - // Verify the config was collected but without a config ID - ConfigSetting setting = settings.get(ConfigOrigin.JVM_PROP).get(key); - assertNotNull(setting); - assertNull(setting.configId); - assertEquals(value, setting.value); - assertEquals(ConfigOrigin.JVM_PROP, setting.origin); - } finally { - ConfigHelper.get().setConfigInversionStrict(strictness); - } - } - - @Test - void defaultSourcesCannotBeOverridden() { - String key = "test.key"; - String value = "test-value"; - String overrideVal = "override-value"; - - // Need to make 2 calls in a row because collect() will empty the map - ConfigCollector.get().putDefault(key, value); - ConfigCollector.get().putDefault(key, overrideVal); - Map defaultConfigByKey = - ConfigCollector.get().collect().get(ConfigOrigin.DEFAULT); - ConfigSetting cs = defaultConfigByKey.get(key); - - assertEquals(key, cs.key); - assertEquals(value, cs.stringValue()); - assertEquals(ConfigOrigin.DEFAULT, cs.origin); - assertEquals(ConfigSetting.DEFAULT_SEQ_ID, cs.seqId); - } -} diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 18109ab2b43..d2b3a3c5104 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2612,8 +2612,7 @@ "version": "A", "type": "string", "default": null, - "aliases": [], - "sensitive": true + "aliases": [] } ], "DD_PROFILING_APIKEY_FILE": [ @@ -2629,8 +2628,7 @@ "version": "A", "type": "string", "default": null, - "aliases": [], - "sensitive": true + "aliases": [] } ], "DD_PROFILING_API_KEY_FILE": [ diff --git a/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java b/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java index 3cdeadcaff1..f0499f0408c 100644 --- a/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java +++ b/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java @@ -37,8 +37,6 @@ public final class ConfigSetting { "otlp.logs.headers", "otlp.metrics.headers", "otlp.traces.headers", - "profiling.api-key", - "profiling.apikey", "profiling.proxy.password", "rum.client.token")); @@ -62,7 +60,9 @@ public static ConfigSetting of( private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId, String configId) { this.key = key; - this.value = CONFIG_FILTER_LIST.contains(key) ? "" : value; + // Only redact when a value is actually set; an unset (null) config stays null so telemetry + // still distinguishes "not configured" from "configured but hidden". + this.value = (value != null && CONFIG_FILTER_LIST.contains(key)) ? "" : value; this.origin = origin; this.seqId = seqId; this.configId = configId; diff --git a/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java b/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java index f70761786b0..8b4588b7377 100644 --- a/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java +++ b/utils/config-utils/src/test/java/datadog/trace/api/ConfigSettingTest.java @@ -45,14 +45,11 @@ void supportsEqualityCheck( // of representative sensitive keys plus a non-sensitive control; the full filter list is kept in // sync with the registry by SensitiveConfigRedactionTest. @TableTest({ - "scenario | key | value | filteredValue", - "api key | api-key | somevalue | ", - "application key | application-key | somevalue | ", - "otlp traces headers | otlp.traces.headers | somevalue | ", - "profiling api key | profiling.api-key | somevalue | ", - "proxy password | crashtracking.proxy.password | somevalue | ", - "rum client token | rum.client.token | somevalue | ", - "non-sensitive key | some.other.key | somevalue | somevalue " + "scenario | key | value | filteredValue", + "api key | api-key | somevalue | ", + "otlp traces headers | otlp.traces.headers | somevalue | ", + "proxy password | profiling.proxy.password | somevalue | ", + "non-sensitive key | some.other.key | somevalue | somevalue " }) void filtersKeyValues(String key, String value, String filteredValue) { assertEquals(filteredValue, ConfigSetting.of(key, value, ConfigOrigin.DEFAULT).stringValue()); From 4cbc945f2c2a265470f5d34052a63c3e84f711cc Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 11 Jun 2026 20:10:52 -0400 Subject: [PATCH 6/6] Apply suggestion from @bm1549 --- .../src/main/java/datadog/trace/api/ConfigSetting.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java b/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java index f0499f0408c..477450ca698 100644 --- a/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java +++ b/utils/config-utils/src/main/java/datadog/trace/api/ConfigSetting.java @@ -60,8 +60,6 @@ public static ConfigSetting of( private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId, String configId) { this.key = key; - // Only redact when a value is actually set; an unset (null) config stays null so telemetry - // still distinguishes "not configured" from "configured but hidden". this.value = (value != null && CONFIG_FILTER_LIST.contains(key)) ? "" : value; this.origin = origin; this.seqId = seqId;