diff --git a/tracer/src/Datadog.Trace/TracerManagerFactory.cs b/tracer/src/Datadog.Trace/TracerManagerFactory.cs index 12a945b03424..827b170394bb 100644 --- a/tracer/src/Datadog.Trace/TracerManagerFactory.cs +++ b/tracer/src/Datadog.Trace/TracerManagerFactory.cs @@ -296,10 +296,8 @@ internal static IDogStatsd CreateDogStatsdClient(ImmutableTracerSettings setting switch (settings.ExporterInternal.MetricsTransport) { case MetricsTransportType.NamedPipe: - // Environment variables for windows named pipes are not explicitly passed to statsd. - // They are retrieved within the vendored code, so there is nothing to pass. - // Passing anything through StatsdConfig may cause bugs when windows named pipes should be used. Log.Information("Using windows named pipes for metrics transport."); + config.PipeName = settings.ExporterInternal.MetricsPipeNameInternal; break; #if NETCOREAPP3_1_OR_GREATER case MetricsTransportType.UDS: @@ -309,7 +307,13 @@ internal static IDogStatsd CreateDogStatsdClient(ImmutableTracerSettings setting #endif case MetricsTransportType.UDP: default: - config.StatsdServerName = settings.ExporterInternal.AgentUriInternal.DnsSafeHost; + // If the customer has enabled UDS traces but _not_ UDS metrics, then the AgentUri will have + // the UDS path set for it, and the DnsSafeHost returns "". Ideally, we would expose + // a TracesAgentUri and MetricsAgentUri or something like that instead. This workaround + // is a horrible hack, but I can't bear to touch ExporterSettings until it's had an + // extensive tidy up, so this should do for now + var traceHostname = settings.ExporterInternal.AgentUriInternal.DnsSafeHost; + config.StatsdServerName = string.IsNullOrEmpty(traceHostname) ? "127.0.0.1" : traceHostname; config.StatsdPort = settings.ExporterInternal.DogStatsdPortInternal; break; } diff --git a/tracer/test/Datadog.Trace.TestHelpers/EnvironmentRestorerAttribute.cs b/tracer/test/Datadog.Trace.TestHelpers/EnvironmentRestorerAttribute.cs new file mode 100644 index 000000000000..61222348a1af --- /dev/null +++ b/tracer/test/Datadog.Trace.TestHelpers/EnvironmentRestorerAttribute.cs @@ -0,0 +1,44 @@ +// +// 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. +// + +using System; +using System.Collections.Generic; +using System.Reflection; +using Xunit.Sdk; + +namespace Datadog.Trace.TestHelpers; + +[AttributeUsage(AttributeTargets.Class)] +public class EnvironmentRestorerAttribute : BeforeAfterTestAttribute +{ + private readonly string[] _variables; + private readonly Dictionary _originalVariables; + + public EnvironmentRestorerAttribute(params string[] args) + { + _variables = args; + _originalVariables = new(args.Length); + } + + public override void Before(MethodInfo methodUnderTest) + { + foreach (var variable in _variables) + { + _originalVariables[variable] = Environment.GetEnvironmentVariable(variable); + // clear variable + Environment.SetEnvironmentVariable(variable, null); + } + + base.Before(methodUnderTest); + } + + public override void After(MethodInfo methodUnderTest) + { + foreach (var variable in _originalVariables) + { + Environment.SetEnvironmentVariable(variable.Key, variable.Value); + } + } +} diff --git a/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs b/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs index d4d5c4e82466..72f2b827ad87 100644 --- a/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs +++ b/tracer/test/Datadog.Trace.Tests/DogStatsDTests.cs @@ -5,18 +5,31 @@ using System; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Threading; using Datadog.Trace.Configuration; using Datadog.Trace.DogStatsd; using Datadog.Trace.TestHelpers; using Datadog.Trace.Vendors.StatsdClient; +using Datadog.Trace.Vendors.StatsdClient.Transport; +using FluentAssertions; using Moq; using Xunit; using Xunit.Abstractions; namespace Datadog.Trace.Tests { + [Collection(nameof(EnvironmentVariablesTestCollection))] + [EnvironmentRestorer( + ConfigurationKeys.AgentHost, + ConfigurationKeys.AgentUri, + ConfigurationKeys.AgentPort, + ConfigurationKeys.DogStatsdPort, + ConfigurationKeys.TracesPipeName, + ConfigurationKeys.TracesUnixDomainSocketPath, + ConfigurationKeys.MetricsPipeName, + ConfigurationKeys.MetricsUnixDomainSocketPath)] public class DogStatsDTests { private readonly ITestOutputHelper _output; @@ -97,6 +110,121 @@ public void Send_metrics_when_enabled() */ } + [SkippableTheory] + [InlineData(null, null, null)] // Should default to udp + [InlineData("http://127.0.0.1:1234", null, null)] + [InlineData(null, "127.0.0.1", null)] + [InlineData(null, "127.0.0.1", "1234")] + public void CanCreateDogStatsD_UDP_FromTraceAgentSettings(string agentUri, string agentHost, string port) + { + SkipOn.Platform(SkipOn.PlatformValue.MacOs); + + var settings = new TracerSettings(new NameValueConfigurationSource(new() + { + { ConfigurationKeys.AgentUri, agentUri }, + { ConfigurationKeys.AgentHost, agentHost }, + { ConfigurationKeys.DogStatsdPort, port }, + })).Build(); + + settings.Exporter.MetricsTransport.Should().Be(TransportType.UDP); + var expectedPort = settings.Exporter.DogStatsdPort; + + // Dogstatsd tries to actually contact the agent during creation, so need to have something listening + // No guarantees it's actually using the _right_ config here, but it's better than nothing + using var agent = MockTracerAgent.Create(_output, useStatsd: true, requestedStatsDPort: expectedPort); + + var dogStatsD = TracerManagerFactory.CreateDogStatsdClient(settings, "test service", null); + + // If there's an error during configuration, we get a no-op instance, so using this as a test + dogStatsD.Should() + .NotBeNull() + .And.BeOfType(); + } + + [SkippableFact] + public void CanCreateDogStatsD_NamedPipes_FromTraceAgentSettings() + { + SkipOn.Platform(SkipOn.PlatformValue.MacOs); + SkipOn.Platform(SkipOn.PlatformValue.Linux); + + using var agent = MockTracerAgent.Create(_output, new WindowsPipesConfig($"trace-{Guid.NewGuid()}", $"metrics-{Guid.NewGuid()}") { UseDogstatsD = true }); + + var settings = new TracerSettings(new NameValueConfigurationSource(new() + { + { ConfigurationKeys.MetricsPipeName, agent.StatsWindowsPipeName }, + })).Build(); + + settings.Exporter.MetricsTransport.Should().Be(TransportType.NamedPipe); + + // Dogstatsd tries to actually contact the agent during creation, so need to have something listening + // No guarantees it's actually using the _right_ config here, but it's better than nothing + var dogStatsD = TracerManagerFactory.CreateDogStatsdClient(settings, "test service", null); + + // If there's an error during configuration, we get a no-op instance, so using this as a test + dogStatsD.Should() + .NotBeNull() + .And.BeOfType(); + } + +#if NETCOREAPP3_1_OR_GREATER + [SkippableFact] + public void CanCreateDogStatsD_UDS_FromTraceAgentSettings() + { + // UDP Datagrams over UDP are not supported on Windows + SkipOn.Platform(SkipOn.PlatformValue.Windows); + SkipOn.Platform(SkipOn.PlatformValue.MacOs); + + var tracesPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var metricsPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + using var agent = MockTracerAgent.Create(_output, new UnixDomainSocketConfig(tracesPath, metricsPath) { UseDogstatsD = true }); + + var settings = new TracerSettings(new NameValueConfigurationSource(new() + { + { ConfigurationKeys.AgentUri, $"unix://{tracesPath}" }, + { ConfigurationKeys.MetricsUnixDomainSocketPath, $"unix://{metricsPath}" }, + })).Build(); + + settings.Exporter.MetricsTransport.Should().Be(TransportType.UDS); + + // Dogstatsd tries to actually contact the agent during creation, so need to have something listening + // No guarantees it's actually using the _right_ config here, but it's better than nothing + var dogStatsD = TracerManagerFactory.CreateDogStatsdClient(settings, "test service", null); + + // If there's an error during configuration, we get a no-op instance, so using this as a test + dogStatsD.Should() + .NotBeNull() + .And.BeOfType(); + } + + [SkippableFact] + public void CanCreateDogStatsD_UDS_FallsBackToUdp_FromTraceAgentSettings() + { + SkipOn.Platform(SkipOn.PlatformValue.MacOs); + + var tracesPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + using var udsAgent = MockTracerAgent.Create(_output, new UnixDomainSocketConfig(tracesPath, null) { UseDogstatsD = false }); + using var udpAgent = MockTracerAgent.Create(_output, useStatsd: true); + + var settings = new TracerSettings(new NameValueConfigurationSource(new() + { + { ConfigurationKeys.AgentUri, $"unix://{tracesPath}" }, + })).Build(); + + // If we're not using the "default" UDS path, then we fallback to UDP for stats + // Should fallback to the "default" stats location + settings.Exporter.MetricsTransport.Should().Be(TransportType.UDP); + + // Dogstatsd tries to actually contact the agent during creation, so need to have something listening + // No guarantees it's actually using the _right_ config here, but it's better than nothing + var dogStatsD = TracerManagerFactory.CreateDogStatsdClient(settings, "test service", null); + + // If there's an error during configuration, we get a no-op instance, so using this as a test + dogStatsD.Should() + .NotBeNull() + .And.BeOfType(); + } +#endif + private static IImmutableList SendSpan(bool tracerMetricsEnabled, IDogStatsd statsd) { IImmutableList spans;