Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions utils/config-utils/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,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"
)
)

Expand All @@ -43,7 +43,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"
)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> configs;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking out loud: do we need null state? Maybe empty map will be better? No null checks in usages? WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use empty map instead. Here is my thought process:

The only situation where initializing configs as null would be better is when no env vars are set at all, since our cache would have the same effect as if we had not yet processed env vars. This is a very improbable situation since we expect customers to set env vars, so I'm happy to change to initializing to an empty map.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I learned that Collections.emptyMap() is a EmptyMap type, which is different than an empty HashMap. There is no reason to not use it here :)


// 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<String, String> getEnvironmentVariables() {
if (configs != null) {
return configs;
}

configs = new HashMap<>();
Map<String, String> env = EnvironmentVariables.getAll();
for (Map.Entry<String, String> 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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we OK with Yoda style? Here and other similar places.
Maybe natural way is better or more human: primaryEnv != null, WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember in a previous PR, I was told that there was some benefit to doing Yoda style, but that may be for a different context. I can't find the old comment but I can reverse the ordering here for readability.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is not strict request, can you search code base to get some stats?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick search on GitHub, instances of != null and == null vs null != and null == seem to be very comparable 🤷‍♂️

Copy link
Member

@jpbempel jpbempel Oct 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was told that there was some benefit to doing Yoda style

in C++, not in Java
see https://jpbempel.github.io/2015/09/21/yoda-conditions.html for more rationale

List<String> 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<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package datadog.trace.config.inversion;

import java.util.ArrayList;
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<String> getAliases(String env) {
return GeneratedSupportedConfigurations.ALIASES.getOrDefault(env, new ArrayList<>());
}

/** @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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
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 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 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<String> testSupported =
new HashSet<>(Arrays.asList(TEST_DD_VAR, TEST_OTEL_VAR, TEST_REGULAR_VAR));

Map<String, List<String>> 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<String, String> 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();
env.clear();
}

@Test
void testBasicConfigHelper() {
env.set(TEST_DD_VAR, TEST_DD_VAR_VAL);
env.set(TEST_OTEL_VAR, TEST_OTEL_VAR_VAL);
env.set(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<String, String> 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));
}

@Test
void testAliasSupport() {
env.set(ALIAS_DD_VAR, ALIAS_DD_VAL);

assertEquals(ALIAS_DD_VAL, ConfigHelper.get().getEnvironmentVariable(TEST_DD_VAR));
Map<String, String> result = ConfigHelper.get().getEnvironmentVariables();
assertEquals(ALIAS_DD_VAL, result.get(TEST_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(TEST_DD_VAR, TEST_DD_VAR_VAL);
env.set(ALIAS_DD_VAR, ALIAS_DD_VAL);

assertEquals(TEST_DD_VAR_VAL, ConfigHelper.get().getEnvironmentVariable(TEST_DD_VAR));
Map<String, String> result = ConfigHelper.get().getEnvironmentVariables();
assertEquals(TEST_DD_VAR_VAL, result.get(TEST_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(TEST_DD_VAR));
Map<String, String> result = ConfigHelper.get().getEnvironmentVariables();
assertEquals(NON_DD_ALIAS_VAL, result.get(TEST_DD_VAR));
assertFalse(result.containsKey(NON_DD_ALIAS_VAR));
}

@Test
void testAliasesWithoutPresentAliases() {
Map<String, String> result = ConfigHelper.get().getEnvironmentVariables();
assertFalse(result.containsKey(ALIAS_DD_VAR));
}

@Test
void testAliasWithEmptyList() {
Map<String, List<String>> 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(TEST_DD_VAR, TEST_DD_VAR_VAL);
env.set(NON_DD_ALIAS_VAR, NON_DD_ALIAS_VAL);

Map<String, String> result = ConfigHelper.get().getEnvironmentVariables();
assertEquals(TEST_DD_VAR_VAL, result.get(TEST_DD_VAR));
assertFalse(result.containsKey(NON_DD_ALIAS_VAR));
}

@Test
void testInconsistentAliasesAndAliasMapping() {
env.set(NEW_ALIAS_KEY_2, "some_value");

Map<String, String> 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);
}
}
Loading
Loading