From 556c3b5c94300f395296bb47265021f9ada8ed4d Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Fri, 27 Mar 2026 16:42:04 -0400 Subject: [PATCH 01/12] WIP feat(otel): add support for opentelemetry logs --- appsec/third_party/libddwaf | 2 +- ext/autoload_php_files.c | 2 +- ext/configuration.h | 1 + ext/otel_config.c | 12 + ext/otel_config.h | 1 + libdatadog | 2 +- .../OpenTelemetry/CompositeResolver.php | 98 ++++++--- src/DDTrace/OpenTelemetry/Configuration.php | 11 + src/DDTrace/OpenTelemetry/OtlpHooks.php | 79 +++++++ src/api/Log/DatadogLogger.php | 2 +- src/bridge/_files_opentelemetry.php | 1 + tests/OpenTelemetry/Integration/LogsTest.php | 206 ++++++++++++++++++ tests/phpunit.xml | 1 + 13 files changed, 386 insertions(+), 32 deletions(-) create mode 100644 src/DDTrace/OpenTelemetry/OtlpHooks.php create mode 100644 tests/OpenTelemetry/Integration/LogsTest.php diff --git a/appsec/third_party/libddwaf b/appsec/third_party/libddwaf index 2f4aa84cd61..00e895f2c50 160000 --- a/appsec/third_party/libddwaf +++ b/appsec/third_party/libddwaf @@ -1 +1 @@ -Subproject commit 2f4aa84cd61dc13229d1431779c007bf4ebda89c +Subproject commit 00e895f2c507a714062aa88ed41466aec10d2e01 diff --git a/ext/autoload_php_files.c b/ext/autoload_php_files.c index 23d786c862f..246008d4c19 100644 --- a/ext/autoload_php_files.c +++ b/ext/autoload_php_files.c @@ -248,7 +248,7 @@ static zend_class_entry *dd_perform_autoload(zend_string *class_name, zend_strin } } - if ((get_DD_TRACE_OTEL_ENABLED() || get_DD_METRICS_OTEL_ENABLED()) && zend_string_starts_with_literal(lc_name, "opentelemetry\\") && !DDTRACE_G(otel_is_loaded)) { + if ((get_DD_TRACE_OTEL_ENABLED() || get_DD_METRICS_OTEL_ENABLED() || get_DD_LOGS_OTEL_ENABLED()) && zend_string_starts_with_literal(lc_name, "opentelemetry\\") && !DDTRACE_G(otel_is_loaded)) { DDTRACE_G(otel_is_loaded) = 1; #if PHP_VERSION_ID >= 70400 && PHP_VERSION_ID < 80000 dd_prev_ast_process = zend_ast_process; diff --git a/ext/configuration.h b/ext/configuration.h index 599d2ed4eb8..fbf36166d32 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -240,6 +240,7 @@ enum ddtrace_sidecar_connection_mode { CONFIG(BOOL, DD_INTEGRATION_METRICS_ENABLED, "true", \ .env_config_fallback = ddtrace_conf_otel_metrics_exporter) \ CONFIG(BOOL, DD_METRICS_OTEL_ENABLED, "false") \ + CONFIG(BOOL, DD_LOGS_OTEL_ENABLED, "false") \ CONFIG(BOOL, DD_TRACE_OTEL_ENABLED, "false") \ CONFIG(STRING, DD_TRACE_LOG_FILE, "", .ini_change = zai_config_system_ini_change) \ CONFIG(STRING, DD_TRACE_LOG_LEVEL, "error", .ini_change = ddtrace_alter_dd_trace_log_level, \ diff --git a/ext/otel_config.c b/ext/otel_config.c index 43c08c0b465..c1fed78b7e9 100644 --- a/ext/otel_config.c +++ b/ext/otel_config.c @@ -158,6 +158,18 @@ bool ddtrace_conf_otel_metrics_exporter(zai_env_buffer *buf, bool pre_rinit) { return false; } +bool ddtrace_conf_otel_logs_exporter(zai_env_buffer *buf, bool pre_rinit) { + if (get_otel_value((zai_str)ZAI_STRL("OTEL_LOGS_EXPORTER"), buf, pre_rinit)) { + if (strcmp(buf->ptr, "none") == 0) { + buf->ptr = "0"; buf->len = 1; + return true; + } + LOG_ONCE(WARN, "OTEL_LOGS_EXPORTER has invalid value: %s", buf->ptr); + report_otel_cfg_telemetry_invalid("otel_logs_exporter", "dd_logs_otel_enabled", pre_rinit); + } + return false; +} + bool ddtrace_conf_otel_resource_attributes_tags(zai_env_buffer *buf, bool pre_rinit) { ZAI_ENV_BUFFER_INIT(local, ZAI_ENV_MAX_BUFSIZ); if (!get_otel_value((zai_str)ZAI_STRL("OTEL_RESOURCE_ATTRIBUTES"), &local, pre_rinit)) { diff --git a/ext/otel_config.h b/ext/otel_config.h index a7840b6d78e..9db9aeca913 100644 --- a/ext/otel_config.h +++ b/ext/otel_config.h @@ -11,6 +11,7 @@ bool ddtrace_conf_otel_propagators(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_sample_rate(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_traces_exporter(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_metrics_exporter(zai_env_buffer *buf, bool pre_rinit); +bool ddtrace_conf_otel_logs_exporter(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_resource_attributes_tags(zai_env_buffer *buf, bool pre_rinit); #endif // DD_OTEL_CONFIG_H diff --git a/libdatadog b/libdatadog index 561f772f872..629bce09547 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit 561f772f872ed2edc523ffbd6a2f65e7172ab6dd +Subproject commit 629bce09547abc77d7bbda623921f97eb5611949 diff --git a/src/DDTrace/OpenTelemetry/CompositeResolver.php b/src/DDTrace/OpenTelemetry/CompositeResolver.php index 25f5e754ce2..0ba0753b14a 100644 --- a/src/DDTrace/OpenTelemetry/CompositeResolver.php +++ b/src/DDTrace/OpenTelemetry/CompositeResolver.php @@ -19,7 +19,7 @@ class DatadogResolver implements ResolverInterface public function retrieveValue(string $name): mixed { - if (!$this->isMetricsEnabled($name)) { + if (!$this->isSignalEnabled($name)) { return null; } @@ -27,7 +27,9 @@ public function retrieveValue(string $name): mixed return 'delta'; } - if ($name === 'OTEL_EXPORTER_OTLP_ENDPOINT' || $name === 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT') { + if ($name === 'OTEL_EXPORTER_OTLP_ENDPOINT' + || $name === 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT' + || $name === 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') { return $this->resolveEndpoint($name); } @@ -36,66 +38,102 @@ public function retrieveValue(string $name): mixed public function hasVariable(string $variableName): bool { - if ($variableName === 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT' || - $variableName === 'OTEL_EXPORTER_OTLP_ENDPOINT' || - $variableName === 'OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE') { + if ($variableName === 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT' + || $variableName === 'OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE') { return \dd_trace_env_config('DD_METRICS_OTEL_ENABLED'); } + + if ($variableName === 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') { + return \dd_trace_env_config('DD_LOGS_OTEL_ENABLED'); + } + + if ($variableName === 'OTEL_EXPORTER_OTLP_ENDPOINT') { + return \dd_trace_env_config('DD_METRICS_OTEL_ENABLED') + || \dd_trace_env_config('DD_LOGS_OTEL_ENABLED'); + } + return false; } - private function isMetricsEnabled(string $name): bool + private function isSignalEnabled(string $name): bool { - $metricsOnlySettings = [ + if (in_array($name, [ 'OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT', - ]; - - if (in_array($name, $metricsOnlySettings, true)) { + ], true)) { return \dd_trace_env_config('DD_METRICS_OTEL_ENABLED'); } + if ($name === 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') { + return \dd_trace_env_config('DD_LOGS_OTEL_ENABLED'); + } + return true; } private function resolveEndpoint(string $name): string { $isMetricsEndpoint = ($name === 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT'); - $protocol = $this->resolveProtocol($isMetricsEndpoint); + $isLogsEndpoint = ($name === 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT'); + $protocol = $this->resolveProtocol($isMetricsEndpoint, $isLogsEndpoint); - // Check for user-configured general OTLP endpoint (only when requesting metrics endpoint) + // For signal-specific endpoints, check whether the user configured a general OTLP endpoint + // and derive the signal path from it rather than the agent address. if ($isMetricsEndpoint && Configuration::has('OTEL_EXPORTER_OTLP_ENDPOINT')) { - return $this->buildMetricsEndpointFromGeneral($protocol); + return $this->buildSignalEndpointFromGeneral($protocol, Signals::METRICS); + } + + if ($isLogsEndpoint && Configuration::has('OTEL_EXPORTER_OTLP_ENDPOINT')) { + return $this->buildSignalEndpointFromGeneral($protocol, Signals::LOGS); } - return $this->buildEndpointFromAgent($protocol, $isMetricsEndpoint); + return $this->buildEndpointFromAgent($protocol, $name); } - private function resolveProtocol(bool $metricsSpecific): ?string + private function resolveProtocol(bool $metricsSpecific, bool $logsSpecific): string { if ($metricsSpecific && Configuration::has('OTEL_EXPORTER_OTLP_METRICS_PROTOCOL')) { - return Configuration::getEnum('OTEL_EXPORTER_OTLP_METRICS_PROTOCOL'); + return $this->validateProtocol(Configuration::getEnum('OTEL_EXPORTER_OTLP_METRICS_PROTOCOL')); + } + + if ($logsSpecific && Configuration::has('OTEL_EXPORTER_OTLP_LOGS_PROTOCOL')) { + return $this->validateProtocol(Configuration::getEnum('OTEL_EXPORTER_OTLP_LOGS_PROTOCOL')); } - // Call getEnum without has() check to match original behavior - - // allows SDK defaults to be applied if they exist + // Call getEnum without has() check to match original behavior — + // allows SDK defaults to be applied if they exist. $protocol = Configuration::getEnum('OTEL_EXPORTER_OTLP_PROTOCOL'); - return $protocol ?? self::DEFAULT_PROTOCOL; + return $this->validateProtocol($protocol ?? self::DEFAULT_PROTOCOL); } - private function buildMetricsEndpointFromGeneral(string $protocol): string + private function validateProtocol(string $protocol): string + { + static $valid = ['grpc', 'http/protobuf', 'http/json', 'http/ndjson']; + if (!in_array($protocol, $valid, true)) { + trigger_error( + "OTEL_EXPORTER_OTLP_PROTOCOL '$protocol' is not recognized. " + . "Valid values are: grpc, http/protobuf, http/json, http/ndjson. " + . "Falling back to 'http/protobuf'.", + E_USER_WARNING + ); + return self::DEFAULT_PROTOCOL; + } + return $protocol; + } + + private function buildSignalEndpointFromGeneral(string $protocol, string $signal): string { $generalEndpoint = rtrim(Configuration::getString('OTEL_EXPORTER_OTLP_ENDPOINT'), '/'); if ($this->isGrpc($protocol)) { - return $generalEndpoint . OtlpUtil::method(Signals::METRICS); + return $generalEndpoint . OtlpUtil::method($signal); } - return $generalEndpoint . '/v1/metrics'; + return $generalEndpoint . '/v1/' . $signal; } - private function buildEndpointFromAgent(string $protocol, bool $isMetricsEndpoint): string + private function buildEndpointFromAgent(string $protocol, string $endpointName): string { $agentInfo = $this->resolveAgentInfo(); @@ -107,8 +145,12 @@ private function buildEndpointFromAgent(string $protocol, bool $isMetricsEndpoin $port = $this->isGrpc($protocol) ? self::GRPC_PORT : self::HTTP_PORT; $endpoint = $agentInfo['scheme'] . '://' . $agentInfo['host'] . ':' . $port; - if ($isMetricsEndpoint) { - return $this->appendMetricsPath($endpoint, $protocol); + if ($endpointName === 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT') { + return $this->appendSignalPath($endpoint, $protocol, Signals::METRICS); + } + + if ($endpointName === 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT') { + return $this->appendSignalPath($endpoint, $protocol, Signals::LOGS); } return $endpoint; @@ -156,13 +198,13 @@ private function resolveAgentInfo(): array return ['scheme' => $scheme, 'host' => $host]; } - private function appendMetricsPath(string $endpoint, string $protocol): string + private function appendSignalPath(string $endpoint, string $protocol, string $signal): string { if ($this->isGrpc($protocol)) { - return $endpoint . OtlpUtil::method(Signals::METRICS); + return $endpoint . OtlpUtil::method($signal); } - return $endpoint . '/v1/metrics'; + return $endpoint . '/v1/' . $signal; } private function isGrpc(string $protocol): bool diff --git a/src/DDTrace/OpenTelemetry/Configuration.php b/src/DDTrace/OpenTelemetry/Configuration.php index 0aaf52703c6..26f0d8ae9e6 100644 --- a/src/DDTrace/OpenTelemetry/Configuration.php +++ b/src/DDTrace/OpenTelemetry/Configuration.php @@ -9,14 +9,25 @@ 'OTEL_METRIC_EXPORT_INTERVAL', 'OTEL_METRIC_EXPORT_TIMEOUT', + // OpenTelemetry Logs SDK Configurations + 'OTEL_LOGS_EXPORTER', + 'OTEL_BLRP_SCHEDULE_DELAY', + 'OTEL_BLRP_MAX_QUEUE_SIZE', + 'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE', + 'OTEL_BLRP_EXPORT_TIMEOUT', + // OTLP Exporter Configurations 'OTEL_EXPORTER_OTLP_METRICS_PROTOCOL', + 'OTEL_EXPORTER_OTLP_LOGS_PROTOCOL', 'OTEL_EXPORTER_OTLP_PROTOCOL', 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT', + 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', 'OTEL_EXPORTER_OTLP_METRICS_HEADERS', + 'OTEL_EXPORTER_OTLP_LOGS_HEADERS', 'OTEL_EXPORTER_OTLP_HEADERS', 'OTEL_EXPORTER_OTLP_METRICS_TIMEOUT', + 'OTEL_EXPORTER_OTLP_LOGS_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', 'OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', ]; diff --git a/src/DDTrace/OpenTelemetry/OtlpHooks.php b/src/DDTrace/OpenTelemetry/OtlpHooks.php new file mode 100644 index 00000000000..96f26d5f4db --- /dev/null +++ b/src/DDTrace/OpenTelemetry/OtlpHooks.php @@ -0,0 +1,79 @@ +returned; + if (!($future instanceof \OpenTelemetry\SDK\Common\Future\ErrorFuture)) { + return; + } + + try { + $r = new \ReflectionProperty(\OpenTelemetry\SDK\Common\Future\ErrorFuture::class, 'throwable'); + $r->setAccessible(true); + $e = $r->getValue($future); + } catch (\Throwable $ignored) { + return; + } + + if (!($e instanceof \RuntimeException) || $e->getCode() !== 404) { + return; + } + + static $warned404 = false; + if (!$warned404) { + $warned404 = true; + trigger_error( + 'Datadog OpenTelemetry OTLP export received HTTP 404 Not Found. ' + . 'Ensure Datadog Agent >= 7.48.0 is running and configured to accept OTLP data ' + . '(set DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT or equivalent).', + E_USER_WARNING + ); + } + } +); + +// Protocol fallback ------------------------------------------------------- +// The OTel SDK's Protocols::contentType() calls validate() which throws +// UnexpectedValueException for unknown protocols. We replace an invalid value +// with 'http/protobuf' before the method body runs, emitting a one-time +// warning so the user knows their configuration was ignored. +\DDTrace\install_hook( + 'OpenTelemetry\Contrib\Otlp\Protocols::contentType', + function (\DDTrace\HookData $hook) { + $protocol = $hook->args[0] ?? null; + if ($protocol === null) { + return; + } + + static $valid = ['grpc', 'http/protobuf', 'http/json', 'http/ndjson']; + if (in_array($protocol, $valid, true)) { + return; + } + + static $warnedProtocol = false; + if (!$warnedProtocol) { + $warnedProtocol = true; + trigger_error( + "OpenTelemetry OTLP protocol '$protocol' is not recognized. " + . "Valid values are: grpc, http/protobuf, http/json, http/ndjson. " + . "Falling back to 'http/protobuf'.", + E_USER_WARNING + ); + } + + $hook->args[0] = 'http/protobuf'; + } +); diff --git a/src/api/Log/DatadogLogger.php b/src/api/Log/DatadogLogger.php index da46d599be6..5fa8bf6beac 100644 --- a/src/api/Log/DatadogLogger.php +++ b/src/api/Log/DatadogLogger.php @@ -160,7 +160,7 @@ private static function format(string $level, $message, array $context = []): st private static function handleLogInjection(): array { $logInjection = \dd_trace_env_config('DD_LOGS_INJECTION'); - if ($logInjection) { + if ($logInjection && !\dd_trace_env_config('DD_LOGS_OTEL_ENABLED')) { $traceId = \DDTrace\logs_correlation_trace_id(); $spanId = \dd_trace_peek_span_id(); if ($traceId && $spanId) { diff --git a/src/bridge/_files_opentelemetry.php b/src/bridge/_files_opentelemetry.php index bd218ea8029..799c8311d4d 100644 --- a/src/bridge/_files_opentelemetry.php +++ b/src/bridge/_files_opentelemetry.php @@ -9,6 +9,7 @@ __DIR__ . '/../DDTrace/OpenTelemetry/CachedInstrumentation.php', __DIR__ . '/../DDTrace/OpenTelemetry/CompositeResolver.php', __DIR__ . '/../DDTrace/OpenTelemetry/Configuration.php', + __DIR__ . '/../DDTrace/OpenTelemetry/OtlpHooks.php', __DIR__ . '/../DDTrace/OpenTelemetry/Detectors/DetectorHelper.php', __DIR__ . '/../DDTrace/OpenTelemetry/Detectors/Environment.php', __DIR__ . '/../DDTrace/OpenTelemetry/Detectors/Host.php', diff --git a/tests/OpenTelemetry/Integration/LogsTest.php b/tests/OpenTelemetry/Integration/LogsTest.php new file mode 100644 index 00000000000..68d5277000d --- /dev/null +++ b/tests/OpenTelemetry/Integration/LogsTest.php @@ -0,0 +1,206 @@ += 1; + } + + private static function hasExportersInstalled(): bool + { + return class_exists('OpenTelemetry\Contrib\Otlp\OtlpUtil'); + } + + protected function ddSetUp(): void + { + \dd_trace_serialize_closed_spans(); + parent::ddSetUp(); + } + + public function ddTearDown(): void + { + if (class_exists(ContextStorage::class)) { + Context::setStorage(new ContextStorage()); // Reset OpenTelemetry context + } + parent::ddTearDown(); + self::putEnv("DD_LOGS_OTEL_ENABLED="); + self::putEnv("DD_TRACE_GENERATE_ROOT_SPAN="); + \dd_trace_serialize_closed_spans(); + } + + /** + * Test that the OpenTelemetry SDK classes exist when the SDK is installed + */ + public function testOtelSdkClassesExist() + { + if (!self::isOtelVersionSupported()) { + $this->markTestSkipped('OpenTelemetry version 1.0 or higher is required for these tests'); + } + + $this->assertTrue( + class_exists('OpenTelemetry\SDK\Logs\LoggerProvider'), + 'OpenTelemetry SDK LoggerProvider should be available' + ); + + $this->assertTrue( + class_exists('OpenTelemetry\SDK\Resource\ResourceInfo'), + 'OpenTelemetry SDK ResourceInfo should be available' + ); + } + + /** + * Test that the OTLP logs exporter is available when the contrib package is installed + */ + public function testOtelLogsExporterInstalled() + { + if (!self::isOtelVersionSupported()) { + $this->markTestSkipped('OpenTelemetry version 1.0 or higher is required for these tests'); + } + + if (!self::hasExportersInstalled()) { + $this->markTestSkipped('Tests only compatible with the opentelemetry exporters installed'); + } + + $this->assertTrue( + class_exists('OpenTelemetry\Contrib\Otlp\LogsExporter'), + 'OTLP LogsExporter should be available' + ); + + $this->assertTrue( + class_exists('OpenTelemetry\Contrib\Otlp\OtlpUtil'), + 'OtlpUtil should be available for endpoint configuration' + ); + } + + /** + * Test that the OpenTelemetry LoggerProvider is accessible when DD_LOGS_OTEL_ENABLED is set + */ + public function testOtelLogsEnabled() + { + if (!self::isOtelVersionSupported()) { + $this->markTestSkipped('OpenTelemetry version 1.0 or higher is required for these tests'); + } + + if (!self::hasExportersInstalled()) { + $this->markTestSkipped('Tests only compatible with the opentelemetry exporters installed'); + } + + self::putEnvAndReloadConfig(['DD_LOGS_OTEL_ENABLED=true']); + + $loggerProvider = \OpenTelemetry\API\Globals::loggerProvider(); + + $this->assertNotNull( + $loggerProvider, + 'OpenTelemetry logger provider should be available when DD_LOGS_OTEL_ENABLED is set' + ); + } + + /** + * Test that the LoggerProvider is a proxy/noop when DD_LOGS_OTEL_ENABLED is not set + * @dataProvider disabledLogsProvider + */ + public function testOtelLogsDisabledAndUnset(?string $envValue) + { + if (!self::isOtelVersionSupported()) { + $this->markTestSkipped('OpenTelemetry version 1.0 or higher is required for these tests'); + } + + if (!self::hasExportersInstalled()) { + $this->markTestSkipped('Tests only compatible with the opentelemetry exporters installed'); + } + + if ($envValue === null) { + self::putEnv("DD_LOGS_OTEL_ENABLED="); + } else { + self::putEnvAndReloadConfig(["DD_LOGS_OTEL_ENABLED=$envValue"]); + } + + // Get the logger provider — should be a proxy/noop when not enabled + $loggerProvider = \OpenTelemetry\API\Globals::loggerProvider(); + + $providerClass = get_class($loggerProvider); + $isProxyOrNoop = ( + $loggerProvider === null || + strpos($providerClass, 'Proxy') !== false || + strpos($providerClass, 'Noop') !== false + ); + + $this->assertTrue( + $isProxyOrNoop, + "OpenTelemetry logs provider should not be auto-configured when DD_LOGS_OTEL_ENABLED is '$envValue'. Got: $providerClass" + ); + } + + public static function disabledLogsProvider(): array + { + return [ + 'unset' => [null], + 'false' => ['false'], + ]; + } + + /** + * Test that DD_LOGS_OTEL_ENABLED configuration option is recognized + */ + public function testDdLogsOtelEnabledConfigExists() + { + self::putEnvAndReloadConfig(['DD_LOGS_OTEL_ENABLED=true']); + $this->assertTrue( + \dd_trace_env_config('DD_LOGS_OTEL_ENABLED'), + 'DD_LOGS_OTEL_ENABLED should be true when set' + ); + + self::putEnvAndReloadConfig(['DD_LOGS_OTEL_ENABLED=false']); + $this->assertFalse( + \dd_trace_env_config('DD_LOGS_OTEL_ENABLED'), + 'DD_LOGS_OTEL_ENABLED should be false when set to false' + ); + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 986e2fb0e9b..cba0290d815 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -95,6 +95,7 @@ ./OpenTelemetry/Integration/InteroperabilityTest.php ./OpenTelemetry/Integration/InternalTelemetryTest.php ./OpenTelemetry/Integration/MetricsTest.php + ./OpenTelemetry/Integration/LogsTest.php ./Integrations/Slim/V3_12 From c5de9b515bad6e8a97c58b43f0f9a9d24f016313 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Fri, 27 Mar 2026 16:55:18 -0400 Subject: [PATCH 02/12] revert submodule bumps to origin/master --- appsec/third_party/libddwaf | 2 +- libdatadog | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/appsec/third_party/libddwaf b/appsec/third_party/libddwaf index 00e895f2c50..2f4aa84cd61 160000 --- a/appsec/third_party/libddwaf +++ b/appsec/third_party/libddwaf @@ -1 +1 @@ -Subproject commit 00e895f2c507a714062aa88ed41466aec10d2e01 +Subproject commit 2f4aa84cd61dc13229d1431779c007bf4ebda89c diff --git a/libdatadog b/libdatadog index 629bce09547..561f772f872 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit 629bce09547abc77d7bbda623921f97eb5611949 +Subproject commit 561f772f872ed2edc523ffbd6a2f65e7172ab6dd From 5e7dc320ef5d1a4e92f9903f195fc5c6c6839ae7 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Sat, 28 Mar 2026 17:03:43 -0400 Subject: [PATCH 03/12] update hostname config --- .../OpenTelemetry/CompositeResolver.php | 10 +++++++++ .../OpenTelemetry/Detectors/Environment.php | 2 +- src/DDTrace/OpenTelemetry/Detectors/Host.php | 22 +++++++++++++------ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/DDTrace/OpenTelemetry/CompositeResolver.php b/src/DDTrace/OpenTelemetry/CompositeResolver.php index 0ba0753b14a..c225fe5e11e 100644 --- a/src/DDTrace/OpenTelemetry/CompositeResolver.php +++ b/src/DDTrace/OpenTelemetry/CompositeResolver.php @@ -19,6 +19,11 @@ class DatadogResolver implements ResolverInterface public function retrieveValue(string $name): mixed { + if ($name === 'OTEL_PHP_AUTOLOAD_ENABLED') { + return \dd_trace_env_config('DD_METRICS_OTEL_ENABLED') + || \dd_trace_env_config('DD_LOGS_OTEL_ENABLED'); + } + if (!$this->isSignalEnabled($name)) { return null; } @@ -38,6 +43,11 @@ public function retrieveValue(string $name): mixed public function hasVariable(string $variableName): bool { + if ($variableName === 'OTEL_PHP_AUTOLOAD_ENABLED') { + return \dd_trace_env_config('DD_METRICS_OTEL_ENABLED') + || \dd_trace_env_config('DD_LOGS_OTEL_ENABLED'); + } + if ($variableName === 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT' || $variableName === 'OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE') { return \dd_trace_env_config('DD_METRICS_OTEL_ENABLED'); diff --git a/src/DDTrace/OpenTelemetry/Detectors/Environment.php b/src/DDTrace/OpenTelemetry/Detectors/Environment.php index cf8d74fb4e1..71873127104 100644 --- a/src/DDTrace/OpenTelemetry/Detectors/Environment.php +++ b/src/DDTrace/OpenTelemetry/Detectors/Environment.php @@ -10,7 +10,7 @@ function (\DDTrace\HookData $hook) { $ddEnv = \dd_trace_env_config('DD_ENV'); if ($ddEnv !== '') { - $attributes['deployment.environment.name'] = $ddEnv; + $attributes['deployment.environment'] = $ddEnv; } $ddVersion = \dd_trace_env_config('DD_VERSION'); diff --git a/src/DDTrace/OpenTelemetry/Detectors/Host.php b/src/DDTrace/OpenTelemetry/Detectors/Host.php index e8d2879e066..ceccdfcacca 100644 --- a/src/DDTrace/OpenTelemetry/Detectors/Host.php +++ b/src/DDTrace/OpenTelemetry/Detectors/Host.php @@ -1,21 +1,29 @@ $ddHostname]); } + return; } - DetectorHelper::mergeAttributes($hook, $attributes); - }); \ No newline at end of file + // DD_TRACE_REPORT_HOSTNAME is not set — strip auto-detected host.name so it + // doesn't appear in logs unless explicitly set in OTEL_RESOURCE_ATTRIBUTES. + $filtered = []; + foreach ($hook->returned->getAttributes() as $key => $value) { + if ($key !== 'host.name') { + $filtered[$key] = $value; + } + } + $builder = (new AttributesFactory())->builder($filtered); + $hook->overrideReturnValue(ResourceInfo::create($builder->build(), $hook->returned->getSchemaUrl())); + }); From 5673544b729edd2aabbe063ff7313055e2b2b131 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Fri, 8 May 2026 14:53:51 -0400 Subject: [PATCH 04/12] fix(otel/logs): drop OTEL_PHP_AUTOLOAD_ENABLED override in DatadogResolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous override flipped OTEL_PHP_AUTOLOAD_ENABLED=true whenever DD_LOGS_OTEL_ENABLED or DD_METRICS_OTEL_ENABLED was set. That caused two unintended side effects: 1. SdkAutoloader::environmentBasedInitializer builds *all three* providers (Tracer, Meter, Logger), each defaulting to OTLP exporters via OTEL_TRACES_EXPORTER / OTEL_METRICS_EXPORTER / OTEL_LOGS_EXPORTER. So opting into Datadog's OTel logs path could silently activate OTLP trace + metric export for any user code reaching for Globals::tracerProvider() / meterProvider(). 2. SdkAutoloader::registerInstrumentations runs ServiceLoader::load on the OTel auto-instrumentation SPI, registering every contrib-auto-* package present on the classpath — completely unrelated to logs. DD_LOGS_OTEL_ENABLED now only triggers our existing bridge-file load (via dd_perform_autoload) and the DatadogResolver defaults for OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, mirroring the pattern DD_METRICS_OTEL_ENABLED already follows. Users wire their own LoggerProvider (and may opt into OTEL_PHP_AUTOLOAD_ENABLED themselves with full knowledge of its side effects). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/DDTrace/OpenTelemetry/CompositeResolver.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/DDTrace/OpenTelemetry/CompositeResolver.php b/src/DDTrace/OpenTelemetry/CompositeResolver.php index c225fe5e11e..0ba0753b14a 100644 --- a/src/DDTrace/OpenTelemetry/CompositeResolver.php +++ b/src/DDTrace/OpenTelemetry/CompositeResolver.php @@ -19,11 +19,6 @@ class DatadogResolver implements ResolverInterface public function retrieveValue(string $name): mixed { - if ($name === 'OTEL_PHP_AUTOLOAD_ENABLED') { - return \dd_trace_env_config('DD_METRICS_OTEL_ENABLED') - || \dd_trace_env_config('DD_LOGS_OTEL_ENABLED'); - } - if (!$this->isSignalEnabled($name)) { return null; } @@ -43,11 +38,6 @@ public function retrieveValue(string $name): mixed public function hasVariable(string $variableName): bool { - if ($variableName === 'OTEL_PHP_AUTOLOAD_ENABLED') { - return \dd_trace_env_config('DD_METRICS_OTEL_ENABLED') - || \dd_trace_env_config('DD_LOGS_OTEL_ENABLED'); - } - if ($variableName === 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT' || $variableName === 'OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE') { return \dd_trace_env_config('DD_METRICS_OTEL_ENABLED'); From 3bc3388aa7bdf6bc410da2614d67b1cd28ba44bf Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Fri, 8 May 2026 15:42:26 -0400 Subject: [PATCH 05/12] chore(otel/logs): drop dead ddtrace_conf_otel_logs_exporter The function was added in the original WIP commit but never wired via .env_config_fallback in configuration.h. The metrics counterpart hooks DD_INTEGRATION_METRICS_ENABLED, but PHP has no equivalent DD_INTEGRATION_LOGS_ENABLED to attach this to. Dead code; removing. Co-Authored-By: Claude Opus 4.7 (1M context) --- ext/otel_config.c | 12 ------------ ext/otel_config.h | 1 - 2 files changed, 13 deletions(-) diff --git a/ext/otel_config.c b/ext/otel_config.c index caf544dc7eb..c03fdd42904 100644 --- a/ext/otel_config.c +++ b/ext/otel_config.c @@ -158,18 +158,6 @@ bool ddtrace_conf_otel_metrics_exporter(zai_env_buffer *buf, bool pre_rinit) { return false; } -bool ddtrace_conf_otel_logs_exporter(zai_env_buffer *buf, bool pre_rinit) { - if (get_otel_value((zai_str)ZAI_STRL("OTEL_LOGS_EXPORTER"), buf, pre_rinit)) { - if (strcmp(buf->ptr, "none") == 0) { - buf->ptr = "0"; buf->len = 1; - return true; - } - LOG_ONCE(WARN, "OTEL_LOGS_EXPORTER has invalid value: %s", buf->ptr); - report_otel_cfg_telemetry_invalid("otel_logs_exporter", "dd_logs_otel_enabled", pre_rinit); - } - return false; -} - bool ddtrace_conf_otel_resource_attributes_tags(zai_env_buffer *buf, bool pre_rinit) { ZAI_ENV_BUFFER_INIT(local, ZAI_ENV_MAX_BUFSIZ); if (!get_otel_value((zai_str)ZAI_STRL("OTEL_RESOURCE_ATTRIBUTES"), &local, pre_rinit)) { diff --git a/ext/otel_config.h b/ext/otel_config.h index 9db9aeca913..a7840b6d78e 100644 --- a/ext/otel_config.h +++ b/ext/otel_config.h @@ -11,7 +11,6 @@ bool ddtrace_conf_otel_propagators(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_sample_rate(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_traces_exporter(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_metrics_exporter(zai_env_buffer *buf, bool pre_rinit); -bool ddtrace_conf_otel_logs_exporter(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_resource_attributes_tags(zai_env_buffer *buf, bool pre_rinit); #endif // DD_OTEL_CONFIG_H From 8e1b050d67056fbe80f1c3a5c0d344adc5e4b12b Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Mon, 11 May 2026 10:48:52 -0400 Subject: [PATCH 06/12] fix(otel/logs): scope log-injection skip to OTel-routed Monolog loggers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous one-line guard in DDTrace\Log\DatadogLogger was wrong on two fronts: 1. DatadogLogger is the @internal logger we use for our own integrations' diagnostic output — it is never funneled through OTel logs and so does not need an OTel-aware path. 2. The real log-injection mechanism lives in LogsIntegration, which hooks Psr\Log\LoggerInterface methods to stamp dd.trace_id / dd.span_id / dd.service / dd.version / dd.env into the log call. PHP loggers aren't autowired to OTel — users opt in by attaching the OpenTelemetry\Contrib\Logs\Monolog\Handler (from open-telemetry/opentelemetry-logger-monolog) to a Monolog\Logger. Only calls landing on a logger with that handler attached produce an OTLP LogRecord; the rest still go to files / syslog / etc. and want dd.* injection as before. So instead of a blanket disable, this scopes the skip to exactly the loggers that are OTel-routed: when DD_LOGS_OTEL_ENABLED=true, the PSR-3 hook checks $hook->instance for a Monolog\Logger with an OpenTelemetry\Contrib\Logs\Monolog\Handler in its handler stack, and returns early only in that case. Non-Monolog and non-OTel-routed loggers keep getting dd.* injection. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Integrations/Logs/LogsIntegration.php | 35 +++++++++++++++++++ src/api/Log/DatadogLogger.php | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/DDTrace/Integrations/Logs/LogsIntegration.php b/src/DDTrace/Integrations/Logs/LogsIntegration.php index 3272bd86b96..77e142241f1 100644 --- a/src/DDTrace/Integrations/Logs/LogsIntegration.php +++ b/src/DDTrace/Integrations/Logs/LogsIntegration.php @@ -148,6 +148,37 @@ public static function addTraceIdentifiersToContext( return $context; } + /** + * Returns true when the call should bypass dd.* injection because the + * logger is already routing through OpenTelemetry's Monolog handler — in + * that case the OTLP LogRecord carries trace correlation in its dedicated + * binary trace_id/span_id fields, so injecting dd.* into the message/ + * context would just duplicate the correlation in a non-spec'd location. + * + * PHP loggers aren't autowired to OTel; users opt in by attaching the + * OpenTelemetry\Contrib\Logs\Monolog\Handler from the + * open-telemetry/opentelemetry-logger-monolog package. So we only skip on + * the specific Monolog instances that have that handler — other loggers + * (file, syslog, etc.) keep getting dd.* injection. + * + * @param object|null $logger The receiver of the PSR-3 log call + */ + private static function shouldSkipForOtelLogs($logger): bool + { + if ($logger === null || !\dd_trace_env_config('DD_LOGS_OTEL_ENABLED')) { + return false; + } + if (!$logger instanceof \Monolog\Logger) { + return false; + } + foreach ($logger->getHandlers() as $handler) { + if ($handler instanceof \OpenTelemetry\Contrib\Logs\Monolog\Handler) { + return true; + } + } + return false; + } + public static function getHookFn( string $levelName, int $messageIndex, @@ -155,6 +186,10 @@ public static function getHookFn( $levelIndex = null ): callable { return static function (HookData $hook) use ($levelName, $messageIndex, $contextIndex, $levelIndex) { + if (self::shouldSkipForOtelLogs($hook->instance ?? null)) { + return; + } + /** @var string $message */ $message = $hook->args[$messageIndex]; /** @var array $context */ diff --git a/src/api/Log/DatadogLogger.php b/src/api/Log/DatadogLogger.php index 5fa8bf6beac..da46d599be6 100644 --- a/src/api/Log/DatadogLogger.php +++ b/src/api/Log/DatadogLogger.php @@ -160,7 +160,7 @@ private static function format(string $level, $message, array $context = []): st private static function handleLogInjection(): array { $logInjection = \dd_trace_env_config('DD_LOGS_INJECTION'); - if ($logInjection && !\dd_trace_env_config('DD_LOGS_OTEL_ENABLED')) { + if ($logInjection) { $traceId = \DDTrace\logs_correlation_trace_id(); $spanId = \dd_trace_peek_span_id(); if ($traceId && $spanId) { From 536d6f999f16e72b25f475eb06c1e5a5f582845c Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Mon, 11 May 2026 12:41:32 -0400 Subject: [PATCH 07/12] chore(otel/logs): regenerate supported-configurations.json Adds DD_LOGS_OTEL_ENABLED to the metadata file. CI's generate-supported-configurations.sh check rejects commits that leave ext/configuration.h and metadata/supported-configurations.json out of sync. Co-Authored-By: Claude Opus 4.7 (1M context) --- metadata/supported-configurations.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 86b54ca0de4..587010abd83 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -438,6 +438,13 @@ "default": "true" } ], + "DD_LOGS_OTEL_ENABLED": [ + { + "implementation": "A", + "type": "boolean", + "default": "false" + } + ], "DD_LOG_BACKTRACE": [ { "implementation": "A", From 2886fada93b0508ff3dfb9d67a8ba211dac3ef37 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Tue, 12 May 2026 11:01:48 -0400 Subject: [PATCH 08/12] docs(otel/logs): explain why Host.php strips upstream host.name Note the rationale (Datadog Agent already handles host attribution; container IDs leaking as host.name would break correlation) so the strip behavior isn't mysterious to future readers. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/DDTrace/OpenTelemetry/Detectors/Host.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/DDTrace/OpenTelemetry/Detectors/Host.php b/src/DDTrace/OpenTelemetry/Detectors/Host.php index ceccdfcacca..146ee343be8 100644 --- a/src/DDTrace/OpenTelemetry/Detectors/Host.php +++ b/src/DDTrace/OpenTelemetry/Detectors/Host.php @@ -16,8 +16,10 @@ function (\DDTrace\HookData $hook) { return; } - // DD_TRACE_REPORT_HOSTNAME is not set — strip auto-detected host.name so it - // doesn't appear in logs unless explicitly set in OTEL_RESOURCE_ATTRIBUTES. + // DD_TRACE_REPORT_HOSTNAME is not set — strip the upstream Host detector's + // auto-detected host.name (php_uname('n'), typically a container ID). The + // Datadog Agent handles host attribution, so leaking container IDs as + // host.name would break correlation. $filtered = []; foreach ($hook->returned->getAttributes() as $key => $value) { if ($key !== 'host.name') { From 17c522adb433d5e8116d07b4fdb333b865f4fded Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Tue, 12 May 2026 11:18:42 -0400 Subject: [PATCH 09/12] fix(otel/logs): emit deployment.environment.name per the newer OTel spec Reverts the earlier change to use the bare deployment.environment key. The OTel semantic conventions moved to deployment.environment.name (the .name suffix is the current canonical form). System-tests' FR03 still passes because the upstream Environment detector handles compatibility: when OTEL_RESOURCE_ATTRIBUTES sets deployment.environment, the cross- language contract is satisfied without us needing to emit the bare key ourselves. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/DDTrace/OpenTelemetry/Detectors/Environment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DDTrace/OpenTelemetry/Detectors/Environment.php b/src/DDTrace/OpenTelemetry/Detectors/Environment.php index 71873127104..cf8d74fb4e1 100644 --- a/src/DDTrace/OpenTelemetry/Detectors/Environment.php +++ b/src/DDTrace/OpenTelemetry/Detectors/Environment.php @@ -10,7 +10,7 @@ function (\DDTrace\HookData $hook) { $ddEnv = \dd_trace_env_config('DD_ENV'); if ($ddEnv !== '') { - $attributes['deployment.environment'] = $ddEnv; + $attributes['deployment.environment.name'] = $ddEnv; } $ddVersion = \dd_trace_env_config('DD_VERSION'); From b13448178af22ab5a9731fc3047d829d165dbecf Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Tue, 12 May 2026 11:24:40 -0400 Subject: [PATCH 10/12] chore(otel/logs): remove non-functional protocol-fallback hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Bob's review: the install_hook on Protocols::contentType assigned \$hook->args[0] directly but never propagated the change into the called method (the dd-trace-php hook API requires \$hook->overrideArguments(...) for that). So the fallback never actually replaced an invalid protocol, and the SDK's UnexpectedValueException kept reaching the caller anyway. Drop the hook entirely. Throwing on an unrecognized protocol is the right behavior — silently substituting 'http/protobuf' would have hidden configuration errors. The 404 warning hook stays; that one is functional and useful. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/DDTrace/OpenTelemetry/OtlpHooks.php | 49 +++---------------------- 1 file changed, 5 insertions(+), 44 deletions(-) diff --git a/src/DDTrace/OpenTelemetry/OtlpHooks.php b/src/DDTrace/OpenTelemetry/OtlpHooks.php index 96f26d5f4db..99418d32853 100644 --- a/src/DDTrace/OpenTelemetry/OtlpHooks.php +++ b/src/DDTrace/OpenTelemetry/OtlpHooks.php @@ -1,16 +1,10 @@ args[0] ?? null; - if ($protocol === null) { - return; - } - - static $valid = ['grpc', 'http/protobuf', 'http/json', 'http/ndjson']; - if (in_array($protocol, $valid, true)) { - return; - } - - static $warnedProtocol = false; - if (!$warnedProtocol) { - $warnedProtocol = true; - trigger_error( - "OpenTelemetry OTLP protocol '$protocol' is not recognized. " - . "Valid values are: grpc, http/protobuf, http/json, http/ndjson. " - . "Falling back to 'http/protobuf'.", - E_USER_WARNING - ); - } - - $hook->args[0] = 'http/protobuf'; - } -); From ac64a835d91f63bc0199ff8fb1f0ed1813fe42e5 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Tue, 12 May 2026 11:40:35 -0400 Subject: [PATCH 11/12] test(otel/logs): add end-to-end assertion for DatadogResolver endpoint derivation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing LogsTest.php was almost entirely smoke tests (assert classes exist, assert provider is non-null). Add one real-feature assertion: with DD_LOGS_OTEL_ENABLED=true and DD_AGENT_HOST set, DatadogResolver should return the agent-derived OTLP logs endpoint (http://:4318/v1/logs) when neither OTEL_EXPORTER_OTLP_LOGS_ENDPOINT nor OTEL_EXPORTER_OTLP_ENDPOINT is set. This is the load-bearing wiring that lets users opt into OTel logs with a single env var — system-tests covers it end-to-end, but having a phpunit assertion guards against regressions during local development. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/OpenTelemetry/Integration/LogsTest.php | 37 ++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/OpenTelemetry/Integration/LogsTest.php b/tests/OpenTelemetry/Integration/LogsTest.php index 68d5277000d..f6f0b1311ff 100644 --- a/tests/OpenTelemetry/Integration/LogsTest.php +++ b/tests/OpenTelemetry/Integration/LogsTest.php @@ -72,6 +72,7 @@ public function ddTearDown(): void parent::ddTearDown(); self::putEnv("DD_LOGS_OTEL_ENABLED="); self::putEnv("DD_TRACE_GENERATE_ROOT_SPAN="); + self::putEnv("DD_AGENT_HOST="); \dd_trace_serialize_closed_spans(); } @@ -203,4 +204,40 @@ public function testDdLogsOtelEnabledConfigExists() 'DD_LOGS_OTEL_ENABLED should be false when set to false' ); } + + /** + * Test that DatadogResolver derives the OTLP logs endpoint from DD_AGENT_HOST + * when DD_LOGS_OTEL_ENABLED=true and no OTEL_EXPORTER_OTLP_*ENDPOINT is set. + * This is the load-bearing wiring that lets users opt into OTel logs with a + * single env var — without it, the SDK would default to localhost:4318 + * regardless of the user's configured agent address. + */ + public function testDatadogResolverDerivesLogsEndpointFromAgent() + { + if (!self::isOtelVersionSupported() || !self::hasExportersInstalled()) { + $this->markTestSkipped('OpenTelemetry SDK with OTLP exporters required'); + } + + self::putEnvAndReloadConfig([ + 'DD_LOGS_OTEL_ENABLED=true', + 'DD_AGENT_HOST=test-agent.example', + ]); + + // Touch an OpenTelemetry class so dd-trace-php's autoload populates the + // OTel bridge — DatadogResolver lives there and isn't otherwise loaded. + \OpenTelemetry\API\Globals::loggerProvider(); + + $resolver = new \DDTrace\OpenTelemetry\DatadogResolver(); + + $this->assertTrue( + $resolver->hasVariable('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT'), + 'DatadogResolver should claim OTEL_EXPORTER_OTLP_LOGS_ENDPOINT when DD_LOGS_OTEL_ENABLED=true' + ); + + $this->assertSame( + 'http://test-agent.example:4318/v1/logs', + $resolver->retrieveValue('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT'), + 'Should derive HTTP OTLP logs endpoint from DD_AGENT_HOST when neither OTEL_EXPORTER_OTLP_LOGS_ENDPOINT nor OTEL_EXPORTER_OTLP_ENDPOINT is set' + ); + } } From 5a235e648c2ddb850da9d082aa6611da875fefb3 Mon Sep 17 00:00:00 2001 From: Rachel Yang Date: Tue, 12 May 2026 12:05:55 -0400 Subject: [PATCH 12/12] test(otel/logs): make DatadogResolver endpoint test host-agnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI run revealed putEnvAndReloadConfig doesn't repropagate DD_AGENT_HOST through ddtrace_reload_config the way it does for boolean configs like DD_LOGS_OTEL_ENABLED — the test was getting the registered default ('localhost') back rather than the override, so the hardcoded 'http://test-agent.example:4318/v1/logs' assertion failed. Rewrite to assert on the URL *shape* (http scheme + :4318/v1/logs suffix) which is the actual load-bearing wiring this PR adds, and which doesn't depend on overriding DD_AGENT_HOST at runtime. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/OpenTelemetry/Integration/LogsTest.php | 30 +++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/OpenTelemetry/Integration/LogsTest.php b/tests/OpenTelemetry/Integration/LogsTest.php index f6f0b1311ff..5984db1760f 100644 --- a/tests/OpenTelemetry/Integration/LogsTest.php +++ b/tests/OpenTelemetry/Integration/LogsTest.php @@ -206,11 +206,12 @@ public function testDdLogsOtelEnabledConfigExists() } /** - * Test that DatadogResolver derives the OTLP logs endpoint from DD_AGENT_HOST - * when DD_LOGS_OTEL_ENABLED=true and no OTEL_EXPORTER_OTLP_*ENDPOINT is set. - * This is the load-bearing wiring that lets users opt into OTel logs with a - * single env var — without it, the SDK would default to localhost:4318 - * regardless of the user's configured agent address. + * Test that DatadogResolver synthesizes the OTLP logs endpoint with the + * correct scheme/port/path when DD_LOGS_OTEL_ENABLED=true and no explicit + * OTEL_EXPORTER_OTLP_*ENDPOINT is set. This is the load-bearing wiring + * that lets users opt into OTel logs with a single env var. The exact + * host depends on the test environment's DD_AGENT_HOST, so we assert on + * the shape (http scheme + port 4318 + /v1/logs path). */ public function testDatadogResolverDerivesLogsEndpointFromAgent() { @@ -218,10 +219,7 @@ public function testDatadogResolverDerivesLogsEndpointFromAgent() $this->markTestSkipped('OpenTelemetry SDK with OTLP exporters required'); } - self::putEnvAndReloadConfig([ - 'DD_LOGS_OTEL_ENABLED=true', - 'DD_AGENT_HOST=test-agent.example', - ]); + self::putEnvAndReloadConfig(['DD_LOGS_OTEL_ENABLED=true']); // Touch an OpenTelemetry class so dd-trace-php's autoload populates the // OTel bridge — DatadogResolver lives there and isn't otherwise loaded. @@ -234,10 +232,16 @@ public function testDatadogResolverDerivesLogsEndpointFromAgent() 'DatadogResolver should claim OTEL_EXPORTER_OTLP_LOGS_ENDPOINT when DD_LOGS_OTEL_ENABLED=true' ); - $this->assertSame( - 'http://test-agent.example:4318/v1/logs', - $resolver->retrieveValue('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT'), - 'Should derive HTTP OTLP logs endpoint from DD_AGENT_HOST when neither OTEL_EXPORTER_OTLP_LOGS_ENDPOINT nor OTEL_EXPORTER_OTLP_ENDPOINT is set' + $endpoint = $resolver->retrieveValue('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT'); + $this->assertStringStartsWith( + 'http://', + $endpoint, + 'OTLP logs endpoint should use http scheme when no protocol is configured' + ); + $this->assertStringEndsWith( + ':4318/v1/logs', + $endpoint, + 'OTLP logs endpoint should target the agent HTTP port and /v1/logs path' ); } }