Skip to content
Open
6 changes: 4 additions & 2 deletions utils/config-utils/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
)

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

Expand All @@ -53,4 +54,5 @@ dependencies {
implementation(libs.slf4j)

testImplementation(project(":utils:test-utils"))
testImplementation("org.snakeyaml:snakeyaml-engine:2.9")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

🎯 suggestion: ‏Helper class usually are final and have a private constructor.

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 was also wondering if this could be made into a singleton as well... and if so there would be no reason to have static methods under it. Not sure if that is the right or wrong direction to go in

Copy link
Contributor

Choose a reason for hiding this comment

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

Usually, an helper class does not have a state. So if you have a state but only one instance, that would be a singleton.
Here might be a big ambiguous because most of the state is immutable, right? It's more like a big static data table and calling the helper methods won't mutate it. But as you have the StrictStyle thing that can change the behavior, I would with a singleton.

Keep in mind helper class / methods should be easier to test than singleton that usually are painful to deal with on test scenario.


/** 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;

// 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();
Map<String, String> aliasMapping = configSource.getAliasMapping();
if (key.startsWith("DD_") || key.startsWith("OTEL_") || aliasMapping.containsKey(key)) {
String baseConfig;
if (configSource.getSupportedConfigurations().contains(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 (aliasMapping.containsKey(key)
&& !configs.containsKey(baseConfig = aliasMapping.get(key))) {
List<String> aliasList = configSource.getAliases().get(baseConfig);
for (String alias : aliasList) {
if (env.containsKey(alias)) {
configs.put(baseConfig, env.get(alias));
break;
}
}
}
// TODO: Follow-up - Add deprecation handling
if (configSource.getDeprecatedConfigurations().containsKey(key)) {
String warning =
"Environment variable "
+ key
+ " is deprecated. "
+ (configSource.getAliasMapping().containsKey(key)
? "Please use " + configSource.getAliasMapping().get(key) + " instead."
: configSource.getDeprecatedConfigurations().get(key));
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_"))
&& !configSource.getAliasMapping().containsKey(name)
&& !configSource.getSupportedConfigurations().contains(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().get(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,32 @@
package datadog.trace.config.inversion;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* 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 configuration keys */
public Set<String> getSupportedConfigurations() {
return GeneratedSupportedConfigurations.SUPPORTED;
}

/** @return Map of configuration keys to their aliases */
public Map<String, List<String>> getAliases() {
return GeneratedSupportedConfigurations.ALIASES;
}

/** @return Map of alias keys to their primary configuration keys */
public Map<String, String> getAliasMapping() {
return GeneratedSupportedConfigurations.ALIAS_MAPPING;
}

/** @return Map of deprecated configurations */
public Map<String, String> getDeprecatedConfigurations() {
return GeneratedSupportedConfigurations.DEPRECATED;
}
}
Loading