diff --git a/utils/config-utils/build.gradle.kts b/utils/config-utils/build.gradle.kts index e77af1d50d0..03d9c8de0d0 100644 --- a/utils/config-utils/build.gradle.kts +++ b/utils/config-utils/build.gradle.kts @@ -30,20 +30,23 @@ val excludedClassesCoverage by extra( "datadog.trace.bootstrap.config.provider.stableconfig.Selector", // tested in internal-api "datadog.trace.bootstrap.config.provider.StableConfigParser", - "datadog.trace.bootstrap.config.provider.SystemPropertiesConfigSource", + "datadog.trace.bootstrap.config.provider.SystemPropertiesConfigSource" ) ) val excludedClassesBranchCoverage by extra( listOf( "datadog.trace.bootstrap.config.provider.AgentArgsInjector", + // Enum + "datadog.trace.config.inversion.ConfigHelper.StrictnessPolicy", "datadog.trace.util.ConfigStrings" ) ) val excludedClassesInstructionCoverage by extra( listOf( - "datadog.trace.config.inversion.GeneratedSupportedConfigurations" + "datadog.trace.config.inversion.GeneratedSupportedConfigurations", + "datadog.trace.config.inversion.SupportedConfigurationSource" ) ) diff --git a/utils/config-utils/src/main/java/datadog/trace/config/inversion/ConfigHelper.java b/utils/config-utils/src/main/java/datadog/trace/config/inversion/ConfigHelper.java new file mode 100644 index 00000000000..53341e0a66a --- /dev/null +++ b/utils/config-utils/src/main/java/datadog/trace/config/inversion/ConfigHelper.java @@ -0,0 +1,153 @@ +package datadog.trace.config.inversion; + +import datadog.environment.EnvironmentVariables; +import datadog.trace.api.telemetry.ConfigInversionMetricCollectorProvider; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ConfigHelper { + + /** Config Inversion strictness policy for enforcement of undocumented environment variables */ + public enum StrictnessPolicy { + STRICT, + WARNING, + TEST; + + private String displayName; + + StrictnessPolicy() { + this.displayName = name().toLowerCase(Locale.ROOT); + } + + @Override + public String toString() { + if (displayName == null) { + displayName = name().toLowerCase(Locale.ROOT); + } + return displayName; + } + } + + private static final Logger log = LoggerFactory.getLogger(ConfigHelper.class); + + private static final ConfigHelper INSTANCE = new ConfigHelper(); + + private StrictnessPolicy configInversionStrict = StrictnessPolicy.WARNING; + + private static final String DD_PREFIX = "DD_"; + private static final String OTEL_PREFIX = "OTEL_"; + + // Cache for configs, init value is EmptyMap + private Map configs = Collections.emptyMap(); + + // Default to production source + private SupportedConfigurationSource configSource = new SupportedConfigurationSource(); + + public static ConfigHelper get() { + return INSTANCE; + } + + public void setConfigInversionStrict(StrictnessPolicy configInversionStrict) { + this.configInversionStrict = configInversionStrict; + } + + public StrictnessPolicy configInversionStrictFlag() { + return configInversionStrict; + } + + // Used only for testing purposes + void setConfigurationSource(SupportedConfigurationSource testSource) { + configSource = testSource; + } + + /** Resetting config cache. Useful for cleaning up after tests. */ + void resetCache() { + configs = Collections.emptyMap(); + } + + /** Reset all configuration data to the generated defaults. Useful for cleaning up after tests. */ + void resetToDefaults() { + configSource = new SupportedConfigurationSource(); + this.configInversionStrict = StrictnessPolicy.WARNING; + resetCache(); + } + + public Map getEnvironmentVariables() { + if (!configs.isEmpty()) { + return configs; + } + + // Initial value is EmptyMap + configs = new HashMap<>(); + + Map env = EnvironmentVariables.getAll(); + for (Map.Entry entry : env.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + String primaryEnv = configSource.primaryEnvFromAlias(key); + if (key.startsWith(DD_PREFIX) || key.startsWith(OTEL_PREFIX) || primaryEnv != null) { + if (configSource.supported(key)) { + configs.put(key, value); + // If this environment variable is the alias of another, and we haven't processed the + // original environment variable yet, handle it here. + } else if (primaryEnv != null && !configs.containsKey(primaryEnv)) { + List aliases = configSource.getAliases(primaryEnv); + for (String alias : aliases) { + if (env.containsKey(alias)) { + configs.put(primaryEnv, env.get(alias)); + break; + } + } + } + String envFromDeprecated = configSource.primaryEnvFromDeprecated(key); + if (envFromDeprecated != null) { + String warning = + "Environment variable " + + key + + " is deprecated. Please use " + + (primaryEnv != null ? primaryEnv : envFromDeprecated) + + " instead."; + log.warn(warning); + } + } else { + configs.put(key, value); + } + } + return configs; + } + + public String getEnvironmentVariable(String name) { + if (configs.containsKey(name)) { + return configs.get(name); + } + + if ((name.startsWith(DD_PREFIX) || name.startsWith(OTEL_PREFIX)) + && configSource.primaryEnvFromAlias(name) == null + && !configSource.supported(name)) { + if (configInversionStrict != StrictnessPolicy.TEST) { + ConfigInversionMetricCollectorProvider.get().setUndocumentedEnvVarMetric(name); + } + + if (configInversionStrict == StrictnessPolicy.STRICT) { + return null; // If strict mode is enabled, return null for unsupported configs + } + } + + String config = EnvironmentVariables.get(name); + List aliases; + if (config == null && (aliases = configSource.getAliases(name)) != null) { + for (String alias : aliases) { + String aliasValue = EnvironmentVariables.get(alias); + if (aliasValue != null) { + return aliasValue; + } + } + } + return config; + } +} diff --git a/utils/config-utils/src/main/java/datadog/trace/config/inversion/SupportedConfigurationSource.java b/utils/config-utils/src/main/java/datadog/trace/config/inversion/SupportedConfigurationSource.java new file mode 100644 index 00000000000..b42baab5481 --- /dev/null +++ b/utils/config-utils/src/main/java/datadog/trace/config/inversion/SupportedConfigurationSource.java @@ -0,0 +1,31 @@ +package datadog.trace.config.inversion; + +import java.util.Collections; +import java.util.List; + +/** + * This class uses {@link #GeneratedSupportedConfigurations} for handling supported configurations + * for Config Inversion Can be extended for testing with custom configuration data. + */ +class SupportedConfigurationSource { + + /** @return Set of supported environment variable keys */ + public boolean supported(String env) { + return GeneratedSupportedConfigurations.SUPPORTED.contains(env); + } + + /** @return List of aliases for an environment variable */ + public List getAliases(String env) { + return GeneratedSupportedConfigurations.ALIASES.getOrDefault(env, Collections.emptyList()); + } + + /** @return Primary environment variable for a queried alias */ + public String primaryEnvFromAlias(String alias) { + return GeneratedSupportedConfigurations.ALIAS_MAPPING.getOrDefault(alias, null); + } + + /** @return Map of deprecated configurations */ + public String primaryEnvFromDeprecated(String deprecated) { + return GeneratedSupportedConfigurations.DEPRECATED.getOrDefault(deprecated, null); + } +} diff --git a/utils/config-utils/src/test/java/datadog/trace/config/inversion/ConfigHelperTest.java b/utils/config-utils/src/test/java/datadog/trace/config/inversion/ConfigHelperTest.java new file mode 100644 index 00000000000..de25a06aefe --- /dev/null +++ b/utils/config-utils/src/test/java/datadog/trace/config/inversion/ConfigHelperTest.java @@ -0,0 +1,195 @@ +package datadog.trace.config.inversion; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import datadog.trace.test.util.ControllableEnvironmentVariables; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ConfigHelperTest { + // Test environment variables + private static final String DD_VAR = "DD_TEST_CONFIG"; + private static final String DD_VAR_VAL = "test_dd_var"; + private static final String OTEL_VAR = "OTEL_TEST_CONFIG"; + private static final String OTEL_VAR_VAL = "test_otel_var"; + private static final String REGULAR_VAR = "REGULAR_TEST_CONFIG"; + private static final String REGULAR_VAR_VAL = "test_regular_var"; + + private static final String ALIAS_DD_VAR = "DD_TEST_CONFIG_ALIAS"; + private static final String ALIAS_DD_VAL = "test_alias_val"; + private static final String NON_DD_ALIAS_VAR = "TEST_CONFIG_ALIAS"; + private static final String NON_DD_ALIAS_VAL = "test_alias_val_non_dd"; + + private static final String NEW_ALIAS_TARGET = "DD_NEW_ALIAS_TARGET"; + private static final String NEW_ALIAS_KEY_1 = "DD_NEW_ALIAS_KEY_1"; + private static final String NEW_ALIAS_KEY_2 = "DD_NEW_ALIAS_KEY_2"; + + private static ControllableEnvironmentVariables env; + + private static ConfigHelper.StrictnessPolicy strictness; + private static TestSupportedConfigurationSource testSource; + + @BeforeAll + static void setUp() { + env = ControllableEnvironmentVariables.setup(); + + // Set up test configurations using SupportedConfigurationSource + Set testSupported = new HashSet<>(Arrays.asList(DD_VAR, OTEL_VAR, REGULAR_VAR)); + + Map> testAliases = new HashMap<>(); + testAliases.put(DD_VAR, Arrays.asList(ALIAS_DD_VAR, NON_DD_ALIAS_VAR)); + testAliases.put(NEW_ALIAS_TARGET, Arrays.asList(NEW_ALIAS_KEY_1)); + + Map testAliasMapping = new HashMap<>(); + testAliasMapping.put(ALIAS_DD_VAR, DD_VAR); + testAliasMapping.put(NON_DD_ALIAS_VAR, DD_VAR); + testAliasMapping.put(NEW_ALIAS_KEY_2, NEW_ALIAS_TARGET); + + // Create and set test configuration source + testSource = + new TestSupportedConfigurationSource( + testSupported, testAliases, testAliasMapping, new HashMap<>()); + ConfigHelper.get().setConfigurationSource(testSource); + strictness = ConfigHelper.get().configInversionStrictFlag(); + ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.STRICT); + } + + @AfterAll + static void tearDown() { + ConfigHelper.get().resetToDefaults(); + ConfigHelper.get().setConfigInversionStrict(strictness); + } + + @AfterEach + void reset() { + ConfigHelper.get().resetCache(); + env.clear(); + } + + @Test + void testBasicConfigHelper() { + env.set(DD_VAR, DD_VAR_VAL); + env.set(OTEL_VAR, OTEL_VAR_VAL); + env.set(REGULAR_VAR, REGULAR_VAR_VAL); + + assertEquals(DD_VAR_VAL, ConfigHelper.get().getEnvironmentVariable(DD_VAR)); + assertEquals(OTEL_VAR_VAL, ConfigHelper.get().getEnvironmentVariable(OTEL_VAR)); + assertEquals(REGULAR_VAR_VAL, ConfigHelper.get().getEnvironmentVariable(REGULAR_VAR)); + + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(DD_VAR_VAL, result.get(DD_VAR)); + assertEquals(OTEL_VAR_VAL, result.get(OTEL_VAR)); + assertEquals(REGULAR_VAR_VAL, result.get(REGULAR_VAR)); + } + + @Test + void testAliasSupport() { + env.set(ALIAS_DD_VAR, ALIAS_DD_VAL); + + assertEquals(ALIAS_DD_VAL, ConfigHelper.get().getEnvironmentVariable(DD_VAR)); + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(ALIAS_DD_VAL, result.get(DD_VAR)); + assertFalse(result.containsKey(ALIAS_DD_VAR)); + } + + @Test + void testMainConfigPrecedence() { + // When both main variable and alias are set, main should take precedence + env.set(DD_VAR, DD_VAR_VAL); + env.set(ALIAS_DD_VAR, ALIAS_DD_VAL); + + assertEquals(DD_VAR_VAL, ConfigHelper.get().getEnvironmentVariable(DD_VAR)); + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(DD_VAR_VAL, result.get(DD_VAR)); + assertFalse(result.containsKey(ALIAS_DD_VAR)); + } + + @Test + void testNonDDAliases() { + env.set(NON_DD_ALIAS_VAR, NON_DD_ALIAS_VAL); + + assertEquals(NON_DD_ALIAS_VAL, ConfigHelper.get().getEnvironmentVariable(DD_VAR)); + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(NON_DD_ALIAS_VAL, result.get(DD_VAR)); + assertFalse(result.containsKey(NON_DD_ALIAS_VAR)); + } + + @Test + void testAliasesWithoutPresentAliases() { + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertFalse(result.containsKey(ALIAS_DD_VAR)); + } + + @Test + void testAliasWithEmptyList() { + Map> aliasMap = new HashMap<>(); + aliasMap.put("EMPTY_ALIAS_CONFIG", new ArrayList<>()); + + ConfigHelper.get() + .setConfigurationSource( + new TestSupportedConfigurationSource( + new HashSet<>(), aliasMap, new HashMap<>(), new HashMap<>())); + + assertNull(ConfigHelper.get().getEnvironmentVariable("EMPTY_ALIAS_CONFIG")); + + // Cleanup + ConfigHelper.get().setConfigurationSource(testSource); + } + + @Test + void testAliasSkippedWhenBaseAlreadyPresent() { + env.set(DD_VAR, DD_VAR_VAL); + env.set(NON_DD_ALIAS_VAR, NON_DD_ALIAS_VAL); + + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(DD_VAR_VAL, result.get(DD_VAR)); + assertFalse(result.containsKey(NON_DD_ALIAS_VAR)); + } + + @Test + void testInconsistentAliasesAndAliasMapping() { + env.set(NEW_ALIAS_KEY_2, "some_value"); + + Map result = ConfigHelper.get().getEnvironmentVariables(); + + assertFalse(result.containsKey(NEW_ALIAS_KEY_2)); + assertFalse(result.containsKey(NEW_ALIAS_TARGET)); + } + + @Test + void testUnsupportedEnvWarningNotInTestMode() { + ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.TEST); + + env.set("DD_FAKE_VAR", "banana"); + + // Should allow unsupported variable in TEST mode + assertEquals("banana", ConfigHelper.get().getEnvironmentVariable("DD_FAKE_VAR")); + + // Cleanup + ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.STRICT); + } + + @Test + void testCache() { + env.set(DD_VAR, DD_VAR_VAL); + + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(DD_VAR_VAL, result.get(DD_VAR)); + + // Ensure that the cached value is returned + env.set(DD_VAR, ALIAS_DD_VAL); + assertEquals(DD_VAR_VAL, result.get(DD_VAR)); + assertEquals(DD_VAR_VAL, ConfigHelper.get().getEnvironmentVariable(DD_VAR)); + } +} diff --git a/utils/config-utils/src/test/java/datadog/trace/config/inversion/TestSupportedConfigurationSource.java b/utils/config-utils/src/test/java/datadog/trace/config/inversion/TestSupportedConfigurationSource.java new file mode 100644 index 00000000000..3dee8925dd1 --- /dev/null +++ b/utils/config-utils/src/test/java/datadog/trace/config/inversion/TestSupportedConfigurationSource.java @@ -0,0 +1,45 @@ +package datadog.trace.config.inversion; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Test implementation of SupportedConfigurationSource that uses custom configuration data */ +class TestSupportedConfigurationSource extends SupportedConfigurationSource { + private final Set supported; + private final Map> aliases; + private final Map aliasMapping; + private final Map deprecated; + + public TestSupportedConfigurationSource( + Set supported, + Map> aliases, + Map aliasMapping, + Map deprecated) { + this.supported = supported; + this.aliases = aliases; + this.aliasMapping = aliasMapping; + this.deprecated = deprecated; + } + + @Override + public boolean supported(String env) { + return supported.contains(env); + } + + @Override + public List getAliases(String env) { + return aliases.getOrDefault(env, Collections.emptyList()); + } + + @Override + public String primaryEnvFromAlias(String alias) { + return aliasMapping.getOrDefault(alias, null); + } + + @Override + public String primaryEnvFromDeprecated(String deprecated) { + return this.deprecated.getOrDefault(deprecated, null); + } +}