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 c5f271b1efd..24ad8b39269 100644
--- a/ext/configuration.h
+++ b/ext/configuration.h
@@ -242,6 +242,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/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",
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/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/Detectors/Host.php b/src/DDTrace/OpenTelemetry/Detectors/Host.php
index e8d2879e066..146ee343be8 100644
--- a/src/DDTrace/OpenTelemetry/Detectors/Host.php
+++ b/src/DDTrace/OpenTelemetry/Detectors/Host.php
@@ -1,21 +1,31 @@
$ddHostname]);
}
+ return;
}
- DetectorHelper::mergeAttributes($hook, $attributes);
- });
\ No newline at end of file
+ // 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') {
+ $filtered[$key] = $value;
+ }
+ }
+ $builder = (new AttributesFactory())->builder($filtered);
+ $hook->overrideReturnValue(ResourceInfo::create($builder->build(), $hook->returned->getSchemaUrl()));
+ });
diff --git a/src/DDTrace/OpenTelemetry/OtlpHooks.php b/src/DDTrace/OpenTelemetry/OtlpHooks.php
new file mode 100644
index 00000000000..99418d32853
--- /dev/null
+++ b/src/DDTrace/OpenTelemetry/OtlpHooks.php
@@ -0,0 +1,40 @@
+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
+ );
+ }
+ }
+);
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..5984db1760f
--- /dev/null
+++ b/tests/OpenTelemetry/Integration/LogsTest.php
@@ -0,0 +1,247 @@
+= 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=");
+ self::putEnv("DD_AGENT_HOST=");
+ \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'
+ );
+ }
+
+ /**
+ * 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()
+ {
+ if (!self::isOtelVersionSupported() || !self::hasExportersInstalled()) {
+ $this->markTestSkipped('OpenTelemetry SDK with OTLP exporters required');
+ }
+
+ 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.
+ \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'
+ );
+
+ $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'
+ );
+ }
+}
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index c2817d25d85..399e4f8273d 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