From e8275d4499950d95db550eddd9a4eeeed006c6a8 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 5 Jun 2026 17:22:12 -0400 Subject: [PATCH 01/14] 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 | 15 ++++++ .../datadog/trace/api/ConfigSettingTest.java | 47 +++++++++++++++---- 4 files changed, 95 insertions(+), 36 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 af0caea701f..d1e7cc4a37a 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": [ @@ -206,7 +207,8 @@ "version": "A", "type": "string", "default": null, - "aliases": ["DD_APP_KEY"] + "aliases": ["DD_APP_KEY"], + "sensitive": true } ], "DD_APPLICATION_KEY_FILE": [ @@ -2398,7 +2400,8 @@ "version": "A", "type": "map", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_OTLP_LOGS_PROTOCOL": [ @@ -2534,7 +2537,8 @@ "version": "B", "type": "map", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_OTLP_METRICS_PROTOCOL": [ @@ -2582,7 +2586,8 @@ "version": "B", "type": "map", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_OTLP_TRACES_PROTOCOL": [ @@ -11870,7 +11875,8 @@ "version": "B", "type": "map", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "OTEL_EXPORTER_OTLP_PROTOCOL": [ @@ -11910,7 +11916,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL": [ @@ -11950,7 +11957,8 @@ "version": "B", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": [ @@ -12006,7 +12014,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 35f27491157..497f3e15a9f 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,6 +23,9 @@ 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( @@ -30,6 +33,18 @@ public final class ConfigSetting { "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", "test.agent.session.token", "DD_TEST_AGENT_SESSION_TOKEN")); 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 1d110be1287..aa0a2d6006f 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.test.junit.utils.tabletest.ConfigValueConverter; @@ -44,19 +45,49 @@ 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 | ", - "session token prop | test.agent.session.token | somevalue | ", - "session token env | DD_TEST_AGENT_SESSION_TOKEN | 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 | ", + "session token prop | test.agent.session.token | somevalue | ", + "session token env | DD_TEST_AGENT_SESSION_TOKEN | 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 cdce6689a8cb0e6b2d55699c597cdb7946a3a9a2 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Wed, 10 Jun 2026 16:15:43 -0400 Subject: [PATCH 02/14] 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 b93242f51580302ce95fdfc87d66b51093fde61f Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 11 Jun 2026 17:22:03 -0400 Subject: [PATCH 03/14] 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 | 32 +- .../datadog/trace/api/ConfigSettingTest.java | 62 ++- .../api/SensitiveConfigRedactionTest.java | 119 ++---- 6 files changed, 451 insertions(+), 471 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 d1e7cc4a37a..d27c9cf55f0 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2683,7 +2683,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_PROFILING_APIKEY_FILE": [ @@ -2699,7 +2700,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 497f3e15a9f..6367bc66035 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,28 +25,32 @@ 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", + "DD_TEST_AGENT_SESSION_TOKEN", + "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", - "test.agent.session.token", - "DD_TEST_AGENT_SESSION_TOKEN")); + "otlp.metrics.headers", + "otlp.traces.headers", + "test.agent.session.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 aa0a2d6006f..73b250d1a87 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.test.junit.utils.tabletest.ConfigValueConverter; @@ -45,49 +44,34 @@ 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 | ", - "session token prop | test.agent.session.token | somevalue | ", - "session token env | DD_TEST_AGENT_SESSION_TOKEN | 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 | ", + "session token prop | test.agent.session.token | somevalue | ", + "session token env | DD_TEST_AGENT_SESSION_TOKEN | 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 341e17ab38ab657a79a48053401f471a167c328e Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 11 Jun 2026 19:19:29 -0400 Subject: [PATCH 04/14] 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 | 12 ++++--- .../java/datadog/trace/api/ConfigSetting.java | 28 ++++++--------- .../datadog/trace/api/ConfigSettingTest.java | 36 +++++++------------ .../api/SensitiveConfigRedactionTest.java | 32 ++++++++++------- 6 files changed, 72 insertions(+), 65 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 fade2b4c417..86d76d0009c 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -2383,7 +2383,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 = @@ -2398,7 +2400,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 = @@ -6397,10 +6402,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 d27c9cf55f0..7ae745409f5 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -1024,7 +1024,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_CRASHTRACKING_PROXY_PORT": [ @@ -3349,7 +3350,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_PROFILING_PROXY_PORT": [ @@ -3733,7 +3735,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_RUM_DEFAULT_PRIVACY_LEVEL": [ @@ -4133,7 +4136,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_TEST_FAILED_TEST_REPLAY_ENABLED": [ 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 6367bc66035..f21b909b0b1 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,33 +23,25 @@ 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", "DD_TEST_AGENT_SESSION_TOKEN", - "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", + "profiling.api-key", + "profiling.apikey", + "profiling.proxy.password", + "rum.client.token", "test.agent.session.token")); public static ConfigSetting of(String key, Object value, ConfigOrigin origin) { 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 73b250d1a87..e493d0bd142 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 @@ -43,30 +43,20 @@ 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 | ", - "session token prop | test.agent.session.token | somevalue | ", - "session token env | DD_TEST_AGENT_SESSION_TOKEN | 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 | ", + "session token prop | test.agent.session.token | somevalue | ", + "session token env | DD_TEST_AGENT_SESSION_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 e3e1a23fa85aff04dc84a65176872a173d4a4b4b Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 11 Jun 2026 19:59:07 -0400 Subject: [PATCH 05/14] 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 | 17 +- 6 files changed, 341 insertions(+), 410 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 86d76d0009c..fade2b4c417 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -2383,9 +2383,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 = @@ -2400,10 +2398,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 = @@ -6402,17 +6397,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 7ae745409f5..49036685a57 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2684,8 +2684,7 @@ "version": "A", "type": "string", "default": null, - "aliases": [], - "sensitive": true + "aliases": [] } ], "DD_PROFILING_APIKEY_FILE": [ @@ -2701,8 +2700,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 f21b909b0b1..c15eb3575fb 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 @@ -38,8 +38,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", "test.agent.session.token")); @@ -64,7 +62,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 e493d0bd142..3ea8729b1b9 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 @@ -47,16 +47,13 @@ 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 | ", - "session token prop | test.agent.session.token | somevalue | ", - "session token env | DD_TEST_AGENT_SESSION_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 | ", + "session token prop | test.agent.session.token | somevalue | ", + "session token env | DD_TEST_AGENT_SESSION_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()); From b5ecc94fbff91f0d9cb58f496b5747fc4687846d Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 11 Jun 2026 20:10:52 -0400 Subject: [PATCH 06/14] 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 c15eb3575fb..1e416c96d32 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 @@ -62,8 +62,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; From 4af48ad44258dec4ce335cab3e7cf9b63ff2b739 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 18 Jun 2026 15:05:06 -0400 Subject: [PATCH 07/14] Generate sensitive-config redaction list from registry Replace the hand-maintained CONFIG_FILTER_LIST and its drift-guard test with a SENSITIVE_KEYS set generated from the "sensitive": true entries in metadata/supported-configurations.json, making the registry the single source of truth. ConfigSetting canonicalizes a collected key to env-var form (without double-prefixing an already-DD_-form key) and matches it against the generated set, so redaction fires whether the value arrived as a property name, dd.* system property, alias, or raw env var. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ParseV2SupportedConfigurationsTask.kt | 33 ++++- .../ParseV2SupportedConfigurationsTest.kt | 18 ++- .../java/datadog/trace/api/ConfigSetting.java | 47 +++---- .../datadog/trace/api/ConfigSettingTest.java | 13 +- .../api/SensitiveConfigRedactionTest.java | 129 ------------------ 5 files changed, 76 insertions(+), 164 deletions(-) delete mode 100644 utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt index 0715d43a8cc..2de1ee15ac1 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt @@ -59,7 +59,8 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( configMap["type"] as? String, configMap["default"] as? String, (configMap["aliases"] as? List) ?: emptyList(), - (configMap["propertyKeys"] as? List) ?: emptyList() + (configMap["propertyKeys"] as? List) ?: emptyList(), + configMap["sensitive"] as? Boolean ?: false ) } } @@ -81,6 +82,14 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( } }.toMap() + // Every key of every config flagged "sensitive": true -- the canonical key plus its aliases. + // Values collected under any of these are redacted in configuration telemetry (see + // ConfigSetting), so this is the single source of truth for the redaction list. Aliases are + // included so a value collected under an alias env-var (e.g. DD_APP_KEY) is redacted too. + val sensitiveKeys: Set = supported.flatMap { (canonical, configList) -> + configList.filter { it.sensitive }.flatMap { listOf(canonical) + it.aliases } + }.toSet() + // Build the output .java path from the fully-qualified class name val pkgName = finalClassName.substringBeforeLast('.', "") val pkgPath = pkgName.replace('.', File.separatorChar) @@ -97,7 +106,8 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( aliases, aliasMapping, deprecated, - reversePropertyKeysMap + reversePropertyKeysMap, + sensitiveKeys ) } @@ -109,7 +119,8 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( aliases: Map>, aliasMapping: Map, deprecated: Map, - reversePropertyKeysMap: Map + reversePropertyKeysMap: Map, + sensitiveKeys: Set ) { val outFile = File(outputPath) outFile.parentFile?.mkdirs() @@ -138,12 +149,15 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( out.println() out.println(" public static final Map REVERSE_PROPERTY_KEYS_MAP;") out.println() + out.println(" public static final Set SENSITIVE_KEYS;") + out.println() out.println(" static {") out.println(" SUPPORTED = initSupported();") out.println(" ALIASES = initAliases();") out.println(" ALIAS_MAPPING = initAliasMapping();") out.println(" DEPRECATED = initDeprecated();") out.println(" REVERSE_PROPERTY_KEYS_MAP = initReversePropertyKeysMap();") + out.println(" SENSITIVE_KEYS = initSensitiveKeys();") out.println(" }") out.println() @@ -218,6 +232,16 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( } out.println(" return unmodifiableMap(reversePropertyKeysMapping);") out.println(" }") + out.println() + + // initSensitiveKeys() + out.println(" private static Set initSensitiveKeys() {") + out.println(" Set sensitiveKeys = new HashSet<>();") + for (key in sensitiveKeys.toSortedSet()) { + out.printf(" sensitiveKeys.add(\"%s\");\n", esc(key)) + } + out.println(" return Collections.unmodifiableSet(sensitiveKeys);") + out.println(" }") out.println("}") } } @@ -258,5 +282,6 @@ private data class SupportedConfigurationItem( val type: String?, val default: String?, val aliases: List, - val propertyKeys: List + val propertyKeys: List, + val sensitive: Boolean ) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt index f6583d4cdba..d08ac490f5b 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt @@ -4,6 +4,7 @@ import datadog.gradle.plugin.GradleFixture import org.gradle.testkit.runner.BuildResult import org.gradle.testkit.runner.TaskOutcome import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.io.File @@ -26,6 +27,7 @@ class ParseV2SupportedConfigurationsTest : GradleFixture() { assertTrue(content.contains("public static final Map ALIAS_MAPPING;")) assertTrue(content.contains("public static final Map DEPRECATED;")) assertTrue(content.contains("public static final Map REVERSE_PROPERTY_KEYS_MAP;")) + assertTrue(content.contains("public static final Set SENSITIVE_KEYS;")) assertTrue(content.contains("private static Map> initSupported()")) assertTrue(content.contains("private static void initSupported1(Map> supportedMap)")) @@ -34,6 +36,7 @@ class ParseV2SupportedConfigurationsTest : GradleFixture() { assertTrue(content.contains("private static Map initAliasMapping()")) assertTrue(content.contains("private static Map initDeprecated()")) assertTrue(content.contains("private static Map initReversePropertyKeysMap()")) + assertTrue(content.contains("private static Set initSensitiveKeys()")) assertContainsSupportedConfig( content, @@ -41,7 +44,7 @@ class ParseV2SupportedConfigurationsTest : GradleFixture() { version = "A", type = "string", default = "null", - aliases = emptyList(), + aliases = listOf("DD_LEGACY_ACTION_EXECUTION_ID"), propertyKeys = listOf("property.key") ) @@ -63,7 +66,7 @@ class ParseV2SupportedConfigurationsTest : GradleFixture() { aliases = listOf("DD_ALIAS") ) - assertTrue(content.contains("""aliasesMap.put("DD_ACTION_EXECUTION_ID", emptyList())""")) + assertTrue(content.contains("""aliasesMap.put("DD_ACTION_EXECUTION_ID", singletonList("DD_LEGACY_ACTION_EXECUTION_ID"))""")) assertTrue(content.contains("""aliasesMap.put("DD_AGENTLESS_LOG_SUBMISSION_ENABLED", singletonList("DD_ALIAS"))""")) assertTrue(content.contains("""aliasMappingMap.put("DD_ALIAS", "DD_AGENTLESS_LOG_SUBMISSION_ENABLED")""")) @@ -72,6 +75,12 @@ class ParseV2SupportedConfigurationsTest : GradleFixture() { assertTrue(content.contains("""deprecatedMap.put("legacy.setting", "No longer supported")""")) assertTrue(content.contains("""reversePropertyKeysMapping.put("property.key", "DD_ACTION_EXECUTION_ID")""")) + + // Configs flagged "sensitive": true land in SENSITIVE_KEYS -- both the canonical key and its + // aliases -- while non-sensitive configs do not. + assertTrue(content.contains("""sensitiveKeys.add("DD_ACTION_EXECUTION_ID")""")) + assertTrue(content.contains("""sensitiveKeys.add("DD_LEGACY_ACTION_EXECUTION_ID")""")) + assertFalse(content.contains("""sensitiveKeys.add("DD_AGENTLESS_LOG_SUBMISSION_ENABLED")""")) } private fun runGradleTask(): Pair { @@ -85,8 +94,9 @@ class ParseV2SupportedConfigurationsTest : GradleFixture() { "version": "A", "type": "string", "default": null, - "aliases": [], - "propertyKeys": ["property.key"] + "aliases": ["DD_LEGACY_ACTION_EXECUTION_ID"], + "propertyKeys": ["property.key"], + "sensitive": true } ], "DD_AGENTLESS_LOG_SUBMISSION_ENABLED": [ 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 1e416c96d32..1e8cbc49798 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 @@ -3,12 +3,10 @@ import static datadog.trace.util.ConfigStrings.propertyNameToEnvironmentVariableName; import static datadog.trace.util.ConfigStrings.toEnvVar; -import java.util.Arrays; +import datadog.trace.config.inversion.GeneratedSupportedConfigurations; import java.util.BitSet; -import java.util.HashSet; import java.util.Map; import java.util.Objects; -import java.util.Set; public final class ConfigSetting { public static final int DEFAULT_SEQ_ID = 1; @@ -23,25 +21,6 @@ public final class ConfigSetting { /** The config ID associated with this setting, or {@code null} if not applicable. */ public final String configId; - // 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_TEST_AGENT_SESSION_TOKEN", - "api-key", - "application-key", - "crashtracking.proxy.password", - "otlp.logs.headers", - "otlp.metrics.headers", - "otlp.traces.headers", - "profiling.proxy.password", - "rum.client.token", - "test.agent.session.token")); - public static ConfigSetting of(String key, Object value, ConfigOrigin origin) { return new ConfigSetting(key, value, origin, ABSENT_SEQ_ID, null); } @@ -62,7 +41,17 @@ public static ConfigSetting of( private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId, String configId) { this.key = key; - this.value = (value != null && CONFIG_FILTER_LIST.contains(key)) ? "" : value; + // Redact values of configs flagged "sensitive": true in metadata/supported-configurations.json. + // The flags (canonical keys plus their aliases) are compiled into + // GeneratedSupportedConfigurations.SENSITIVE_KEYS in env-var form by the supported-config + // generator, so the registry is the single source of truth for what gets hidden. The collected + // key is canonicalized to the same env-var form before lookup, regardless of which form it was + // collected under (property name, dd.* system property, alias, or raw env var). + this.value = + (value != null + && GeneratedSupportedConfigurations.SENSITIVE_KEYS.contains(redactionKey(key))) + ? "" + : value; this.origin = origin; this.seqId = seqId; this.configId = configId; @@ -76,6 +65,18 @@ public String normalizedKey() { return propertyNameToEnvironmentVariableName(key); } + // Canonical env-var form used to match a collected key against SENSITIVE_KEYS. Unlike + // normalizedKey(), this never double-prefixes a key already in DD_ env-var form: the api-key + // property name, the dd.api-key system property, and a raw DD_API_KEY env var all canonicalize to + // DD_API_KEY, so redaction matches whichever form the value arrived in. + private static String redactionKey(String key) { + if (key.startsWith("otel.") || key.startsWith("OTEL_")) { + return toEnvVar(key); + } + String env = toEnvVar(key); + return env.startsWith("DD_") ? env : "DD_" + env; + } + public String stringValue() { if (value == null) { return 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 3ea8729b1b9..a4958d1ae2a 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 @@ -43,12 +43,17 @@ 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. + // Sensitive values are redacted regardless of which form the key was collected under -- property + // name, dd.* system property, raw env var, or alias env var all canonicalize to the same key. The + // full set is generated from the "sensitive": true entries in + // metadata/supported-configurations.json. @TableTest({ "scenario | key | value | filteredValue", - "api key | api-key | somevalue | ", + "api key property | api-key | somevalue | ", + "api key system prop | dd.api-key | somevalue | ", + "api key env var | DD_API_KEY | somevalue | ", + "application key | application-key | somevalue | ", + "application alias | DD_APP_KEY | somevalue | ", "otlp traces headers | otlp.traces.headers | somevalue | ", "proxy password | profiling.proxy.password | somevalue | ", "session token prop | test.agent.session.token | somevalue | ", 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 deleted file mode 100644 index 711a620d99d..00000000000 --- a/utils/config-utils/src/test/java/datadog/trace/api/SensitiveConfigRedactionTest.java +++ /dev/null @@ -1,129 +0,0 @@ -package datadog.trace.api; - -import static datadog.trace.util.ConfigStrings.toEnvVar; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; - -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 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 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; - } - - @Test - void sensitiveRegistryEntriesAndFilterListStayInSync() { - Set registryCanonical = - sensitiveRegistryKeys().stream() - .map(SensitiveConfigRedactionTest::canonical) - .collect(toTreeSet()); - Set filterCanonical = - configFilterList().stream() - .map(SensitiveConfigRedactionTest::canonical) - .collect(toTreeSet()); - - assertFalse(registryCanonical.isEmpty(), "expected at least one \"sensitive\": true config"); - assertEquals( - registryCanonical, - filterCanonical, - "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 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(); - 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()) { - for (Object def : (List) entry.getValue()) { - Map definition = (Map) def; - if (Boolean.TRUE.equals(definition.get("sensitive"))) { - sensitive.add(entry.getKey()); - } - } - } - return sensitive; - } - - // Reads CONFIG_FILTER_LIST from 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 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()) { - Path candidate = current.resolve(REGISTRY_RELATIVE_PATH); - if (Files.isRegularFile(candidate)) { - return candidate; - } - } - throw new IllegalStateException("Could not locate " + REGISTRY_RELATIVE_PATH + " from " + dir); - } - - private static java.util.stream.Collector> toTreeSet() { - return Collectors.toCollection(TreeSet::new); - } -} From 3c288b6c208aa387b06b7534463f74c6ad3b91dc Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 18 Jun 2026 15:17:25 -0400 Subject: [PATCH 08/14] Import Set/HashSet in generated config class for SENSITIVE_KEYS The merge with master switched the generated file to explicit imports, but the initSensitiveKeys() emission still used Set/HashSet/Collections.unmodifiableSet without emitting their imports, breaking compilation of the generated GeneratedSupportedConfigurations. Emit the java.util.Set/HashSet imports and use the unmodifiableSet static import to match the rest of the generated file. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plugin/config/ParseV2SupportedConfigurationsTask.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt index 2de1ee15ac1..643cd1368af 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt @@ -129,13 +129,16 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( out.println("package $packageName;") out.println() out.println("import java.util.HashMap;") + out.println("import java.util.HashSet;") out.println("import java.util.List;") out.println("import java.util.Map;") + out.println("import java.util.Set;") out.println("import static java.util.Arrays.asList;") out.println("import static java.util.Collections.emptyList;") out.println("import static java.util.Collections.singletonList;") out.println("import static java.util.Collections.unmodifiableList;") out.println("import static java.util.Collections.unmodifiableMap;") + out.println("import static java.util.Collections.unmodifiableSet;") out.println() out.println("public final class $className {") out.println() @@ -240,7 +243,7 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( for (key in sensitiveKeys.toSortedSet()) { out.printf(" sensitiveKeys.add(\"%s\");\n", esc(key)) } - out.println(" return Collections.unmodifiableSet(sensitiveKeys);") + out.println(" return unmodifiableSet(sensitiveKeys);") out.println(" }") out.println("}") } From 4abb12f01d4c065b68962c8f683aa6b75fbd676a Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 18 Jun 2026 16:13:33 -0400 Subject: [PATCH 09/14] Centralize config key canonicalization in ConfigStrings Move the env-var canonicalization out of ConfigSetting's private redactionKey into a reusable ConfigStrings.toCanonicalEnvVar, so the "config key in any form -> canonical DD_ env-var name" logic lives alongside the other key conversions and is unit-tested directly. Behavior is unchanged; normalizedKey() is untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/datadog/trace/api/ConfigSetting.java | 15 ++------------ .../datadog/trace/util/ConfigStrings.java | 20 +++++++++++++++++++ .../datadog/trace/util/ConfigStringsTest.java | 18 +++++++++++++++++ 3 files changed, 40 insertions(+), 13 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 1e8cbc49798..2e874fdcec2 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 @@ -1,6 +1,7 @@ package datadog.trace.api; import static datadog.trace.util.ConfigStrings.propertyNameToEnvironmentVariableName; +import static datadog.trace.util.ConfigStrings.toCanonicalEnvVar; import static datadog.trace.util.ConfigStrings.toEnvVar; import datadog.trace.config.inversion.GeneratedSupportedConfigurations; @@ -49,7 +50,7 @@ private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId, // collected under (property name, dd.* system property, alias, or raw env var). this.value = (value != null - && GeneratedSupportedConfigurations.SENSITIVE_KEYS.contains(redactionKey(key))) + && GeneratedSupportedConfigurations.SENSITIVE_KEYS.contains(toCanonicalEnvVar(key))) ? "" : value; this.origin = origin; @@ -65,18 +66,6 @@ public String normalizedKey() { return propertyNameToEnvironmentVariableName(key); } - // Canonical env-var form used to match a collected key against SENSITIVE_KEYS. Unlike - // normalizedKey(), this never double-prefixes a key already in DD_ env-var form: the api-key - // property name, the dd.api-key system property, and a raw DD_API_KEY env var all canonicalize to - // DD_API_KEY, so redaction matches whichever form the value arrived in. - private static String redactionKey(String key) { - if (key.startsWith("otel.") || key.startsWith("OTEL_")) { - return toEnvVar(key); - } - String env = toEnvVar(key); - return env.startsWith("DD_") ? env : "DD_" + env; - } - public String stringValue() { if (value == null) { return null; diff --git a/utils/config-utils/src/main/java/datadog/trace/util/ConfigStrings.java b/utils/config-utils/src/main/java/datadog/trace/util/ConfigStrings.java index 2937e19a372..16307cdbea2 100644 --- a/utils/config-utils/src/main/java/datadog/trace/util/ConfigStrings.java +++ b/utils/config-utils/src/main/java/datadog/trace/util/ConfigStrings.java @@ -27,6 +27,26 @@ public static String propertyNameToEnvironmentVariableName(final String setting) return "DD_" + toEnvVar(setting); } + /** + * Converts a config key in any form into its canonical environment variable name, regardless of + * whether it arrived as a property name ({@code api-key}), a {@code dd.} system property ({@code + * dd.api-key}), or a raw environment variable ({@code DD_API_KEY}) -- all canonicalize to {@code + * DD_API_KEY}. Unlike {@link #propertyNameToEnvironmentVariableName(String)}, the {@code DD_} + * namespace is added only when not already present, so a key already in env-var form is not + * double-prefixed. {@code otel.}/{@code OTEL_} keys keep their own namespace. + * + * @param key The config key in any form + * @return The canonical environment variable name + */ + @Nonnull + public static String toCanonicalEnvVar(final String key) { + if (key.startsWith("otel.") || key.startsWith("OTEL_")) { + return toEnvVar(key); + } + final String env = toEnvVar(key); + return env.startsWith("DD_") ? env : "DD_" + env; + } + /** * Converts the property name, e.g. 'service.name' into a public system property name, e.g. * `dd.service.name`. diff --git a/utils/config-utils/src/test/java/datadog/trace/util/ConfigStringsTest.java b/utils/config-utils/src/test/java/datadog/trace/util/ConfigStringsTest.java index c8b5acb8dfd..30f7768909e 100644 --- a/utils/config-utils/src/test/java/datadog/trace/util/ConfigStringsTest.java +++ b/utils/config-utils/src/test/java/datadog/trace/util/ConfigStringsTest.java @@ -5,6 +5,7 @@ import java.util.Locale; import org.junit.jupiter.api.Test; +import org.tabletest.junit.TableTest; class ConfigStringsTest { @@ -30,4 +31,21 @@ void toEnvVarUppercasesLowerIToAsciiIOnTurkishLocale() { Locale.setDefault(previousDefault); } } + + // Every spelling of a config key canonicalizes to the same DD_ env-var name, and an already-DD_ + // key is not double-prefixed. otel.*/OTEL_* keys keep their own namespace. + @TableTest({ + "scenario | key | canonical ", + "property name | api-key | DD_API_KEY ", + "dotted property | profiling.proxy.password | DD_PROFILING_PROXY_PASSWORD", + "dd system property | dd.api-key | DD_API_KEY ", + "raw env var | DD_API_KEY | DD_API_KEY ", + "alias env var | DD_APP_KEY | DD_APP_KEY ", + "otel property | otel.exporter.otlp.headers | OTEL_EXPORTER_OTLP_HEADERS ", + "otel env var | OTEL_EXPORTER_OTLP_HEADERS | OTEL_EXPORTER_OTLP_HEADERS ", + "non-dd env var | AWS_LAMBDA_FUNCTION_NAME | DD_AWS_LAMBDA_FUNCTION_NAME" + }) + void canonicalizesKeysToEnvVarForm(String key, String canonical) { + assertEquals(canonical, ConfigStrings.toCanonicalEnvVar(key)); + } } From 60863f052a4ba989c6ea9a115797141041e35ff1 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 18 Jun 2026 16:32:59 -0400 Subject: [PATCH 10/14] Fix double DD_ prefix in telemetry config key names ConfigSetting.normalizedKey() always prepended DD_, so a key already collected in env-var form (e.g. DD_PROFILING_API_KEY / DD_AAS_JAVA_EXTENSION_VERSION read via getEnv) was reported in config telemetry as the malformed DD_DD_PROFILING_API_KEY. Delegate to ConfigStrings.toCanonicalEnvVar, which adds the DD_ namespace only when not already present, fixing the name and collapsing the two near-identical canonicalization helpers into one. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../main/java/datadog/trace/api/ConfigSetting.java | 8 +------- .../java/datadog/trace/api/ConfigSettingTest.java | 13 +++++++++++++ 2 files changed, 14 insertions(+), 7 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 2e874fdcec2..0446b93ca93 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 @@ -1,8 +1,6 @@ package datadog.trace.api; -import static datadog.trace.util.ConfigStrings.propertyNameToEnvironmentVariableName; import static datadog.trace.util.ConfigStrings.toCanonicalEnvVar; -import static datadog.trace.util.ConfigStrings.toEnvVar; import datadog.trace.config.inversion.GeneratedSupportedConfigurations; import java.util.BitSet; @@ -59,11 +57,7 @@ private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId, } public String normalizedKey() { - // OTel configurations should not be normalized with DD_ - if (key.startsWith("otel.") || key.startsWith("OTEL_")) { - return toEnvVar(key); - } - return propertyNameToEnvironmentVariableName(key); + return toCanonicalEnvVar(key); } public String stringValue() { 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 a4958d1ae2a..8bb609bf9ea 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 @@ -64,6 +64,19 @@ void filtersKeyValues(String key, String value, String filteredValue) { assertEquals(filteredValue, ConfigSetting.of(key, value, ConfigOrigin.DEFAULT).stringValue()); } + // The telemetry-reported key name is the canonical env-var form; a key already in DD_ env-var + // form + // (e.g. collected via getEnv) is not double-prefixed into DD_DD_*. + @TableTest({ + "scenario | key | normalized ", + "property name | api-key | DD_API_KEY ", + "dd env var | DD_PROFILING_API_KEY | DD_PROFILING_API_KEY", + "otel env var | OTEL_SERVICE_NAME | OTEL_SERVICE_NAME " + }) + void normalizesKeyName(String key, String normalized) { + assertEquals(normalized, ConfigSetting.of(key, "v", ConfigOrigin.DEFAULT).normalizedKey()); + } + @TableTest({ "scenario | value | rendered", "null | | ", From 094e55d51ed99c0108167a2adfbd750131b7f1ec Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 18 Jun 2026 16:41:37 -0400 Subject: [PATCH 11/14] Drop superfluous comments around sensitive-config redaction The code and the ConfigStrings.toCanonicalEnvVar javadoc already explain the behavior; remove the restating comments in ConfigSetting, the generator, and the tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plugin/config/ParseV2SupportedConfigurationsTask.kt | 4 ---- .../plugin/config/ParseV2SupportedConfigurationsTest.kt | 2 -- .../src/main/java/datadog/trace/api/ConfigSetting.java | 6 ------ .../src/test/java/datadog/trace/api/ConfigSettingTest.java | 7 ------- .../test/java/datadog/trace/util/ConfigStringsTest.java | 2 -- 5 files changed, 21 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt index 643cd1368af..614b3d7809c 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt @@ -82,10 +82,6 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( } }.toMap() - // Every key of every config flagged "sensitive": true -- the canonical key plus its aliases. - // Values collected under any of these are redacted in configuration telemetry (see - // ConfigSetting), so this is the single source of truth for the redaction list. Aliases are - // included so a value collected under an alias env-var (e.g. DD_APP_KEY) is redacted too. val sensitiveKeys: Set = supported.flatMap { (canonical, configList) -> configList.filter { it.sensitive }.flatMap { listOf(canonical) + it.aliases } }.toSet() diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt index d08ac490f5b..a71de75adbd 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt @@ -76,8 +76,6 @@ class ParseV2SupportedConfigurationsTest : GradleFixture() { assertTrue(content.contains("""reversePropertyKeysMapping.put("property.key", "DD_ACTION_EXECUTION_ID")""")) - // Configs flagged "sensitive": true land in SENSITIVE_KEYS -- both the canonical key and its - // aliases -- while non-sensitive configs do not. assertTrue(content.contains("""sensitiveKeys.add("DD_ACTION_EXECUTION_ID")""")) assertTrue(content.contains("""sensitiveKeys.add("DD_LEGACY_ACTION_EXECUTION_ID")""")) assertFalse(content.contains("""sensitiveKeys.add("DD_AGENTLESS_LOG_SUBMISSION_ENABLED")""")) 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 0446b93ca93..26178407f93 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 @@ -40,12 +40,6 @@ public static ConfigSetting of( private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId, String configId) { this.key = key; - // Redact values of configs flagged "sensitive": true in metadata/supported-configurations.json. - // The flags (canonical keys plus their aliases) are compiled into - // GeneratedSupportedConfigurations.SENSITIVE_KEYS in env-var form by the supported-config - // generator, so the registry is the single source of truth for what gets hidden. The collected - // key is canonicalized to the same env-var form before lookup, regardless of which form it was - // collected under (property name, dd.* system property, alias, or raw env var). this.value = (value != null && GeneratedSupportedConfigurations.SENSITIVE_KEYS.contains(toCanonicalEnvVar(key))) 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 8bb609bf9ea..ad2f23c491a 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 @@ -43,10 +43,6 @@ void supportsEqualityCheck( } } - // Sensitive values are redacted regardless of which form the key was collected under -- property - // name, dd.* system property, raw env var, or alias env var all canonicalize to the same key. The - // full set is generated from the "sensitive": true entries in - // metadata/supported-configurations.json. @TableTest({ "scenario | key | value | filteredValue", "api key property | api-key | somevalue | ", @@ -64,9 +60,6 @@ void filtersKeyValues(String key, String value, String filteredValue) { assertEquals(filteredValue, ConfigSetting.of(key, value, ConfigOrigin.DEFAULT).stringValue()); } - // The telemetry-reported key name is the canonical env-var form; a key already in DD_ env-var - // form - // (e.g. collected via getEnv) is not double-prefixed into DD_DD_*. @TableTest({ "scenario | key | normalized ", "property name | api-key | DD_API_KEY ", diff --git a/utils/config-utils/src/test/java/datadog/trace/util/ConfigStringsTest.java b/utils/config-utils/src/test/java/datadog/trace/util/ConfigStringsTest.java index 30f7768909e..d87c5581426 100644 --- a/utils/config-utils/src/test/java/datadog/trace/util/ConfigStringsTest.java +++ b/utils/config-utils/src/test/java/datadog/trace/util/ConfigStringsTest.java @@ -32,8 +32,6 @@ void toEnvVarUppercasesLowerIToAsciiIOnTurkishLocale() { } } - // Every spelling of a config key canonicalizes to the same DD_ env-var name, and an already-DD_ - // key is not double-prefixed. otel.*/OTEL_* keys keep their own namespace. @TableTest({ "scenario | key | canonical ", "property name | api-key | DD_API_KEY ", From 75f39488b43d76eeda88040a5879dc834850962a Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Thu, 18 Jun 2026 16:55:06 -0400 Subject: [PATCH 12/14] Document sensitive config value redaction Add comment to clarify sensitive config handling --- .../src/main/java/datadog/trace/api/ConfigSetting.java | 1 + 1 file changed, 1 insertion(+) 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 26178407f93..76f9bfcd5bd 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 @@ -38,6 +38,7 @@ public static ConfigSetting of( return new ConfigSetting(key, value, origin, seqId, configId); } + // Redact values of configs flagged "sensitive": true in metadata/supported-configurations.json. private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId, String configId) { this.key = key; this.value = From bbb3d5adf0d8e25e735b76c5c2dbb5b860d4b80e Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 28 Aug 2026 10:29:03 -0400 Subject: [PATCH 13/14] Store canonical configuration keys in ConfigSetting Normalize keys once during construction and use the stored value for redaction and telemetry serialization.\n\nMark the deprecated profiling API key fallbacks sensitive, add coverage for their property and environment forms, and document the sensitive metadata field. --- .../ParseV2SupportedConfigurationsTask.kt | 1 - docs/add_new_configurations.md | 8 +++-- .../trace/api/ConfigCollectorTest.groovy | 12 ++++---- .../config/provider/ConfigProviderTest.groovy | 13 -------- metadata/supported-configurations.json | 6 ++-- .../telemetry/TelemetryRequestBody.java | 2 +- .../telemetry/TestTelemetryRouter.groovy | 2 +- .../java/datadog/trace/api/ConfigSetting.java | 17 +++-------- .../datadog/trace/util/ConfigStrings.java | 7 +---- .../datadog/trace/api/ConfigSettingTest.java | 30 +++++++++++-------- 10 files changed, 38 insertions(+), 60 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt index 614b3d7809c..8a3d6fa10df 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt @@ -233,7 +233,6 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( out.println(" }") out.println() - // initSensitiveKeys() out.println(" private static Set initSensitiveKeys() {") out.println(" Set sensitiveKeys = new HashSet<>();") for (key in sensitiveKeys.toSortedSet()) { diff --git a/docs/add_new_configurations.md b/docs/add_new_configurations.md index 2c8eb1c785c..258438a1b16 100644 --- a/docs/add_new_configurations.md +++ b/docs/add_new_configurations.md @@ -41,17 +41,19 @@ In order to properly add a new configuration in the library, follow the below st 3. Default. This is a _mandatory_ field and accepts null as a valid value. 4. Aliases. This is a _mandatory_ field. If there are no aliases for the configuration, use an empty array as the value. 5. PropertyKeys. This is an _optional_ field that should only be used if there are additional telemetry keys being sent from the tracer (that are not the environment variable itself). + 6. Sensitive. This is an _optional_ boolean field that defaults to `false`. Set it to `true` for credentials or other secret values that must be redacted from configuration telemetry. Aliases inherit the setting. See below for an example of the `supported-configurations.json` file. ``` { "supportedConfigurations": { - "DD_SERVICE": [ + "DD_APPLICATION_KEY": [ { - "version": "D", // Mandatory, generated by Feature Parity Dashboard + "version": "A", // Mandatory, generated by Feature Parity Dashboard "type": "string", // Mandatory, choose from boolean, int, decimal, string, map, array "default": null, // Mandatory, allows null values - "aliases": ["DD_SERVICE_NAME"] // Mandatory, allows empty array if no aliases exist + "aliases": ["DD_APP_KEY"], // Mandatory, allows empty array if no aliases exist + "sensitive": true // Optional, defaults to false and applies to aliases } ], "DD_ENV_WITH_TELEMETRY_KEYS": [ 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 bbb88b6ef96..2da76c85cdc 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy @@ -126,7 +126,6 @@ class ConfigCollectorTest extends DDSpecification { ConfigSetting cs = defaultConfigByKey.get(configKey) then: - cs.key == configKey cs.stringValue() == null cs.origin == ConfigOrigin.DEFAULT @@ -148,7 +147,6 @@ class ConfigCollectorTest extends DDSpecification { ConfigSetting cs = defaultConfigByKey.get(configKey) then: - cs.key == configKey cs.stringValue() == "" cs.origin == ConfigOrigin.DEFAULT @@ -184,11 +182,14 @@ class ConfigCollectorTest extends DDSpecification { ConfigCollector.get().collect() when: - ConfigCollector.get().put('api-key', 'sensitive data', ConfigOrigin.ENV, ABSENT_SEQ_ID) + ConfigCollector.get().put(configKey, 'sensitive data', ConfigOrigin.ENV, ABSENT_SEQ_ID) then: def collected = ConfigCollector.get().collect() - collected.get(ConfigOrigin.ENV).get('api-key').stringValue() == '' + collected.get(ConfigOrigin.ENV).get(configKey).stringValue() == '' + + where: + configKey << ['api-key', 'DD_PROFILING_API_KEY', 'DD_PROFILING_APIKEY'] } def "collects common setting default values"() { @@ -198,7 +199,6 @@ class ConfigCollectorTest extends DDSpecification { then: def setting = defaultConfigByKey.get(key) - setting.key == key setting.stringValue() == value setting.origin == ConfigOrigin.DEFAULT @@ -232,7 +232,6 @@ class ConfigCollectorTest extends DDSpecification { then: def setting = envConfigByKey.get(key) - setting.key == key setting.stringValue() == value setting.origin == ConfigOrigin.ENV @@ -317,7 +316,6 @@ class ConfigCollectorTest extends DDSpecification { 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/groovy/datadog/trace/bootstrap/config/provider/ConfigProviderTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/ConfigProviderTest.groovy index 0217d6b4da7..eced33f3049 100644 --- a/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/ConfigProviderTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/ConfigProviderTest.groovy @@ -78,18 +78,15 @@ class ConfigProviderTest extends DDSpecification { then: // Check the default def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.key") - defaultSetting.key == "test.key" defaultSetting.stringValue() == "defaultValue" defaultSetting.origin == ConfigOrigin.DEFAULT defaultSetting.seqId == ConfigSetting.DEFAULT_SEQ_ID def envSetting = collected.get(ConfigOrigin.ENV).get("test.key") - envSetting.key == "test.key" envSetting.stringValue() == "envValue" envSetting.origin == ConfigOrigin.ENV def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.key") - jvmSetting.key == "test.key" jvmSetting.stringValue() == "jvmValue" jvmSetting.origin == ConfigOrigin.JVM_PROP @@ -117,20 +114,17 @@ class ConfigProviderTest extends DDSpecification { then: // Default def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get(configKey) - defaultSetting.key == configKey defaultSetting.stringValue() == String.valueOf(defaultValue) defaultSetting.origin == ConfigOrigin.DEFAULT defaultSetting.seqId == ConfigSetting.DEFAULT_SEQ_ID // ENV (valid) def envSetting = collected.get(ConfigOrigin.ENV).get(configKey) - envSetting.key == configKey envSetting.stringValue() == validValue envSetting.origin == ConfigOrigin.ENV // JVM_PROP (invalid, should still be reported) def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get(configKey) - jvmSetting.key == configKey jvmSetting.stringValue() == invalidValue jvmSetting.origin == ConfigOrigin.JVM_PROP @@ -174,26 +168,22 @@ class ConfigProviderTest extends DDSpecification { then: // Default def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get(configKey) - defaultSetting.key == configKey defaultSetting.stringValue() == String.valueOf(defaultValue) defaultSetting.origin == ConfigOrigin.DEFAULT defaultSetting.seqId == ConfigSetting.DEFAULT_SEQ_ID // ENV (valid) def envSetting = collected.get(ConfigOrigin.ENV).get(configKey) - envSetting.key == configKey envSetting.stringValue() == envValue envSetting.origin == ConfigOrigin.ENV // JVM_PROP (invalid, should still be reported) def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get(configKey) - jvmSetting.key == configKey jvmSetting.stringValue() == propValue jvmSetting.origin == ConfigOrigin.JVM_PROP // Config was evaluated to false and reported with CALCULATED origin def calcSetting = collected.get(ConfigOrigin.CALCULATED).get(configKey) - calcSetting.key == configKey calcSetting.stringValue() == "false" calcSetting.origin == ConfigOrigin.CALCULATED @@ -245,20 +235,17 @@ class ConfigProviderTest extends DDSpecification { then: // Default def defaultSetting = collected.get(ConfigOrigin.DEFAULT).get("test.string") - defaultSetting.key == "test.string" defaultSetting.stringValue() == "defaultValue" defaultSetting.origin == ConfigOrigin.DEFAULT defaultSetting.seqId == ConfigSetting.DEFAULT_SEQ_ID // ENV def envSetting = collected.get(ConfigOrigin.ENV).get("test.string") - envSetting.key == "test.string" envSetting.stringValue() == "envValue" envSetting.origin == ConfigOrigin.ENV // JVM_PROP (highest precedence) def jvmSetting = collected.get(ConfigOrigin.JVM_PROP).get("test.string") - jvmSetting.key == "test.string" jvmSetting.stringValue() == "jvmValue" jvmSetting.origin == ConfigOrigin.JVM_PROP diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 49036685a57..7ae745409f5 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -2684,7 +2684,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_PROFILING_APIKEY_FILE": [ @@ -2700,7 +2701,8 @@ "version": "A", "type": "string", "default": null, - "aliases": [] + "aliases": [], + "sensitive": true } ], "DD_PROFILING_API_KEY_FILE": [ diff --git a/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java b/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java index 40bd2e46471..1926c3ff8c8 100644 --- a/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java +++ b/telemetry/src/main/java/datadog/telemetry/TelemetryRequestBody.java @@ -227,7 +227,7 @@ public void beginConfiguration() throws IOException { public void writeConfiguration(ConfigSetting configSetting) throws IOException { bodyWriter.beginObject(); - bodyWriter.name("name").value(configSetting.normalizedKey()); + bodyWriter.name("name").value(configSetting.key); bodyWriter.setSerializeNulls(true); bodyWriter.name("value").value(configSetting.stringValue()); bodyWriter.setSerializeNulls(false); diff --git a/telemetry/src/test/groovy/datadog/telemetry/TestTelemetryRouter.groovy b/telemetry/src/test/groovy/datadog/telemetry/TestTelemetryRouter.groovy index 2aecd2a18ee..78017872b4f 100644 --- a/telemetry/src/test/groovy/datadog/telemetry/TestTelemetryRouter.groovy +++ b/telemetry/src/test/groovy/datadog/telemetry/TestTelemetryRouter.groovy @@ -250,7 +250,7 @@ class TestTelemetryRouter extends TelemetryRouter { def expected = configuration == null ? null : [] if (configuration != null) { for (ConfigSetting cs : configuration) { - def item = [name: cs.normalizedKey(), value: cs.stringValue(), origin: cs.origin.value, 'seq_id': cs.seqId] + def item = [name: cs.key, value: cs.stringValue(), origin: cs.origin.value, 'seq_id': cs.seqId] expected.add(item) } } 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 76f9bfcd5bd..43ec0e34353 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 @@ -1,8 +1,8 @@ package datadog.trace.api; +import static datadog.trace.config.inversion.GeneratedSupportedConfigurations.SENSITIVE_KEYS; import static datadog.trace.util.ConfigStrings.toCanonicalEnvVar; -import datadog.trace.config.inversion.GeneratedSupportedConfigurations; import java.util.BitSet; import java.util.Map; import java.util.Objects; @@ -38,23 +38,14 @@ public static ConfigSetting of( return new ConfigSetting(key, value, origin, seqId, configId); } - // Redact values of configs flagged "sensitive": true in metadata/supported-configurations.json. private ConfigSetting(String key, Object value, ConfigOrigin origin, int seqId, String configId) { - this.key = key; - this.value = - (value != null - && GeneratedSupportedConfigurations.SENSITIVE_KEYS.contains(toCanonicalEnvVar(key))) - ? "" - : value; + this.key = toCanonicalEnvVar(key); + this.value = (value != null && SENSITIVE_KEYS.contains(this.key)) ? "" : value; this.origin = origin; this.seqId = seqId; this.configId = configId; } - public String normalizedKey() { - return toCanonicalEnvVar(key); - } - public String stringValue() { if (value == null) { return null; @@ -140,7 +131,7 @@ public int hashCode() { public String toString() { return "ConfigSetting{" + "key='" - + normalizedKey() + + key + '\'' + ", value=" + stringValue() diff --git a/utils/config-utils/src/main/java/datadog/trace/util/ConfigStrings.java b/utils/config-utils/src/main/java/datadog/trace/util/ConfigStrings.java index 16307cdbea2..e9d38ea4c72 100644 --- a/utils/config-utils/src/main/java/datadog/trace/util/ConfigStrings.java +++ b/utils/config-utils/src/main/java/datadog/trace/util/ConfigStrings.java @@ -28,12 +28,7 @@ public static String propertyNameToEnvironmentVariableName(final String setting) } /** - * Converts a config key in any form into its canonical environment variable name, regardless of - * whether it arrived as a property name ({@code api-key}), a {@code dd.} system property ({@code - * dd.api-key}), or a raw environment variable ({@code DD_API_KEY}) -- all canonicalize to {@code - * DD_API_KEY}. Unlike {@link #propertyNameToEnvironmentVariableName(String)}, the {@code DD_} - * namespace is added only when not already present, so a key already in env-var form is not - * double-prefixed. {@code otel.}/{@code OTEL_} keys keep their own namespace. + * Converts a config key to its canonical {@code DD_} or {@code OTEL_} environment-variable form. * * @param key The config key in any form * @return The canonical environment variable name 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 ad2f23c491a..8e71067cf6f 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 @@ -44,17 +44,21 @@ void supportsEqualityCheck( } @TableTest({ - "scenario | key | value | filteredValue", - "api key property | api-key | somevalue | ", - "api key system prop | dd.api-key | somevalue | ", - "api key env var | DD_API_KEY | somevalue | ", - "application key | application-key | somevalue | ", - "application alias | DD_APP_KEY | somevalue | ", - "otlp traces headers | otlp.traces.headers | somevalue | ", - "proxy password | profiling.proxy.password | somevalue | ", - "session token prop | test.agent.session.token | somevalue | ", - "session token env | DD_TEST_AGENT_SESSION_TOKEN | somevalue | ", - "non-sensitive key | some.other.key | somevalue | somevalue " + "scenario | key | value | filteredValue", + "api key property | api-key | somevalue | ", + "api key system prop | dd.api-key | somevalue | ", + "api key env var | DD_API_KEY | somevalue | ", + "application key | application-key | somevalue | ", + "application alias | DD_APP_KEY | somevalue | ", + "otlp traces headers | otlp.traces.headers | somevalue | ", + "profiling API key | profiling.api-key | somevalue | ", + "profiling APIKEY | profiling.apikey | somevalue | ", + "profiling API env | DD_PROFILING_API_KEY | somevalue | ", + "profiling APIKEY env | DD_PROFILING_APIKEY | somevalue | ", + "proxy password | profiling.proxy.password | somevalue | ", + "session token prop | test.agent.session.token | somevalue | ", + "session token env | DD_TEST_AGENT_SESSION_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()); @@ -66,8 +70,8 @@ void filtersKeyValues(String key, String value, String filteredValue) { "dd env var | DD_PROFILING_API_KEY | DD_PROFILING_API_KEY", "otel env var | OTEL_SERVICE_NAME | OTEL_SERVICE_NAME " }) - void normalizesKeyName(String key, String normalized) { - assertEquals(normalized, ConfigSetting.of(key, "v", ConfigOrigin.DEFAULT).normalizedKey()); + void storesNormalizedKeyName(String key, String normalized) { + assertEquals(normalized, ConfigSetting.of(key, "v", ConfigOrigin.DEFAULT).key); } @TableTest({ From 8a2eb2c838db29bafccee00dc8f13d603cb4541f Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Fri, 28 Aug 2026 11:11:22 -0400 Subject: [PATCH 14/14] Redact sensitive configuration property keys --- .../config/ParseV2SupportedConfigurationsTask.kt | 13 ++++++++++++- .../config/ParseV2SupportedConfigurationsTest.kt | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt index 8a3d6fa10df..63116350507 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTask.kt @@ -15,6 +15,7 @@ import org.gradle.kotlin.dsl.property import java.io.File import java.io.FileInputStream import java.io.PrintWriter +import java.util.Locale import javax.inject.Inject @CacheableTask @@ -83,7 +84,9 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( }.toMap() val sensitiveKeys: Set = supported.flatMap { (canonical, configList) -> - configList.filter { it.sensitive }.flatMap { listOf(canonical) + it.aliases } + configList.filter { it.sensitive }.flatMap { + listOf(canonical) + it.aliases + it.propertyKeys.map(::toCanonicalEnvVar) + } }.toSet() // Build the output .java path from the fully-qualified class name @@ -107,6 +110,14 @@ abstract class ParseV2SupportedConfigurationsTask @Inject constructor( ) } + private fun toCanonicalEnvVar(key: String): String { + val env = key.replace('.', '_').replace('-', '_').uppercase(Locale.ROOT) + if (key.startsWith("otel.") || key.startsWith("OTEL_")) { + return env + } + return if (env.startsWith("DD_")) env else "DD_$env" + } + private fun generateJavaFile( outputPath: String, className: String, diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt index a71de75adbd..70935758004 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/config/ParseV2SupportedConfigurationsTest.kt @@ -78,6 +78,7 @@ class ParseV2SupportedConfigurationsTest : GradleFixture() { assertTrue(content.contains("""sensitiveKeys.add("DD_ACTION_EXECUTION_ID")""")) assertTrue(content.contains("""sensitiveKeys.add("DD_LEGACY_ACTION_EXECUTION_ID")""")) + assertTrue(content.contains("""sensitiveKeys.add("DD_PROPERTY_KEY")""")) assertFalse(content.contains("""sensitiveKeys.add("DD_AGENTLESS_LOG_SUBMISSION_ENABLED")""")) }