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/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/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())); + }); 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 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