Skip to content

Commit

Permalink
ASP.NET metrics tests (#901)
Browse files Browse the repository at this point in the history
* .NET Framework metrics tests

* save work in progress

* Metrics exporter interval.

* Remove old changes

* Fix collector

* OTEL_AUTO_METRIC_EXPORT_INTERVAL->OTEL_METRIC_EXPORT_INTERVAL

* PR feedback.

* PR feedback
  • Loading branch information
rajkumar-rangaraj authored Jun 30, 2022
1 parent f75eba9 commit 94fbe95
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 16 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ This release is built on top of [OpenTelemetry .NET](https://github.com/open-tel
- `OTEL_DOTNET_AUTO_INTEGRATIONS_FILE` can accept multiple filepaths
delimted by the platform-specific path separator
(`;` on Windows, `:` on Linux and macOS).
- Support for metric exporter interval using environment variable:
`OTEL_METRIC_EXPORT_INTERVAL`.

### Changed

Expand Down
1 change: 1 addition & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ Important environment variables include:
| `OTEL_DOTNET_AUTO_FLUSH_ON_UNHANDLEDEXCEPTION` | Controls whether the telemetry data is flushed when an [AppDomain.UnhandledException](https://docs.microsoft.com/en-us/dotnet/api/system.appdomain.unhandledexception) event is raised. Set to `true` when you suspect that you are experiencing a problem with missing telemetry data and also experiencing unhandled exceptions. | `false` |
| `OTEL_DOTNET_AUTO_TRACES_PLUGINS` | Colon-separated list of OTel SDK instrumentation tracer plugin types, specified with the [assembly-qualified name](https://docs.microsoft.com/en-us/dotnet/api/system.type.assemblyqualifiedname?view=net-6.0#system-type-assemblyqualifiedname). _Note: This list must be colon-separated because the type names may include commas._ | |
| `OTEL_DOTNET_AUTO_METRICS_ADDITIONAL_SOURCES` | Comma-separated list of additional `System.Diagnostics.Metrics.Meter` names to be added to the meter at the startup. Use it to capture manually instrumented spans. | |
| `OTEL_METRIC_EXPORT_INTERVAL` | The time interval (in milliseconds) between the start of two export attempts. | 6000 |
| `OTEL_DOTNET_AUTO_METRICS_PLUGINS` | Colon-separated list of OTel SDK instrumentation meter plugin types, specified with the [assembly-qualified name](https://docs.microsoft.com/en-us/dotnet/api/system.type.assemblyqualifiedname?view=net-6.0#system-type-assemblyqualifiedname). _Note: This list must be colon-separated because the type names may include commas._ | |

You can use `OTEL_DOTNET_AUTO_TRACES_PLUGINS` to extend the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ public static class Metrics
/// </summary>
public const string Exporter = "OTEL_METRICS_EXPORTER";

/// <summary>
/// Configuration key for the time interval (in milliseconds) between the start of two metrics export attempts.
/// </summary>
public const string ExportInterval = "OTEL_METRIC_EXPORT_INTERVAL";

/// <summary>
/// Configuration key for whether the metrics console exporter is enabled.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,17 @@ private static MeterProviderBuilder SetExporter(this MeterProviderBuilder builde
AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
}
#endif
builder.AddOtlpExporter(options =>
builder.AddOtlpExporter((options, metricReaderOptions) =>
{
if (settings.OtlpExportProtocol.HasValue)
{
options.Protocol = settings.OtlpExportProtocol.Value;
}

if (settings.MetricExportInterval != null)
{
metricReaderOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = settings.MetricExportInterval;
}
});
break;
case MetricsExporter.None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ private MeterSettings(IConfigurationSource source)
}
}

MetricExportInterval = source.GetInt32(ConfigurationKeys.Metrics.ExportInterval);
MetricsEnabled = source.GetBool(ConfigurationKeys.Metrics.Enabled) ?? true;
LoadMetricsAtStartup = source.GetBool(ConfigurationKeys.Metrics.LoadMeterAtStartup) ?? true;
}
Expand All @@ -103,6 +104,11 @@ private MeterSettings(IConfigurationSource source)
/// </summary>
public MetricsExporter MetricExporter { get; }

/// <summary>
/// Gets the metrics export interval.
/// </summary>
public int? MetricExportInterval { get; }

/// <summary>
/// Gets a value indicating whether the console exporter is enabled.
/// </summary>
Expand Down
61 changes: 56 additions & 5 deletions test/IntegrationTests/AspNetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@
// </copyright>

#if NETFRAMEWORK
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using FluentAssertions;
using FluentAssertions.Execution;
using IntegrationTests.Helpers;
using Opentelemetry.Proto.Common.V1;
using Xunit;
using Xunit.Abstractions;

Expand All @@ -43,13 +48,18 @@ public async Task SubmitsTraces()
var agentPort = TcpPortProvider.GetOpenPort();
var webPort = TcpPortProvider.GetOpenPort();

// Using "*" as host requires Administrator. This is needed to make the mock collector endpoint
// accessible to the Windows docker container where the test application is executed by binding
// the endpoint to all network interfaces. In order to do that it is necessary to open the port
// on the firewall.
var testSettings = new TestSettings
{
TracesSettings = new TracesSettings { Port = agentPort }
};

// Using "*" as host requires Administrator. This is needed to make the mock collector endpoint
// accessible to the Windows docker container where the test application is executed by binding
// the endpoint to all network interfaces. In order to do that it is necessary to open the port
// on the firewall.
using var fwPort = FirewallHelper.OpenWinPort(agentPort, Output);
using var agent = new MockZipkinCollector(Output, agentPort, host: "*");
using var container = await StartContainerAsync(agentPort, webPort);
using var container = await StartContainerAsync(testSettings, webPort);

var client = new HttpClient();

Expand All @@ -65,5 +75,46 @@ public async Task SubmitsTraces()

Assert.True(spans.Count >= 1, $"Expecting at least 1 span, only received {spans.Count}");
}

[Fact]
[Trait("Category", "EndToEnd")]
[Trait("Containers", "Windows")]
public async Task SubmitMetrics()
{
var collectorPort = TcpPortProvider.GetOpenPort();
var webPort = TcpPortProvider.GetOpenPort();
const int expectedMetricRequests = 1;

var testSettings = new TestSettings
{
MetricsSettings = new MetricsSettings { Port = collectorPort },
};

// Using "*" as host requires Administrator. This is needed to make the mock collector endpoint
// accessible to the Windows docker container where the test application is executed by binding
// the endpoint to all network interfaces. In order to do that it is necessary to open the port
// on the firewall.
using var fwPort = FirewallHelper.OpenWinPort(collectorPort, Output);
using var collector = new MockCollector(Output, collectorPort, host: "*");
using var container = await StartContainerAsync(testSettings, webPort);

var client = new HttpClient();

var response = await client.GetAsync($"http://localhost:{webPort}");
var content = await response.Content.ReadAsStringAsync();

Output.WriteLine("Sample response:");
Output.WriteLine(content);

var metricRequests = collector.WaitForMetrics(expectedMetricRequests, TimeSpan.FromSeconds(5));

using (new AssertionScope())
{
metricRequests.Count.Should().BeGreaterThanOrEqualTo(expectedMetricRequests);
var resourceMetrics = metricRequests.SelectMany(r => r.ResourceMetrics).Where(s => s.ScopeMetrics.Count > 0).FirstOrDefault();
var aspnetMetrics = resourceMetrics.ScopeMetrics.Should().ContainSingle(x => x.Scope.Name == "OpenTelemetry.Instrumentation.AspNet").Which.Metrics;
aspnetMetrics.Should().ContainSingle(x => x.Name == "http.server.duration");
}
}
}
#endif
5 changes: 3 additions & 2 deletions test/IntegrationTests/Helpers/MockCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class MockCollector : IDisposable
private readonly HttpListener _listener;
private readonly Thread _listenerThread;

