Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ext/autoload_php_files.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions ext/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down
7 changes: 7 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,13 @@
"default": "true"
}
],
"DD_LOGS_OTEL_ENABLED": [
{
"implementation": "A",
"type": "boolean",
"default": "false"
}
],
"DD_LOG_BACKTRACE": [
{
"implementation": "A",
Expand Down
35 changes: 35 additions & 0 deletions src/DDTrace/Integrations/Logs/LogsIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,48 @@ 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,
int $contextIndex,
$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 */
Expand Down
98 changes: 70 additions & 28 deletions src/DDTrace/OpenTelemetry/CompositeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ class DatadogResolver implements ResolverInterface

public function retrieveValue(string $name): mixed
{
if (!$this->isMetricsEnabled($name)) {
if (!$this->isSignalEnabled($name)) {
return null;
}

if ($name === 'OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE') {
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);
}

Expand All @@ -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();

Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/DDTrace/OpenTelemetry/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
];
Expand Down
24 changes: 17 additions & 7 deletions src/DDTrace/OpenTelemetry/Detectors/Host.php
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
<?php

use DDTrace\OpenTelemetry\Detectors\DetectorHelper;
use OpenTelemetry\SDK\Common\Attribute\AttributesFactory;
use OpenTelemetry\SDK\Resource\ResourceInfo;

\DDTrace\install_hook(
'OpenTelemetry\SDK\Resource\Detectors\Host::getResource',
null,
function (\DDTrace\HookData $hook) {
$attributes = [];

if (\dd_trace_env_config('DD_TRACE_REPORT_HOSTNAME')) {
$ddHostname = \dd_trace_env_config('DD_HOSTNAME');
// Only override if DD_HOSTNAME is explicitly set to avoid
// clobbering the hostname detected by OTel's Host detector
if ($ddHostname !== '') {
$attributes['host.name'] = $ddHostname;
DetectorHelper::mergeAttributes($hook, ['host.name' => $ddHostname]);
}
return;
}

DetectorHelper::mergeAttributes($hook, $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') {
$filtered[$key] = $value;
}
}
$builder = (new AttributesFactory())->builder($filtered);
$hook->overrideReturnValue(ResourceInfo::create($builder->build(), $hook->returned->getSchemaUrl()));
});
40 changes: 40 additions & 0 deletions src/DDTrace/OpenTelemetry/OtlpHooks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

// One-time actionable warning when the Datadog Agent returns HTTP 404 to an
// OTLP export — almost always means Agent < 7.48.0 (no OTLP ingest support)
// or the OTLP receiver isn't configured. PsrTransport wraps 4xx (non-408/429)
// errors in ErrorFuture; peek at the stored exception via reflection so we
// can emit the message once without modifying the future the caller receives.
\DDTrace\install_hook(
'OpenTelemetry\SDK\Common\Export\Http\PsrTransport::send',
null,
function (\DDTrace\HookData $hook) {
$future = $hook->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
);
}
}
);
1 change: 1 addition & 0 deletions src/bridge/_files_opentelemetry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading
Loading