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