public MockCollector(ITestOutputHelper output, int port = 4318, int retries = 5)
public MockCollector(ITestOutputHelper output, int port = 4318, int retries = 5, string host = "127.0.0.1")
{
_output = output;

Expand All @@ -51,7 +51,8 @@ public MockCollector(ITestOutputHelper output, int port = 4318, int retries = 5)
try
{
listener.Start();
listener.Prefixes.Add($"http://127.0.0.1:{port}/");
string prefix = new UriBuilder("http", host, port).ToString();
listener.Prefixes.Add(prefix);

// successfully listening
Port = port;
Expand Down
30 changes: 22 additions & 8 deletions test/IntegrationTests/Helpers/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,14 @@ protected TestHelper(EnvironmentHelper environmentHelper, ITestOutputHelper outp

protected ITestOutputHelper Output { get; }

public async Task<Container> StartContainerAsync(int traceAgentPort, int webPort)
public async Task<Container> StartContainerAsync(TestSettings testSettings, int webPort)
{
// get path to test application that the profiler will attach to
string testApplicationName = $"testapplication-{EnvironmentHelper.TestApplicationName.ToLowerInvariant()}";

string agentBaseUrl = $"http://{DockerNetworkHelper.IntegrationTestsGateway}:{traceAgentPort}";
string agentHealthzUrl = $"{agentBaseUrl}/healthz";
string zipkinEndpoint = $"{agentBaseUrl}/api/v2/spans";
string networkName = DockerNetworkHelper.IntegrationTestsNetworkName;
string networkId = await DockerNetworkHelper.SetupIntegrationTestsNetworkAsync();

Output.WriteLine($"Zipkin Endpoint: {zipkinEndpoint}");

string logPath = EnvironmentHelper.IsRunningOnCI()
? Path.Combine(Environment.GetEnvironmentVariable("GITHUB_WORKSPACE"), "build_data", "profiler-logs")
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), @"OpenTelemetry .NET AutoInstrumentation", "logs");
Expand All @@ -73,17 +68,36 @@ public async Task<Container> StartContainerAsync(int traceAgentPort, int webPort

Output.WriteLine("Collecting docker logs to: " + logPath);

var agentPort = testSettings.TracesSettings?.Port ?? testSettings.MetricsSettings?.Port;
var builder = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage(testApplicationName)
.WithCleanUp(cleanUp: true)
.WithOutputConsumer(Consume.RedirectStdoutAndStderrToConsole())
.WithName($"{testApplicationName}-{traceAgentPort}-{webPort}")
.WithName($"{testApplicationName}-{agentPort}-{webPort}")
.WithNetwork(networkId, networkName)
.WithPortBinding(webPort, 80)
.WithEnvironment("OTEL_EXPORTER_ZIPKIN_ENDPOINT", zipkinEndpoint)
.WithBindMount(logPath, "c:/inetpub/wwwroot/logs")
.WithBindMount(EnvironmentHelper.GetNukeBuildOutput(), "c:/opentelemetry");

string agentBaseUrl = $"http://{DockerNetworkHelper.IntegrationTestsGateway}:{agentPort}";
string agentHealthzUrl = $"{agentBaseUrl}/healthz";

if (testSettings.TracesSettings != null)
{
string zipkinEndpoint = $"{agentBaseUrl}/api/v2/spans";
Output.WriteLine($"Zipkin Endpoint: {zipkinEndpoint}");

builder = builder.WithEnvironment("OTEL_EXPORTER_ZIPKIN_ENDPOINT", zipkinEndpoint);
}

if (testSettings.MetricsSettings != null)
{
Output.WriteLine($"Otlp Endpoint: {agentBaseUrl}");
builder = builder.WithEnvironment("OTEL_EXPORTER_OTLP_ENDPOINT", agentBaseUrl);
builder = builder.WithEnvironment("OTEL_METRIC_EXPORT_INTERVAL", "1000");
builder = builder.WithEnvironment("OTEL_DOTNET_AUTO_METRICS_ENABLED_INSTRUMENTATIONS", "AspNet");
}

var container = builder.Build();
var wasStarted = container.StartAsync().Wait(TimeSpan.FromMinutes(5));

Expand Down

0 comments on commit 94fbe95

Please sign in to comment.