diff --git a/dd-trace-api/src/main/java/datadog/trace/api/Config.java b/dd-trace-api/src/main/java/datadog/trace/api/Config.java index 9b960b4450..37ca73aa57 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/Config.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/Config.java @@ -1,5 +1,9 @@ package datadog.trace.api; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Arrays; @@ -19,8 +23,8 @@ import lombok.ToString; import lombok.extern.slf4j.Slf4j; /** - * Config gives priority to system properties and falls back to environment variables. It also - * includes default values to ensure a valid config. + * Config reads values with the following priority: 1) system properties, 2) environment variables, + * 3) optional configuration file. It also includes default values to ensure a valid config. * *

* @@ -35,6 +39,7 @@ public class Config { private static final Pattern ENV_REPLACEMENT = Pattern.compile("[^a-zA-Z0-9_]"); + public static final String CONFIGURATION_FILE = "trace.config"; public static final String SERVICE_NAME = "service.name"; public static final String SERVICE = "service"; public static final String TRACE_ENABLED = "trace.enabled"; @@ -190,9 +195,14 @@ public class Config { @Getter private final boolean traceAnalyticsEnabled; - // Read order: System Properties -> Env Variables, [-> default value] + // Values from an optionally provided properties file + private static Properties propertiesFromConfigFile; + + // Read order: System Properties -> Env Variables, [-> properties file], [-> default value] // Visible for testing Config() { + propertiesFromConfigFile = loadConfigurationFile(); + runtimeId = UUID.randomUUID().toString(); serviceName = getSettingFromEnvironment(SERVICE_NAME, DEFAULT_SERVICE_NAME); @@ -566,7 +576,8 @@ public class Config { /** * Helper method that takes the name, adds a "dd." prefix then checks for System Properties of * that name. If none found, the name is converted to an Environment Variable and used to check - * the env. If setting not configured in either location, defaultValue is returned. + * the env. If none of the above returns a value, then an optional properties file if checked. If + * setting is not configured in either location, defaultValue is returned. * * @param name * @param defaultValue @@ -574,17 +585,34 @@ public class Config { * @deprecated This method should only be used internally. Use the explicit getter instead. */ public static String getSettingFromEnvironment(final String name, final String defaultValue) { - final String completeName = PREFIX + name; - final String value = - System.getProperties() - .getProperty(completeName, System.getenv(propertyToEnvironmentName(completeName))); - return value == null ? defaultValue : value; + String value; + + // System properties and properties provided from command line have the highest precedence + value = System.getProperties().getProperty(propertyNameToSystemPropertyName(name)); + if (null != value) { + return value; + } + + // If value not provided from system properties, looking at env variables + value = System.getenv(propertyNameToEnvironmentVariableName(name)); + if (null != value) { + return value; + } + + // If value is not defined yet, we look at properties optionally defined in a properties file + value = propertiesFromConfigFile.getProperty(propertyNameToSystemPropertyName(name)); + if (null != value) { + return value; + } + + return defaultValue; } /** @deprecated This method should only be used internally. Use the explicit getter instead. */ private static Map getMapSettingFromEnvironment( final String name, final String defaultValue) { - return parseMap(getSettingFromEnvironment(name, defaultValue), PREFIX + name); + return parseMap( + getSettingFromEnvironment(name, defaultValue), propertyNameToSystemPropertyName(name)); } /** @@ -674,8 +702,28 @@ public class Config { } } - private static String propertyToEnvironmentName(final String name) { - return ENV_REPLACEMENT.matcher(name.toUpperCase()).replaceAll("_"); + /** + * Converts the property name, e.g. 'service.name' into a public environment variable name, e.g. + * `DD_SERVICE_NAME`. + * + * @param setting The setting name, e.g. `service.name` + * @return The public facing environment variable name + */ + private static String propertyNameToEnvironmentVariableName(final String setting) { + return ENV_REPLACEMENT + .matcher(propertyNameToSystemPropertyName(setting).toUpperCase()) + .replaceAll("_"); + } + + /** + * Converts the property name, e.g. 'service.name' into a public system property name, e.g. + * `dd.service.name`. + * + * @param setting The setting name, e.g. `service.name` + * @return The public facing system property name + */ + private static String propertyNameToSystemPropertyName(String setting) { + return PREFIX + setting; } private static Map getPropertyMapValue( @@ -830,6 +878,50 @@ public class Config { return Collections.unmodifiableSet(result); } + /** + * Loads the optional configuration properties file into the global {@link Properties} object. + * + * @return The {@link Properties} object. the returned instance might be empty of file does not + * exist or if it is in a wrong format. + */ + private static Properties loadConfigurationFile() { + Properties properties = new Properties(); + + // Reading from system property first and from env after + String configurationFilePath = + System.getProperty(propertyNameToSystemPropertyName(CONFIGURATION_FILE)); + if (null == configurationFilePath) { + configurationFilePath = + System.getenv(propertyNameToEnvironmentVariableName(CONFIGURATION_FILE)); + } + if (null == configurationFilePath) { + return properties; + } + + // Normalizing tilde (~) paths for unix systems + configurationFilePath = + configurationFilePath.replaceFirst("^~", System.getProperty("user.home")); + + // Configuration properties file is optional + File configurationFile = new File(configurationFilePath); + if (!configurationFile.exists()) { + log.error("Configuration file '{}' not found.", configurationFilePath); + return properties; + } + + try { + FileReader fileReader = new FileReader(configurationFile); + properties.load(fileReader); + } catch (FileNotFoundException fnf) { + log.error("Configuration file '{}' not found.", configurationFilePath); + } catch (IOException ioe) { + log.error( + "Configuration file '{}' cannot be accessed or correctly parsed.", configurationFilePath); + } + + return properties; + } + /** * Returns the detected hostname. This operation is time consuming so if the usage changes and * this method will be called several times then we should implement some sort of caching. diff --git a/dd-trace-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy b/dd-trace-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy index afc7ca1941..f9fd7f3894 100644 --- a/dd-trace-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy +++ b/dd-trace-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy @@ -8,6 +8,7 @@ import spock.lang.Specification import static datadog.trace.api.Config.AGENT_HOST import static datadog.trace.api.Config.AGENT_PORT_LEGACY import static datadog.trace.api.Config.AGENT_UNIX_DOMAIN_SOCKET +import static datadog.trace.api.Config.CONFIGURATION_FILE import static datadog.trace.api.Config.DB_CLIENT_HOST_SPLIT_BY_INSTANCE import static datadog.trace.api.Config.DEFAULT_JMX_FETCH_STATSD_PORT import static datadog.trace.api.Config.GLOBAL_TAGS @@ -725,4 +726,65 @@ class ConfigTest extends Specification { then: config.localRootSpanTags.get('_dd.hostname') == InetAddress.localHost.hostName } + + def "verify fallback to properties file"() { + setup: + System.setProperty(PREFIX + CONFIGURATION_FILE, "src/test/resources/dd-java-tracer.properties") + + when: + def config = new Config() + + then: + config.serviceName == "set-in-properties" + + cleanup: + System.clearProperty(PREFIX + CONFIGURATION_FILE) + } + + def "verify fallback to properties file has lower priority than system property"() { + setup: + System.setProperty(PREFIX + CONFIGURATION_FILE, "src/test/resources/dd-java-tracer.properties") + System.setProperty(PREFIX + SERVICE_NAME, "set-in-system") + + when: + def config = new Config() + + then: + config.serviceName == "set-in-system" + + cleanup: + System.clearProperty(PREFIX + CONFIGURATION_FILE) + System.clearProperty(PREFIX + SERVICE_NAME) + } + + def "verify fallback to properties file has lower priority than env var"() { + setup: + System.setProperty(PREFIX + CONFIGURATION_FILE, "src/test/resources/dd-java-tracer.properties") + environmentVariables.set("DD_SERVICE_NAME", "set-in-env") + + when: + def config = new Config() + + then: + config.serviceName == "set-in-env" + + cleanup: + System.clearProperty(PREFIX + CONFIGURATION_FILE) + System.clearProperty(PREFIX + SERVICE_NAME) + environmentVariables.clear("DD_SERVICE_NAME") + } + + def "verify fallback to properties file that does not exist does not crash app"() { + setup: + System.setProperty(PREFIX + CONFIGURATION_FILE, "src/test/resources/do-not-exist.properties") + + when: + def config = new Config() + + then: + config.serviceName == 'unnamed-java-app' + + cleanup: + System.clearProperty(PREFIX + CONFIGURATION_FILE) + } } diff --git a/dd-trace-api/src/test/resources/dd-java-tracer.properties b/dd-trace-api/src/test/resources/dd-java-tracer.properties new file mode 100644 index 0000000000..1b4c322c6d --- /dev/null +++ b/dd-trace-api/src/test/resources/dd-java-tracer.properties @@ -0,0 +1 @@ +dd.service.name=set-in-properties