diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy b/internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.groovy similarity index 100% rename from internal-api/src/test/groovy/datadog/trace/api/ConfigCollectorTest.groovy rename to internal-api/src/test/java/datadog/trace/api/ConfigCollectorTest.groovy diff --git a/utils/config-utils/build.gradle.kts b/utils/config-utils/build.gradle.kts index 533808be60d..2174d0fc7e4 100644 --- a/utils/config-utils/build.gradle.kts +++ b/utils/config-utils/build.gradle.kts @@ -28,7 +28,7 @@ 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" ) ) @@ -42,7 +42,8 @@ val excludedClassesBranchCoverage by extra( val excludedClassesInstructionCoverage by extra( listOf( - "datadog.trace.config.inversion.GeneratedSupportedConfigurations" + "datadog.trace.config.inversion.GeneratedSupportedConfigurations", + "datadog.trace.config.inversion.SupportedConfigurationSource" ) ) @@ -53,4 +54,5 @@ dependencies { implementation(libs.slf4j) testImplementation(project(":utils:test-utils")) + testImplementation("org.snakeyaml:snakeyaml-engine:2.9") } 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..527bbcf7794 --- /dev/null +++ b/utils/config-utils/src/main/java/datadog/trace/config/inversion/ConfigHelper.java @@ -0,0 +1,146 @@ +package datadog.trace.config.inversion; + +import datadog.environment.EnvironmentVariables; +import datadog.trace.api.telemetry.ConfigInversionMetricCollectorProvider; +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; + + // Cache for configs, init value is null + private Map configs; + + // 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 = null; + } + + /** Reset all configuration data to the generated defaults. Useful for cleaning up after tests. */ + void resetToDefaults() { + configSource = new SupportedConfigurationSource(); + this.configInversionStrict = StrictnessPolicy.WARNING; + } + + public Map getEnvironmentVariables() { + if (configs != null) { + return configs; + } + + 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_") || key.startsWith("OTEL_") || null != primaryEnv) { + 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 (null != primaryEnv && !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; + if ((envFromDeprecated = configSource.primaryEnvFromDeprecated(key)) != 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 != null && configs.containsKey(name)) { + return configs.get(name); + } + + if ((name.startsWith("DD_") || name.startsWith("OTEL_")) + && null != configSource.primaryEnvFromAlias(name) + && !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..80b8b3d4f4f --- /dev/null +++ b/utils/config-utils/src/main/java/datadog/trace/config/inversion/SupportedConfigurationSource.java @@ -0,0 +1,30 @@ +package datadog.trace.config.inversion; + +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, null); + } + + /** @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..6bd940b471b --- /dev/null +++ b/utils/config-utils/src/test/java/datadog/trace/config/inversion/ConfigHelperTest.java @@ -0,0 +1,221 @@ +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 java.lang.reflect.Field; +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 TEST_DD_VAR = "DD_TEST_CONFIG"; + private static final String TEST_DD_VAR_VAL = "test_dd_var"; + private static final String TEST_OTEL_VAR = "OTEL_TEST_CONFIG"; + private static final String TEST_OTEL_VAR_VAL = "test_otel_var"; + private static final String TEST_REGULAR_VAR = "REGULAR_TEST_CONFIG"; + private static final String TEST_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 ConfigHelper.StrictnessPolicy strictness; + private static TestSupportedConfigurationSource testSource; + + @BeforeAll + static void setUp() { + // Set up test configurations using SupportedConfigurationSource + // + // ConfigInversionMetricCollectorProvider.register(ConfigInversionMetricCollectorImpl.getInstance()); + Set testSupported = + new HashSet<>(Arrays.asList(TEST_DD_VAR, TEST_OTEL_VAR, TEST_REGULAR_VAR)); + + Map> testAliases = new HashMap<>(); + testAliases.put(TEST_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, TEST_DD_VAR); + testAliasMapping.put(NON_DD_ALIAS_VAR, TEST_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(); + } + + @Test + void testBasicConfigHelper() { + setEnvVar(TEST_DD_VAR, TEST_DD_VAR_VAL); + setEnvVar(TEST_OTEL_VAR, TEST_OTEL_VAR_VAL); + setEnvVar(TEST_REGULAR_VAR, TEST_REGULAR_VAR_VAL); + + assertEquals(TEST_DD_VAR_VAL, ConfigHelper.get().getEnvironmentVariable(TEST_DD_VAR)); + assertEquals(TEST_OTEL_VAR_VAL, ConfigHelper.get().getEnvironmentVariable(TEST_OTEL_VAR)); + assertEquals(TEST_REGULAR_VAR_VAL, ConfigHelper.get().getEnvironmentVariable(TEST_REGULAR_VAR)); + + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(TEST_DD_VAR_VAL, result.get(TEST_DD_VAR)); + assertEquals(TEST_OTEL_VAR_VAL, result.get(TEST_OTEL_VAR)); + assertEquals(TEST_REGULAR_VAR_VAL, result.get(TEST_REGULAR_VAR)); + + // Cleanup + setEnvVar(TEST_DD_VAR, null); + setEnvVar(TEST_OTEL_VAR, null); + setEnvVar(TEST_REGULAR_VAR, null); + } + + @Test + void testAliasSupport() { + setEnvVar(ALIAS_DD_VAR, ALIAS_DD_VAL); + + assertEquals(ALIAS_DD_VAL, ConfigHelper.get().getEnvironmentVariable(TEST_DD_VAR)); + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(ALIAS_DD_VAL, result.get(TEST_DD_VAR)); + assertFalse(result.containsKey(ALIAS_DD_VAR)); + + // Cleanup + setEnvVar(ALIAS_DD_VAR, null); + } + + @Test + void testMainConfigPrecedence() { + // When both main variable and alias are set, main should take precedence + setEnvVar(TEST_DD_VAR, TEST_DD_VAR_VAL); + setEnvVar(ALIAS_DD_VAR, ALIAS_DD_VAL); + + assertEquals(TEST_DD_VAR_VAL, ConfigHelper.get().getEnvironmentVariable(TEST_DD_VAR)); + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(TEST_DD_VAR_VAL, result.get(TEST_DD_VAR)); + assertFalse(result.containsKey(ALIAS_DD_VAR)); + + // Cleanup + setEnvVar(TEST_DD_VAR, null); + setEnvVar(ALIAS_DD_VAR, null); + } + + @Test + void testNonDDAliases() { + setEnvVar(NON_DD_ALIAS_VAR, NON_DD_ALIAS_VAL); + + assertEquals(NON_DD_ALIAS_VAL, ConfigHelper.get().getEnvironmentVariable(TEST_DD_VAR)); + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(NON_DD_ALIAS_VAL, result.get(TEST_DD_VAR)); + assertFalse(result.containsKey(NON_DD_ALIAS_VAR)); + + // Cleanup + setEnvVar(NON_DD_ALIAS_VAR, null); + } + + @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() { + setEnvVar(TEST_DD_VAR, TEST_DD_VAR_VAL); + setEnvVar(NON_DD_ALIAS_VAR, NON_DD_ALIAS_VAL); + + Map result = ConfigHelper.get().getEnvironmentVariables(); + assertEquals(TEST_DD_VAR_VAL, result.get(TEST_DD_VAR)); + assertFalse(result.containsKey(NON_DD_ALIAS_VAR)); + + // Cleanup + setEnvVar(TEST_DD_VAR, null); + setEnvVar(NON_DD_ALIAS_VAR, null); + } + + @Test + void testInconsistentAliasesAndAliasMapping() { + setEnvVar(NEW_ALIAS_KEY_2, "some_value"); + + Map result = ConfigHelper.get().getEnvironmentVariables(); + + assertFalse(result.containsKey(NEW_ALIAS_KEY_2)); + assertFalse(result.containsKey(NEW_ALIAS_TARGET)); + + // Cleanup + setEnvVar(NEW_ALIAS_KEY_2, null); + } + + // TODO: Update to verify telemetry when implemented + @Test + void testUnsupportedEnvWarningNotInTestMode() { + ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.TEST); + + setEnvVar("DD_FAKE_VAR", "banana"); + + // Should allow unsupported variable in TEST mode + assertEquals("banana", ConfigHelper.get().getEnvironmentVariable("DD_FAKE_VAR")); + + // Cleanup + setEnvVar("DD_FAKE_VAR", null); + ConfigHelper.get().setConfigInversionStrict(ConfigHelper.StrictnessPolicy.STRICT); + } + + // Copied from utils.TestHelper + @SuppressWarnings("unchecked") + private static void setEnvVar(String envName, String envValue) { + try { + Class classOfMap = System.getenv().getClass(); + Field field = classOfMap.getDeclaredField("m"); + field.setAccessible(true); + if (envValue == null) { + ((Map) field.get(System.getenv())).remove(envName); + } else { + ((Map) field.get(System.getenv())).put(envName, envValue); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} 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..c7d4b5e0632 --- /dev/null +++ b/utils/config-utils/src/test/java/datadog/trace/config/inversion/TestSupportedConfigurationSource.java @@ -0,0 +1,44 @@ +package datadog.trace.config.inversion; + +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, null); + } + + @Override + public String primaryEnvFromAlias(String alias) { + return aliasMapping.getOrDefault(alias, null); + } + + @Override + public String primaryEnvFromDeprecated(String deprecated) { + return this.deprecated.getOrDefault(deprecated, null); + } +}