From 0d6eb3f7874f0f46c149ad44190f480ab0a42aee Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Wed, 12 Jun 2024 03:12:47 +0100 Subject: [PATCH 1/3] Map OpenTelemetry environment variables and system properties to their Datadog equivalents --- .../java/datadog/trace/bootstrap/Agent.java | 3 + .../datadog/trace/api/ConfigDefaults.java | 2 +- .../config/provider/ConfigProvider.java | 6 + .../provider/OtelEnvironmentConfigSource.java | 335 ++++++++++++++++++ .../OtelEnvironmentConfigSourceTest.groovy | 295 +++++++++++++++ 5 files changed, 640 insertions(+), 1 deletion(-) create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java create mode 100644 internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index b4b244a6ebf..fcb5b33ad67 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -1052,6 +1052,9 @@ private static void configureLogger() { logLevel = "DEBUG"; } else { logLevel = ddGetProperty("dd.log.level"); + if (null == logLevel) { + logLevel = System.getenv("OTEL_LOG_LEVEL"); + } } if (null == logLevel && !isFeatureEnabled(AgentFeature.STARTUP_LOGS)) { diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index 93c821cda5c..e869c798784 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -49,7 +49,7 @@ public final class ConfigDefaults { static final boolean DEFAULT_SPAN_ORIGIN_ENABLED = false; static final boolean DEFAULT_TRACE_ENABLED = true; - static final boolean DEFAULT_TRACE_OTEL_ENABLED = false; + public static final boolean DEFAULT_TRACE_OTEL_ENABLED = false; static final boolean DEFAULT_INTEGRATIONS_ENABLED = true; static final boolean DEFAULT_RUNTIME_CONTEXT_FIELD_INJECTION = true; diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/ConfigProvider.java b/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/ConfigProvider.java index c7b49d6ec06..110731a313e 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/ConfigProvider.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/ConfigProvider.java @@ -357,12 +357,14 @@ public static ConfigProvider createDefault() { return new ConfigProvider( new SystemPropertiesConfigSource(), new EnvironmentConfigSource(), + new OtelEnvironmentConfigSource(), new CapturedEnvironmentConfigSource()); } else { return new ConfigProvider( new SystemPropertiesConfigSource(), new EnvironmentConfigSource(), new PropertiesConfigSource(configProperties, true), + new OtelEnvironmentConfigSource(), new CapturedEnvironmentConfigSource()); } } @@ -377,6 +379,7 @@ public static ConfigProvider withoutCollector() { false, new SystemPropertiesConfigSource(), new EnvironmentConfigSource(), + new OtelEnvironmentConfigSource(), new CapturedEnvironmentConfigSource()); } else { return new ConfigProvider( @@ -384,6 +387,7 @@ public static ConfigProvider withoutCollector() { new SystemPropertiesConfigSource(), new EnvironmentConfigSource(), new PropertiesConfigSource(configProperties, true), + new OtelEnvironmentConfigSource(), new CapturedEnvironmentConfigSource()); } } @@ -401,6 +405,7 @@ public static ConfigProvider withPropertiesOverride(Properties properties) { new SystemPropertiesConfigSource(), new EnvironmentConfigSource(), providedConfigSource, + new OtelEnvironmentConfigSource(), new CapturedEnvironmentConfigSource()); } else { return new ConfigProvider( @@ -408,6 +413,7 @@ public static ConfigProvider withPropertiesOverride(Properties properties) { new SystemPropertiesConfigSource(), new EnvironmentConfigSource(), new PropertiesConfigSource(configProperties, true), + new OtelEnvironmentConfigSource(), new CapturedEnvironmentConfigSource()); } } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java b/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java new file mode 100644 index 00000000000..61ee7fdcefc --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java @@ -0,0 +1,335 @@ +package datadog.trace.bootstrap.config.provider; + +import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_OTEL_ENABLED; +import static datadog.trace.api.config.GeneralConfig.ENV; +import static datadog.trace.api.config.GeneralConfig.LOG_LEVEL; +import static datadog.trace.api.config.GeneralConfig.RUNTIME_METRICS_ENABLED; +import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME; +import static datadog.trace.api.config.GeneralConfig.TAGS; +import static datadog.trace.api.config.GeneralConfig.VERSION; +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ENABLED; +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_EXTENSIONS_PATH; +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_OTEL_ENABLED; +import static datadog.trace.api.config.TracerConfig.REQUEST_HEADER_TAGS; +import static datadog.trace.api.config.TracerConfig.RESPONSE_HEADER_TAGS; +import static datadog.trace.api.config.TracerConfig.TRACE_PROPAGATION_STYLE; +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE; + +import datadog.trace.api.ConfigOrigin; +import datadog.trace.api.TracePropagationStyle; +import datadog.trace.util.Strings; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Maps OpenTelemetry system properties and environment variables to their Datadog equivalents. */ +final class OtelEnvironmentConfigSource extends ConfigProvider.Source { + private static final Logger log = LoggerFactory.getLogger(OtelEnvironmentConfigSource.class); + + private final Map otelEnvironment = new HashMap<>(); + + private final Properties otelConfigFile = loadOtelConfigFile(); + + @Override + protected String get(String key) { + return otelEnvironment.get(key); + } + + @Override + public ConfigOrigin origin() { + return ConfigOrigin.ENV; + } + + OtelEnvironmentConfigSource() { + if (!traceOtelEnabled()) { + return; + } + + // only applies when OTEL is enabled by default (otherwise TRACE_OTEL_ENABLED takes precedence) + String sdkDisabled = getOtelProperty("otel.sdk.disabled", "dd." + TRACE_OTEL_ENABLED); + if ("true".equalsIgnoreCase(sdkDisabled)) { + capture(TRACE_OTEL_ENABLED, "false"); + return; + } + + String serviceName = getOtelProperty("otel.service.name", "dd." + SERVICE_NAME); + String logLevel = getOtelProperty("otel.log.level", "dd." + LOG_LEVEL); + String propagators = getOtelProperty("otel.propagators", "dd." + TRACE_PROPAGATION_STYLE); + String tracesSampler = getOtelProperty("otel.traces.sampler", "dd." + TRACE_SAMPLE_RATE); + String resourceAttributes = getOtelProperty("otel.resource.attributes", "dd." + TAGS); + String requestHeaders = getOtelHeaders("request-headers", "dd." + REQUEST_HEADER_TAGS); + String responseHeaders = getOtelHeaders("response-headers", "dd." + RESPONSE_HEADER_TAGS); + String extensions = getOtelProperty("otel.javaagent.extensions", "dd." + TRACE_EXTENSIONS_PATH); + + if (null != resourceAttributes) { + Map attributeMap = parseOtelMap(resourceAttributes); + capture(SERVICE_NAME, attributeMap.remove("service.name")); + capture(VERSION, attributeMap.remove("service.version")); + capture(ENV, attributeMap.remove("deployment.environment")); + capture(TAGS, renderDatadogMap(attributeMap, 10)); + } + + capture(LOG_LEVEL, logLevel); + capture(SERVICE_NAME, serviceName); + capture(TRACE_PROPAGATION_STYLE, mapPropagationStyle(propagators)); + capture(TRACE_SAMPLE_RATE, mapSampleRate(tracesSampler)); + + capture(TRACE_ENABLED, mapDataCollection("traces")); + capture(RUNTIME_METRICS_ENABLED, mapDataCollection("metrics")); + mapDataCollection("logs"); // check setting, but no need to capture it + + capture(REQUEST_HEADER_TAGS, mapHeaderTags("http.request.header.", requestHeaders)); + capture(RESPONSE_HEADER_TAGS, mapHeaderTags("http.response.header.", responseHeaders)); + + capture(TRACE_EXTENSIONS_PATH, extensions); + } + + private static boolean traceOtelEnabled() { + String enabled = getProperty("dd." + TRACE_OTEL_ENABLED); + if (null != enabled) { + return Boolean.parseBoolean(enabled); + } else { + return DEFAULT_TRACE_OTEL_ENABLED; + } + } + + /** + * Gets an OpenTelemetry property. + * + *

Checks system properties, environment variables, and the optional OpenTelemetry config file. + * If the equivalent Datadog property is also set then log a warning and return {@code null}. + */ + private String getOtelProperty(String otelSysProp, String ddSysProp) { + String otelValue = getOtelProperty(otelSysProp); + if (null == otelValue) { + return null; + } + String ddValue = getProperty(ddSysProp); + if (null != ddValue) { + String otelEnvVar = Strings.toEnvVar(otelSysProp); + log.warn( + "Both {} and {} are set, ignoring {}", + Strings.toEnvVar(ddSysProp), + otelEnvVar, + otelEnvVar); + return null; + } + return otelValue; + } + + /** + * Gets an OpenTelemetry property. + * + *

Checks system properties, environment variables, and the optional OpenTelemetry config file. + */ + private String getOtelProperty(String sysProp) { + String value = getProperty(sysProp); + if (null == value && null != otelConfigFile) { + value = otelConfigFile.getProperty(sysProp); + } + return value; + } + + /** + * Gets a general property. + * + *

Checks system properties and environment variables. + */ + private static String getProperty(String sysProp) { + String value = System.getProperty(sysProp); + if (null == value) { + value = System.getenv(Strings.toEnvVar(sysProp)); + } + return value; + } + + /** Captures a mapped OpenTelemetry property. */ + private void capture(String key, String value) { + if (null != value) { + otelEnvironment.put(key, value); + } + } + + /** Loads the optional OpenTelemetry configuration file. */ + private static Properties loadOtelConfigFile() { + String path = getProperty("otel.javaagent.configuration-file"); + if (null != path && !path.isEmpty()) { + if (path.charAt(0) == '~') { + path = System.getProperty("user.home") + path.substring(1); + } + File file = new File(path); + if (file.exists()) { + try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { + Properties properties = new Properties(); + properties.load(in); + return properties; + } catch (Throwable e) { + log.warn("Problem reading OTEL_JAVAAGENT_CONFIGURATION_FILE {} - {}", path, e.toString()); + } + } + } + return null; + } + + /** Parses a comma-separated list of items. */ + private static List parseOtelList(String value) { + List list = new ArrayList<>(); + int start = 0; + while (start < value.length()) { + int end = value.indexOf(',', start); + if (end < 0) { + end = value.length(); + } + if (end > start) { + list.add(value.substring(start, end)); + } + start = end + 1; + } + return list; + } + + /** Parses a comma-separated list of key=value entries. */ + private static Map parseOtelMap(String value) { + Map map = new LinkedHashMap<>(); + int start = 0; + while (start < value.length()) { + int end = value.indexOf(',', start); + if (end < 0) { + end = value.length(); + } + if (end > start) { + String entry = value.substring(start, end); + int eq = entry.indexOf('='); + if (eq > 0) { + map.put(entry.substring(0, eq), entry.substring(eq + 1)); + } + } + start = end + 1; + } + return map; + } + + /** Renders the map as a comma-separated list of key:value entries. */ + private static String renderDatadogMap(Map map, int maxEntries) { + StringBuilder buf = new StringBuilder(); + + int entries = 0; + for (Map.Entry entry : map.entrySet()) { + buf.append(entry.getKey()).append(':').append(entry.getValue()).append(','); + if (++entries >= maxEntries) { + break; + } + } + + // remove trailing comma, mapping empty conversion to null + return buf.length() > 1 ? buf.substring(0, buf.length() - 1) : null; + } + + /** Maps OpenTelemetry propagators to a list of accepted propagation styles. */ + private static String mapPropagationStyle(String propagators) { + if (null == propagators) { + return null; + } + + StringBuilder buf = new StringBuilder(); + for (String style : parseOtelList(propagators)) { + if ("b3".equalsIgnoreCase(style)) { + buf.append("b3single,"); // force use of OpenTelemetry alias + } else { + try { + buf.append(TracePropagationStyle.valueOfDisplayName(style)).append(','); + } catch (IllegalArgumentException e) { + log.warn("OTEL_PROPAGATORS={} is not supported", style); + } + } + } + + // remove trailing comma, mapping empty conversion to null + return buf.length() > 1 ? buf.substring(0, buf.length() - 1) : null; + } + + /** Maps known OpenTelemetry samplers to a trace sample rate. */ + private String mapSampleRate(String tracesSampler) { + if (null == tracesSampler) { + return null; + } + + if ("traceidratio".equalsIgnoreCase(tracesSampler) + || "always_on".equalsIgnoreCase(tracesSampler) + || "always_off".equalsIgnoreCase(tracesSampler)) { + log.warn( + "OTEL_TRACES_SAMPLER changed from {} to parentbased_{}; only parent based sampling is supported.", + tracesSampler, + tracesSampler); + tracesSampler = "parentbased_" + tracesSampler; + } + + if ("parentbased_traceidratio".equalsIgnoreCase(tracesSampler)) { + return getOtelProperty("otel.traces.sampler.arg"); + } else if ("parentbased_always_on".equalsIgnoreCase(tracesSampler)) { + return "1.0"; + } else if ("parentbased_always_off".equalsIgnoreCase(tracesSampler)) { + return "0.0"; + } + + log.warn("OTEL_TRACES_SAMPLER={} is not supported", tracesSampler); + return null; + } + + /** Maps an OpenTelemetry exporter setting to the equivalent Datadog collection setting. */ + private String mapDataCollection(String type) { + String exporter = getOtelProperty("otel." + type + ".exporter"); + if (null == exporter) { + return null; + } + + if ("none".equalsIgnoreCase(exporter)) { + return "false"; // currently we only accept "none" which maps to disable data collection + } + + log.warn("OTEL_{}_EXPORTER={} is not supported", type, exporter.toUpperCase(Locale.ROOT)); + return null; + } + + /** Merges the OpenTelemetry client and server headers to capture into a single list. */ + private String getOtelHeaders(String otelSuffix, String ddSysProp) { + String clientTags = + getOtelProperty("otel.instrumentation.http.client.capture-" + otelSuffix, ddSysProp); + String serverTags = + getOtelProperty("otel.instrumentation.http.server.capture-" + otelSuffix, ddSysProp); + if (null == clientTags) { + return serverTags; + } else if (null == serverTags) { + return clientTags; + } else { + return clientTags + ',' + serverTags; + } + } + + /** + * Maps OpenTelemetry list of headers to Datadog header tags, preserving the expected tag name. + */ + private static String mapHeaderTags(String tagPrefix, String headers) { + if (null == headers) { + return null; + } + + StringBuilder buf = new StringBuilder(); + for (String header : parseOtelList(headers)) { + buf.append(header).append(':').append(tagPrefix).append(header).append(','); + } + + // remove trailing comma, mapping empty conversion to null + return buf.length() > 1 ? buf.substring(0, buf.length() - 1) : null; + } +} diff --git a/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy b/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy new file mode 100644 index 00000000000..25b55d2518b --- /dev/null +++ b/internal-api/src/test/groovy/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSourceTest.groovy @@ -0,0 +1,295 @@ +package datadog.trace.bootstrap.config.provider + +import datadog.trace.test.util.DDSpecification +import spock.lang.Ignore + +import static datadog.trace.api.config.GeneralConfig.ENV +import static datadog.trace.api.config.GeneralConfig.LOG_LEVEL +import static datadog.trace.api.config.GeneralConfig.RUNTIME_METRICS_ENABLED +import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME +import static datadog.trace.api.config.GeneralConfig.TAGS +import static datadog.trace.api.config.GeneralConfig.VERSION +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_ENABLED +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_EXTENSIONS_PATH +import static datadog.trace.api.config.TraceInstrumentationConfig.TRACE_OTEL_ENABLED +import static datadog.trace.api.config.TracerConfig.REQUEST_HEADER_TAGS +import static datadog.trace.api.config.TracerConfig.RESPONSE_HEADER_TAGS +import static datadog.trace.api.config.TracerConfig.TRACE_PROPAGATION_STYLE +import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE + +class OtelEnvironmentConfigSourceTest extends DDSpecification { + + // ignore this test when we enable the OpenTelemetry integration by default + def "no otel system properties are mapped by default"() { + setup: + injectSysConfig('otel.service.name', 'TEST_SERVICE', false) + injectSysConfig('otel.propagators', 'xray,b3,datadog', false) + injectSysConfig('otel.traces.sampler', 'parentbased_traceidratio', false) + injectSysConfig('otel.traces.sampler.arg', '0.5', false) + injectSysConfig('otel.traces.exporter', 'none', false) + injectSysConfig('otel.metrics.exporter', 'none', false) + injectSysConfig('otel.logs.exporter', 'none', false) + injectSysConfig('otel.resource.attributes', 'service.name=DEV_SERVICE', false) + injectSysConfig('otel.instrumentation.http.client.capture-request-headers', 'content-type', false) + injectSysConfig('otel.instrumentation.http.client.capture-response-headers', 'content-length', false) + injectSysConfig('otel.instrumentation.http.server.capture-request-headers', 'custom-header', false) + injectSysConfig('otel.instrumentation.http.server.capture-response-headers', 'another-header', false) + injectSysConfig('otel.javaagent.extensions', '/opt/opentelemetry/extensions', false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(TRACE_OTEL_ENABLED) == null + source.get(LOG_LEVEL) == null + source.get(SERVICE_NAME) == null + source.get(VERSION) == null + source.get(ENV) == null + source.get(TAGS) == null + source.get(TRACE_PROPAGATION_STYLE) == null + source.get(TRACE_SAMPLE_RATE) == null + source.get(TRACE_ENABLED) == null + source.get(RUNTIME_METRICS_ENABLED) == null + source.get(REQUEST_HEADER_TAGS) == null + source.get(RESPONSE_HEADER_TAGS) == null + source.get(TRACE_EXTENSIONS_PATH) == null + } + + // ignore this test when we enable the OpenTelemetry integration by default + def "no otel environment variables are mapped by default"() { + setup: + injectEnvConfig('OTEL_LOG_LEVEL', 'debug', false) + injectEnvConfig('OTEL_SERVICE_NAME', 'TEST_SERVICE', false) + injectEnvConfig('OTEL_PROPAGATORS', 'xray,b3,datadog', false) + injectEnvConfig('OTEL_TRACES_SAMPLER', 'parentbased_traceidratio', false) + injectEnvConfig('OTEL_TRACES_SAMPLER_ARG', '0.5', false) + injectEnvConfig('OTEL_TRACES_EXPORTER', 'none', false) + injectEnvConfig('OTEL_METRICS_EXPORTER', 'none', false) + injectEnvConfig('OTEL_LOGS_EXPORTER', 'none', false) + injectEnvConfig('OTEL_RESOURCE_ATTRIBUTES', 'service.name=DEV_SERVICE', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_REQUEST_HEADERS', 'content-type', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_RESPONSE_HEADERS', 'content-length', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS', 'custom-header', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_RESPONSE_HEADERS', 'another-header', false) + injectEnvConfig('OTEL_JAVAAGENT_EXTENSIONS', '/opt/opentelemetry/extensions', false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(TRACE_OTEL_ENABLED) == null + source.get(LOG_LEVEL) == null + source.get(SERVICE_NAME) == null + source.get(VERSION) == null + source.get(ENV) == null + source.get(TAGS) == null + source.get(TRACE_PROPAGATION_STYLE) == null + source.get(TRACE_SAMPLE_RATE) == null + source.get(TRACE_ENABLED) == null + source.get(RUNTIME_METRICS_ENABLED) == null + source.get(REQUEST_HEADER_TAGS) == null + source.get(RESPONSE_HEADER_TAGS) == null + source.get(TRACE_EXTENSIONS_PATH) == null + } + + @Ignore // enable this test when we enable the OpenTelemetry integration by default + def "disabling otel with system property disables otel integration"() { + setup: + injectSysConfig('otel.sdk.disabled', 'true', false) + injectSysConfig('otel.service.name', 'TEST_SERVICE', false) + injectSysConfig('otel.propagators', 'xray,b3,datadog', false) + injectSysConfig('otel.traces.sampler', 'parentbased_traceidratio', false) + injectSysConfig('otel.traces.sampler.arg', '0.5', false) + injectSysConfig('otel.traces.exporter', 'none', false) + injectSysConfig('otel.metrics.exporter', 'none', false) + injectSysConfig('otel.logs.exporter', 'none', false) + injectSysConfig('otel.resource.attributes', 'service.name=DEV_SERVICE', false) + injectSysConfig('otel.instrumentation.http.client.capture-request-headers', 'content-type', false) + injectSysConfig('otel.instrumentation.http.client.capture-response-headers', 'content-length', false) + injectSysConfig('otel.instrumentation.http.server.capture-request-headers', 'custom-header', false) + injectSysConfig('otel.instrumentation.http.server.capture-response-headers', 'another-header', false) + injectSysConfig('otel.javaagent.extensions', '/opt/opentelemetry/extensions', false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(TRACE_OTEL_ENABLED) == 'false' + source.get(LOG_LEVEL) == null + source.get(SERVICE_NAME) == null + source.get(VERSION) == null + source.get(ENV) == null + source.get(TAGS) == null + source.get(TRACE_PROPAGATION_STYLE) == null + source.get(TRACE_SAMPLE_RATE) == null + source.get(TRACE_ENABLED) == null + source.get(RUNTIME_METRICS_ENABLED) == null + source.get(REQUEST_HEADER_TAGS) == null + source.get(RESPONSE_HEADER_TAGS) == null + source.get(TRACE_EXTENSIONS_PATH) == null + } + + @Ignore // enable this test when we enable the OpenTelemetry integration by default + def "disabling otel with environment variable disables otel integration"() { + setup: + injectEnvConfig('OTEL_SDK_DISABLED', 'true', false) + injectEnvConfig('OTEL_LOG_LEVEL', 'debug', false) + injectEnvConfig('OTEL_SERVICE_NAME', 'TEST_SERVICE', false) + injectEnvConfig('OTEL_PROPAGATORS', 'xray,b3,datadog', false) + injectEnvConfig('OTEL_TRACES_SAMPLER', 'parentbased_traceidratio', false) + injectEnvConfig('OTEL_TRACES_SAMPLER_ARG', '0.5', false) + injectEnvConfig('OTEL_TRACES_EXPORTER', 'none', false) + injectEnvConfig('OTEL_METRICS_EXPORTER', 'none', false) + injectEnvConfig('OTEL_LOGS_EXPORTER', 'none', false) + injectEnvConfig('OTEL_RESOURCE_ATTRIBUTES', 'service.name=DEV_SERVICE', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_REQUEST_HEADERS', 'content-type', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_RESPONSE_HEADERS', 'content-length', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS', 'custom-header', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_RESPONSE_HEADERS', 'another-header', false) + injectEnvConfig('OTEL_JAVAAGENT_EXTENSIONS', '/opt/opentelemetry/extensions', false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(TRACE_OTEL_ENABLED) == 'false' + source.get(LOG_LEVEL) == null + source.get(SERVICE_NAME) == null + source.get(VERSION) == null + source.get(ENV) == null + source.get(TAGS) == null + source.get(TRACE_PROPAGATION_STYLE) == null + source.get(TRACE_SAMPLE_RATE) == null + source.get(TRACE_ENABLED) == null + source.get(RUNTIME_METRICS_ENABLED) == null + source.get(REQUEST_HEADER_TAGS) == null + source.get(RESPONSE_HEADER_TAGS) == null + source.get(TRACE_EXTENSIONS_PATH) == null + } + + def "otel system properties are mapped when otel is enabled"() { + setup: + injectSysConfig('dd.trace.otel.enabled', 'true', false) + injectSysConfig('otel.service.name', 'TEST_SERVICE', false) + injectSysConfig('otel.propagators', 'xray,b3,datadog', false) + injectSysConfig('otel.traces.sampler', 'parentbased_traceidratio', false) + injectSysConfig('otel.traces.sampler.arg', '0.5', false) + injectSysConfig('otel.traces.exporter', 'none', false) + injectSysConfig('otel.metrics.exporter', 'none', false) + injectSysConfig('otel.logs.exporter', 'none', false) + injectSysConfig('otel.resource.attributes', 'service.name=DEV_SERVICE', false) + injectSysConfig('otel.instrumentation.http.client.capture-request-headers', 'content-type', false) + injectSysConfig('otel.instrumentation.http.client.capture-response-headers', 'content-length', false) + injectSysConfig('otel.instrumentation.http.server.capture-request-headers', 'custom-header', false) + injectSysConfig('otel.instrumentation.http.server.capture-response-headers', 'another-header', false) + injectSysConfig('otel.javaagent.extensions', '/opt/opentelemetry/extensions', false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(SERVICE_NAME) == 'TEST_SERVICE' // value from otel.service.name overrides the one from otel.resource.attributes + source.get(TRACE_PROPAGATION_STYLE) == 'xray,b3single,datadog' + source.get(TRACE_SAMPLE_RATE) == '0.5' + source.get(TRACE_ENABLED) == 'false' + source.get(RUNTIME_METRICS_ENABLED) == 'false' + source.get(REQUEST_HEADER_TAGS) == 'content-type:http.request.header.content-type,custom-header:http.request.header.custom-header' + source.get(RESPONSE_HEADER_TAGS) == 'content-length:http.response.header.content-length,another-header:http.response.header.another-header' + source.get(TRACE_EXTENSIONS_PATH) == '/opt/opentelemetry/extensions' + } + + def "otel environment variables are mapped when otel is enabled"() { + setup: + injectEnvConfig('DD_TRACE_OTEL_ENABLED', 'true', false) + injectEnvConfig('OTEL_LOG_LEVEL', 'debug', false) + injectEnvConfig('OTEL_SERVICE_NAME', 'TEST_SERVICE', false) + injectEnvConfig('OTEL_PROPAGATORS', 'xray,b3,datadog', false) + injectEnvConfig('OTEL_TRACES_SAMPLER', 'parentbased_traceidratio', false) + injectEnvConfig('OTEL_TRACES_SAMPLER_ARG', '0.5', false) + injectEnvConfig('OTEL_TRACES_EXPORTER', 'none', false) + injectEnvConfig('OTEL_METRICS_EXPORTER', 'none', false) + injectEnvConfig('OTEL_LOGS_EXPORTER', 'none', false) + injectEnvConfig('OTEL_RESOURCE_ATTRIBUTES', 'service.name=DEV_SERVICE', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_REQUEST_HEADERS', 'content-type', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_RESPONSE_HEADERS', 'content-length', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS', 'custom-header', false) + injectEnvConfig('OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_RESPONSE_HEADERS', 'another-header', false) + injectEnvConfig('OTEL_JAVAAGENT_EXTENSIONS', '/opt/opentelemetry/extensions', false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(LOG_LEVEL) == 'debug' + source.get(SERVICE_NAME) == 'TEST_SERVICE' // value from OTEL_SERVICE_NAME overrides the one from OTEL_RESOURCE_ATTRIBUTES + source.get(TRACE_PROPAGATION_STYLE) == 'xray,b3single,datadog' + source.get(TRACE_SAMPLE_RATE) == '0.5' + source.get(TRACE_ENABLED) == 'false' + source.get(RUNTIME_METRICS_ENABLED) == 'false' + source.get(REQUEST_HEADER_TAGS) == 'content-type:http.request.header.content-type,custom-header:http.request.header.custom-header' + source.get(RESPONSE_HEADER_TAGS) == 'content-length:http.response.header.content-length,another-header:http.response.header.another-header' + source.get(TRACE_EXTENSIONS_PATH) == '/opt/opentelemetry/extensions' + } + + def "otel resource attributes system property is mapped"() { + setup: + injectSysConfig('dd.trace.otel.enabled', 'true', false) + injectSysConfig('otel.resource.attributes', + 'key1=one,' + + 'key2=two,' + + 'key3=three,' + + 'service.name=DEV_SERVICE,' + + 'key4=four,' + + 'key5=five,' + + 'key6=six,' + + 'deployment.environment=staging,' + + 'key7=seven,' + + 'key8=eight,' + + 'key9=nine,' + + 'service.version=42,'+ + 'key10=ten,'+ + 'key11=eleven,'+ + 'key12=twelve', false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(SERVICE_NAME) == 'DEV_SERVICE' + source.get(ENV) == 'staging' + source.get(VERSION) == '42' + // only the first 10 custom attributes are mapped to tags + source.get(TAGS) == 'key1:one,key2:two,key3:three,key4:four,key5:five,key6:six,key7:seven,key8:eight,key9:nine,key10:ten' + } + + def "otel resource attributes environment variable is mapped"() { + setup: + injectEnvConfig('DD_TRACE_OTEL_ENABLED', 'true', false) + injectEnvConfig('OTEL_RESOURCE_ATTRIBUTES', + 'key1=one,' + + 'key2=two,' + + 'key3=three,' + + 'service.name=DEV_SERVICE,' + + 'key4=four,' + + 'key5=five,' + + 'key6=six,' + + 'deployment.environment=staging,' + + 'key7=seven,' + + 'key8=eight,' + + 'key9=nine,' + + 'service.version=42,'+ + 'key10=ten,'+ + 'key11=eleven,'+ + 'key12=twelve', false) + + when: + def source = new OtelEnvironmentConfigSource() + + then: + source.get(SERVICE_NAME) == 'DEV_SERVICE' + source.get(ENV) == 'staging' + source.get(VERSION) == '42' + // only the first 10 custom attributes are mapped to tags + source.get(TAGS) == 'key1:one,key2:two,key3:three,key4:four,key5:five,key6:six,key7:seven,key8:eight,key9:nine,key10:ten' + } +} From 1fdd7039a5de984e55ee2ec04ab5c7c6ce7b1926 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Thu, 13 Jun 2024 14:51:24 +0100 Subject: [PATCH 2/3] Check optional Datadog configuration file for Datadog properties --- .../config/provider/ConfigProvider.java | 6 ++-- .../provider/OtelEnvironmentConfigSource.java | 34 ++++++++++++++++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/ConfigProvider.java b/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/ConfigProvider.java index 110731a313e..8a3d910a5f1 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/ConfigProvider.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/ConfigProvider.java @@ -364,7 +364,7 @@ public static ConfigProvider createDefault() { new SystemPropertiesConfigSource(), new EnvironmentConfigSource(), new PropertiesConfigSource(configProperties, true), - new OtelEnvironmentConfigSource(), + new OtelEnvironmentConfigSource(configProperties), new CapturedEnvironmentConfigSource()); } } @@ -387,7 +387,7 @@ public static ConfigProvider withoutCollector() { new SystemPropertiesConfigSource(), new EnvironmentConfigSource(), new PropertiesConfigSource(configProperties, true), - new OtelEnvironmentConfigSource(), + new OtelEnvironmentConfigSource(configProperties), new CapturedEnvironmentConfigSource()); } } @@ -413,7 +413,7 @@ public static ConfigProvider withPropertiesOverride(Properties properties) { new SystemPropertiesConfigSource(), new EnvironmentConfigSource(), new PropertiesConfigSource(configProperties, true), - new OtelEnvironmentConfigSource(), + new OtelEnvironmentConfigSource(configProperties), new CapturedEnvironmentConfigSource()); } } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java b/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java index 61ee7fdcefc..6c490d7716b 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/config/provider/OtelEnvironmentConfigSource.java @@ -40,6 +40,8 @@ final class OtelEnvironmentConfigSource extends ConfigProvider.Source { private final Properties otelConfigFile = loadOtelConfigFile(); + private final Properties datadogConfigFile; + @Override protected String get(String key) { return otelEnvironment.get(key); @@ -51,9 +53,18 @@ public ConfigOrigin origin() { } OtelEnvironmentConfigSource() { - if (!traceOtelEnabled()) { - return; + this(null); + } + + OtelEnvironmentConfigSource(Properties datadogConfigFile) { + this.datadogConfigFile = datadogConfigFile; + + if (traceOtelEnabled()) { + setupOteEnvironment(); } + } + + private void setupOteEnvironment() { // only applies when OTEL is enabled by default (otherwise TRACE_OTEL_ENABLED takes precedence) String sdkDisabled = getOtelProperty("otel.sdk.disabled", "dd." + TRACE_OTEL_ENABLED); @@ -94,8 +105,8 @@ public ConfigOrigin origin() { capture(TRACE_EXTENSIONS_PATH, extensions); } - private static boolean traceOtelEnabled() { - String enabled = getProperty("dd." + TRACE_OTEL_ENABLED); + private boolean traceOtelEnabled() { + String enabled = getDatadogProperty("dd." + TRACE_OTEL_ENABLED); if (null != enabled) { return Boolean.parseBoolean(enabled); } else { @@ -114,7 +125,7 @@ private String getOtelProperty(String otelSysProp, String ddSysProp) { if (null == otelValue) { return null; } - String ddValue = getProperty(ddSysProp); + String ddValue = getDatadogProperty(ddSysProp); if (null != ddValue) { String otelEnvVar = Strings.toEnvVar(otelSysProp); log.warn( @@ -140,6 +151,19 @@ private String getOtelProperty(String sysProp) { return value; } + /** + * Gets a Datadog property. + * + *

Checks system properties, environment variables, and the optional Datadog config file. + */ + private String getDatadogProperty(String sysProp) { + String value = getProperty(sysProp); + if (null == value && null != datadogConfigFile) { + value = datadogConfigFile.getProperty(sysProp); + } + return value; + } + /** * Gets a general property. * From 138a373b38c5dad1141aab4bafdbbd70106dbcf9 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Thu, 13 Jun 2024 15:11:32 +0100 Subject: [PATCH 3/3] Register OtelEnvironmentConfigSource with GraalVM for build-time --- .../nativeimage/NativeImageGeneratorRunnerInstrumentation.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java index 319f6b12fc4..7ccbc1f0c52 100644 --- a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java +++ b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java @@ -82,6 +82,7 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) String[ + "datadog.trace.bootstrap.config.provider.ConfigConverter:build_time," + "datadog.trace.bootstrap.config.provider.ConfigProvider:build_time," + "datadog.trace.bootstrap.config.provider.ConfigProvider$Singleton:build_time," + + "datadog.trace.bootstrap.config.provider.OtelEnvironmentConfigSource:build_time," + "datadog.trace.bootstrap.Agent:build_time," + "datadog.trace.bootstrap.BootstrapProxy:build_time," + "datadog.trace.bootstrap.CallDepthThreadLocalMap:build_time,"