Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 20 additions & 4 deletions tracer/src/Datadog.Trace/Agent/ManagedApiOtlp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Datadog.Trace.Agent.Transports;
using Datadog.Trace.Configuration;
using Datadog.Trace.DogStatsd;
using Datadog.Trace.Logging;

namespace Datadog.Trace.Agent;

Expand All @@ -23,6 +22,8 @@ namespace Datadog.Trace.Agent;
/// </summary>
internal sealed class ManagedApiOtlp : IApi
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<ManagedApiOtlp>();

private IApi _api;

public ManagedApiOtlp(TracerSettings settings)
Expand All @@ -34,7 +35,7 @@ public ManagedApiOtlp(TracerSettings settings)
[MemberNotNull(nameof(_api))]
void UpdateApi(TracerSettings settings, ExporterSettings exporterSettings)
{
var apiRequestFactory = TracesTransportStrategy.Get(exporterSettings);
var apiRequestFactory = CreateOtlpRequestFactory(exporterSettings);
var api = new ApiOtlp(apiRequestFactory, settings, exporterSettings);
Interlocked.Exchange(ref _api!, api);
}
Expand All @@ -49,6 +50,21 @@ public Task<bool> SendTracesAsync(ArraySegment<byte> traces, int numberOfTraces,

public Task<bool> SendStatsAsync(StatsBuffer stats, long bucketDuration)
=> Volatile.Read(ref _api).SendStatsAsync(stats, bucketDuration);

/// <summary>
/// Creates an <see cref="IApiRequestFactory"/> for OTLP export using the OTLP endpoint
/// directly, bypassing the APM trace transport strategy. This is necessary because
/// <see cref="TracesTransportStrategy"/> selects the transport based on the APM agent
/// configuration (e.g. the APM Unix domain socket), which is incorrect for OTLP export
/// that should use the OTLP-specific endpoint.
/// </summary>
internal static IApiRequestFactory CreateOtlpRequestFactory(ExporterSettings exporterSettings)
{
var otlpEndpoint = exporterSettings.OtlpTracesEndpoint;
var baseEndpoint = new Uri($"{otlpEndpoint.Scheme}://{otlpEndpoint.Authority}");
Comment on lines +63 to +64
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can OTLP not be sent over UDS then? 🤔

Log.Debug("Using " + nameof(HttpClientRequestFactory) + " for OTLP traces transport to {Endpoint}", baseEndpoint);
return new HttpClientRequestFactory(baseEndpoint, AgentHttpHeaderNames.DefaultHeaders);
}
}

#endif
90 changes: 90 additions & 0 deletions tracer/test/Datadog.Trace.Tests/Agent/ManagedApiOtlpTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// <copyright file="ManagedApiOtlpTests.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#if NET6_0_OR_GREATER

using System;
using System.Collections.Specialized;
using Datadog.Trace.Agent;
using Datadog.Trace.Agent.Transports;
using Datadog.Trace.Configuration;
using Datadog.Trace.Configuration.Telemetry;
using FluentAssertions;
using Xunit;

namespace Datadog.Trace.Tests.Agent;

public class ManagedApiOtlpTests
{
[Fact]
public void CreateOtlpRequestFactory_WhenApmUsesUnixDomainSocket_UsesHttpForOtlp()
{
// Simulate the Linux scenario: APM UDS exists at the default path,
// and the OTLP endpoint defaults to http://localhost:4318
var source = BuildSource("OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318");
Func<string, bool> fileExists = path => path == ExporterSettings.DefaultTracesUnixDomainSocket;
var exporterSettings = new ExporterSettings(source, fileExists, NullConfigurationTelemetry.Instance);

// Verify the APM transport is UDS (this is the precondition that caused the bug)
exporterSettings.TracesTransport.Should().Be(TracesTransportType.UnixDomainSocket);

var factory = ManagedApiOtlp.CreateOtlpRequestFactory(exporterSettings);

// The factory must be a plain HttpClientRequestFactory, not a
// SocketHandlerRequestFactory (which would route through the APM UDS)
factory.Should().BeOfType<HttpClientRequestFactory>();
}

[Theory]
[InlineData("http://localhost:4318", "http://localhost:4318/v1/traces")]
[InlineData("http://otel-collector:4318", "http://otel-collector:4318/v1/traces")]
[InlineData("http://localhost:9999", "http://localhost:9999/v1/traces")]
public void CreateOtlpRequestFactory_UsesCorrectOtlpEndpoint(string otlpEndpoint, string expectedTracesEndpoint)
{
var source = BuildSource($"OTEL_EXPORTER_OTLP_ENDPOINT:{otlpEndpoint}");
Func<string, bool> fileExists = path => path == ExporterSettings.DefaultTracesUnixDomainSocket;
var exporterSettings = new ExporterSettings(source, fileExists, NullConfigurationTelemetry.Instance);

// Verify the OTLP endpoint is correct
exporterSettings.OtlpTracesEndpoint.Should().Be(new Uri(expectedTracesEndpoint));

var factory = ManagedApiOtlp.CreateOtlpRequestFactory(exporterSettings);

// The factory should target the OTLP endpoint's authority
var expectedBase = new Uri($"{new Uri(expectedTracesEndpoint).Scheme}://{new Uri(expectedTracesEndpoint).Authority}");
factory.GetEndpoint("/v1/traces").Should().Be(new Uri(expectedBase, "/v1/traces"));
}

[Fact]
public void CreateOtlpRequestFactory_WithExplicitTracesEndpoint_UsesSignalEndpoint()
{
var source = BuildSource(
"OTEL_EXPORTER_OTLP_ENDPOINT:http://general-endpoint:4318",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:http://traces-endpoint:4318/v1/traces");
var exporterSettings = new ExporterSettings(source, _ => false, NullConfigurationTelemetry.Instance);

exporterSettings.OtlpTracesEndpoint.Should().Be(new Uri("http://traces-endpoint:4318/v1/traces"));

var factory = ManagedApiOtlp.CreateOtlpRequestFactory(exporterSettings);

factory.Should().BeOfType<HttpClientRequestFactory>();
factory.GetEndpoint("/v1/traces").Should().Be(new Uri("http://traces-endpoint:4318/v1/traces"));
}

private static NameValueConfigurationSource BuildSource(params string[] config)
{
var configNameValues = new NameValueCollection();

foreach (var item in config)
{
var separatorIndex = item.IndexOf(':');
configNameValues.Add(item.Substring(0, separatorIndex), item.Substring(separatorIndex + 1));
}

return new NameValueConfigurationSource(configNameValues);
}
}

#endif
Loading