Refine MockMetricsCollector and metrics tests (#1276)
* Refine MockMetricsCollector and metrics tests * Add missing metrics expectation in AspNetTests * Fix checking InstrumentationScopeName * Remove space * Fix AssertExpectations and cleanup * Fix missingExpectations loop * Do not expect OpenTelemetry.Instrumentation.Http in AspNetTests * Refine AssertExpectations in MockMetricsCollector * Refine looping logic in AssertExpectations * Fix race condition * Better comment
This commit is contained in:
parent
146709ac16
commit
ad16e9c11e
|
|
@ -16,12 +16,14 @@
|
|||
|
||||
#if NETFRAMEWORK
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using DotNet.Testcontainers.Builders;
|
||||
using DotNet.Testcontainers.Containers;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Execution;
|
||||
using IntegrationTests.Helpers;
|
||||
using IntegrationTests.Helpers.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
|
|
@ -74,18 +76,17 @@ public class AspNetTests : TestHelper
|
|||
[Trait("Containers", "Windows")]
|
||||
public async Task SubmitMetrics()
|
||||
{
|
||||
// Helps to reduce noise by enabling only AspNet metrics.
|
||||
SetEnvironmentVariable("OTEL_DOTNET_AUTO_METRICS_ENABLED_INSTRUMENTATIONS", "AspNet");
|
||||
|
||||
Assert.True(EnvironmentTools.IsWindowsAdministrator(), "This test requires Windows Administrator privileges.");
|
||||
|
||||
const int expectedMetricRequests = 1;
|
||||
|
||||
// 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 collector = await MockMetricsCollector.Start(Output, host: "*");
|
||||
collector.Expect("OpenTelemetry.Instrumentation.AspNet");
|
||||
|
||||
// Helps to reduce noise by enabling only AspNet metrics.
|
||||
SetEnvironmentVariable("OTEL_DOTNET_AUTO_METRICS_ENABLED_INSTRUMENTATIONS", "AspNet");
|
||||
using var fwPort = FirewallHelper.OpenWinPort(collector.Port, Output);
|
||||
var testSettings = new TestSettings
|
||||
{
|
||||
|
|
@ -95,22 +96,81 @@ public class AspNetTests : TestHelper
|
|||
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);
|
||||
collector.AssertExpectations();
|
||||
}
|
||||
|
||||
using (new AssertionScope())
|
||||
private 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 networkName = DockerNetworkHelper.IntegrationTestsNetworkName;
|
||||
string networkId = await DockerNetworkHelper.SetupIntegrationTestsNetworkAsync();
|
||||
|
||||
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");
|
||||
|
||||
Directory.CreateDirectory(logPath);
|
||||
|
||||
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}-{agentPort}-{webPort}")
|
||||
.WithNetwork(networkId, networkName)
|
||||
.WithPortBinding(webPort, 80)
|
||||
.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)
|
||||
{
|
||||
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");
|
||||
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");
|
||||
}
|
||||
|
||||
foreach (var env in EnvironmentHelper.CustomEnvironmentVariables)
|
||||
{
|
||||
builder = builder.WithEnvironment(env.Key, env.Value);
|
||||
}
|
||||
|
||||
var container = builder.Build();
|
||||
var wasStarted = container.StartAsync().Wait(TimeSpan.FromMinutes(5));
|
||||
|
||||
wasStarted.Should().BeTrue($"Container based on {testApplicationName} has to be operational for the test.");
|
||||
|
||||
Output.WriteLine($"Container was started successfully.");
|
||||
|
||||
PowershellHelper.RunCommand($"docker exec {container.Name} curl -v {agentHealthzUrl}", Output);
|
||||
|
||||
var webAppHealthzUrl = $"http://localhost:{webPort}/healthz";
|
||||
var webAppHealthzResult = await HealthzHelper.TestHealtzAsync(webAppHealthzUrl, "IIS WebApp", Output);
|
||||
|
||||
webAppHealthzResult.Should().BeTrue("IIS WebApp health check never returned OK.");
|
||||
|
||||
Output.WriteLine($"IIS WebApp was started successfully.");
|
||||
|
||||
return new Container(container);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -70,13 +70,15 @@ public class MockLogsCollector : IDisposable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
_listener.Dispose();
|
||||
WriteOutput($"Shutting down. Total logs requests received: '{_logs.Count}'");
|
||||
_logs.Dispose();
|
||||
_listener.Dispose();
|
||||
}
|
||||
|
||||
public void Expect(Func<global::OpenTelemetry.Proto.Logs.V1.LogRecord, bool> predicate, string description = null)
|
||||
{
|
||||
description ??= "<no description>";
|
||||
|
||||
_expectations.Add(new Expectation { Predicate = predicate, Description = description });
|
||||
}
|
||||
|
||||
|
|
@ -90,31 +92,6 @@ public class MockLogsCollector : IDisposable
|
|||
var missingExpectations = new List<Expectation>(_expectations);
|
||||
var expectationsMet = new List<global::OpenTelemetry.Proto.Logs.V1.LogRecord>();
|
||||
var additionalEntries = new List<global::OpenTelemetry.Proto.Logs.V1.LogRecord>();
|
||||
var fail = () =>
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
message.AppendLine();
|
||||
|
||||
message.AppendLine("Missing expectations:");
|
||||
foreach (var logline in missingExpectations)
|
||||
{
|
||||
message.AppendLine($" - \"{logline.Description ?? "<no description>"}\"");
|
||||
}
|
||||
|
||||
message.AppendLine("Entries meeting expectations:");
|
||||
foreach (var logline in expectationsMet)
|
||||
{
|
||||
message.AppendLine($" \"{logline}\"");
|
||||
}
|
||||
|
||||
message.AppendLine("Additional entries:");
|
||||
foreach (var logline in additionalEntries)
|
||||
{
|
||||
message.AppendLine($" + \"{logline}\"");
|
||||
}
|
||||
|
||||
Assert.Fail(message.ToString());
|
||||
};
|
||||
|
||||
timeout ??= DefaultWaitTimeout;
|
||||
var cts = new CancellationTokenSource();
|
||||
|
|
@ -125,7 +102,7 @@ public class MockLogsCollector : IDisposable
|
|||
foreach (var logRecord in _logs.GetConsumingEnumerable(cts.Token))
|
||||
{
|
||||
bool found = false;
|
||||
for (int i = 0; i < missingExpectations.Count; i++)
|
||||
for (int i = missingExpectations.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!missingExpectations[i].Predicate(logRecord))
|
||||
{
|
||||
|
|
@ -135,6 +112,7 @@ public class MockLogsCollector : IDisposable
|
|||
expectationsMet.Add(logRecord);
|
||||
missingExpectations.RemoveAt(i);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!found)
|
||||
|
|
@ -147,7 +125,7 @@ public class MockLogsCollector : IDisposable
|
|||
{
|
||||
if (IsStrict && additionalEntries.Count > 0)
|
||||
{
|
||||
fail();
|
||||
FailExpectations(missingExpectations, expectationsMet, additionalEntries);
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
@ -157,15 +135,44 @@ public class MockLogsCollector : IDisposable
|
|||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
// CancelAfter called with non-positive value
|
||||
fail();
|
||||
FailExpectations(missingExpectations, expectationsMet, additionalEntries);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// timeout
|
||||
fail();
|
||||
FailExpectations(missingExpectations, expectationsMet, additionalEntries);
|
||||
}
|
||||
}
|
||||
|
||||
private static void FailExpectations(
|
||||
List<Expectation> missingExpectations,
|
||||
List<global::OpenTelemetry.Proto.Logs.V1.LogRecord> expectationsMet,
|
||||
List<global::OpenTelemetry.Proto.Logs.V1.LogRecord> additionalEntries)
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
message.AppendLine();
|
||||
|
||||
message.AppendLine("Missing expectations:");
|
||||
foreach (var logline in missingExpectations)
|
||||
{
|
||||
message.AppendLine($" - \"{logline.Description}\"");
|
||||
}
|
||||
|
||||
message.AppendLine("Entries meeting expectations:");
|
||||
foreach (var logline in expectationsMet)
|
||||
{
|
||||
message.AppendLine($" \"{logline}\"");
|
||||
}
|
||||
|
||||
message.AppendLine("Additional entries:");
|
||||
foreach (var logline in additionalEntries)
|
||||
{
|
||||
message.AppendLine($" + \"{logline}\"");
|
||||
}
|
||||
|
||||
Assert.Fail(message.ToString());
|
||||
}
|
||||
|
||||
private void HandleHttpRequests(HttpListenerContext ctx)
|
||||
{
|
||||
if (ctx.Request.RawUrl.Equals("/v1/logs", StringComparison.OrdinalIgnoreCase))
|
||||
|
|
|
|||
|
|
@ -15,17 +15,17 @@
|
|||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using IntegrationTests.Helpers.Models;
|
||||
using OpenTelemetry.Proto.Collector.Metrics.V1;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace IntegrationTests.Helpers;
|
||||
|
|
@ -34,9 +34,10 @@ public class MockMetricsCollector : IDisposable
|
|||
{
|
||||
private static readonly TimeSpan DefaultWaitTimeout = TimeSpan.FromMinutes(1);
|
||||
|
||||
private readonly object _syncRoot = new object();
|
||||
private readonly ITestOutputHelper _output;
|
||||
private readonly TestHttpListener _listener;
|
||||
private readonly BlockingCollection<global::OpenTelemetry.Proto.Metrics.V1.ResourceMetrics> _metrics = new(10); // bounded to avoid memory leak
|
||||
private readonly List<Expectation> _expectations = new();
|
||||
|
||||
private MockMetricsCollector(ITestOutputHelper output, string host = "localhost")
|
||||
{
|
||||
|
|
@ -44,28 +45,15 @@ public class MockMetricsCollector : IDisposable
|
|||
_listener = new(output, HandleHttpRequests, host);
|
||||
}
|
||||
|
||||
public event EventHandler<EventArgs<HttpListenerContext>> RequestReceived;
|
||||
|
||||
public event EventHandler<EventArgs<ExportMetricsServiceRequest>> RequestDeserialized;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to skip deserialization of metrics.
|
||||
/// </summary>
|
||||
public bool ShouldDeserializeMetrics { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the TCP port that this collector is listening on.
|
||||
/// </summary>
|
||||
public int Port { get => _listener.Port; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filters used to filter out metrics we don't want to look at for a test.
|
||||
/// IsStrict defines if all entries must be expected.
|
||||
/// </summary>
|
||||
public List<Func<ExportMetricsServiceRequest, bool>> MetricFilters { get; private set; } = new List<Func<ExportMetricsServiceRequest, bool>>();
|
||||
|
||||
private IImmutableList<ExportMetricsServiceRequest> MetricsMessages { get; set; } = ImmutableList<ExportMetricsServiceRequest>.Empty;
|
||||
|
||||
private IImmutableList<NameValueCollection> RequestHeaders { get; set; } = ImmutableList<NameValueCollection>.Empty;
|
||||
public bool IsStrict { get; set; }
|
||||
|
||||
public static async Task<MockMetricsCollector> Start(ITestOutputHelper output, string host = "localhost")
|
||||
{
|
||||
|
|
@ -82,77 +70,146 @@ public class MockMetricsCollector : IDisposable
|
|||
return collector;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for the given number of metric requests to appear.
|
||||
/// </summary>
|
||||
/// <param name="count">The expected number of metric requests.</param>
|
||||
/// <param name="timeout">The timeout</param>
|
||||
/// <returns>The list of metric requests.</returns>
|
||||
public IImmutableList<ExportMetricsServiceRequest> WaitForMetrics(
|
||||
int count,
|
||||
TimeSpan? timeout = null)
|
||||
{
|
||||
timeout ??= DefaultWaitTimeout;
|
||||
var deadline = DateTime.Now.Add(timeout.Value);
|
||||
|
||||
IImmutableList<ExportMetricsServiceRequest> relevantMetricRequests = ImmutableList<ExportMetricsServiceRequest>.Empty;
|
||||
|
||||
while (DateTime.Now < deadline)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
relevantMetricRequests =
|
||||
MetricsMessages
|
||||
.Where(m => MetricFilters.All(shouldReturn => shouldReturn(m)))
|
||||
.ToImmutableList();
|
||||
}
|
||||
|
||||
if (relevantMetricRequests.Count >= count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
|
||||
return relevantMetricRequests;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
WriteOutput($"Shutting down. Total metric requests received: '{MetricsMessages.Count}'");
|
||||
}
|
||||
|
||||
WriteOutput($"Shutting down.");
|
||||
_metrics.Dispose();
|
||||
_listener.Dispose();
|
||||
}
|
||||
|
||||
protected virtual void OnRequestReceived(HttpListenerContext context)
|
||||
public void Expect(string instrumentationScopeName, Func<global::OpenTelemetry.Proto.Metrics.V1.Metric, bool> predicate = null, string description = null)
|
||||
{
|
||||
RequestReceived?.Invoke(this, new EventArgs<HttpListenerContext>(context));
|
||||
predicate ??= x => true;
|
||||
description ??= instrumentationScopeName;
|
||||
|
||||
_expectations.Add(new Expectation { InstrumentationScopeName = instrumentationScopeName, Predicate = predicate, Description = description });
|
||||
}
|
||||
|
||||
protected virtual void OnRequestDeserialized(ExportMetricsServiceRequest metricsRequest)
|
||||
public void AssertExpectations(TimeSpan? timeout = null)
|
||||
{
|
||||
RequestDeserialized?.Invoke(this, new EventArgs<ExportMetricsServiceRequest>(metricsRequest));
|
||||
if (_expectations.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Expectations were not set");
|
||||
}
|
||||
|
||||
var missingExpectations = new List<Expectation>(_expectations);
|
||||
var expectationsMet = new List<Collected>();
|
||||
var additionalEntries = new List<Collected>();
|
||||
|
||||
timeout ??= DefaultWaitTimeout;
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
cts.CancelAfter(timeout.Value);
|
||||
|
||||
// loop until expectations met or timeout
|
||||
while (true)
|
||||
{
|
||||
var resourceMetrics = _metrics.Take(cts.Token); // get the metrics snapshot
|
||||
|
||||
missingExpectations = new List<Expectation>(_expectations);
|
||||
expectationsMet = new List<Collected>();
|
||||
additionalEntries = new List<Collected>();
|
||||
|
||||
foreach (var scopeMetrics in resourceMetrics.ScopeMetrics)
|
||||
{
|
||||
foreach (var metric in scopeMetrics.Metrics)
|
||||
{
|
||||
var colleted = new Collected
|
||||
{
|
||||
InstrumentationScopeName = scopeMetrics.Scope.Name,
|
||||
Metric = metric
|
||||
};
|
||||
|
||||
bool found = false;
|
||||
for (int i = missingExpectations.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (colleted.InstrumentationScopeName != missingExpectations[i].InstrumentationScopeName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!missingExpectations[i].Predicate(colleted.Metric))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
expectationsMet.Add(colleted);
|
||||
missingExpectations.RemoveAt(i);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
additionalEntries.Add(colleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingExpectations.Count == 0)
|
||||
{
|
||||
if (IsStrict && additionalEntries.Count > 0)
|
||||
{
|
||||
FailExpectations(missingExpectations, expectationsMet, additionalEntries);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
// CancelAfter called with non-positive value
|
||||
FailExpectations(missingExpectations, expectationsMet, additionalEntries);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// timeout
|
||||
FailExpectations(missingExpectations, expectationsMet, additionalEntries);
|
||||
}
|
||||
}
|
||||
|
||||
private static void FailExpectations(
|
||||
List<Expectation> missingExpectations,
|
||||
List<Collected> expectationsMet,
|
||||
List<Collected> additionalEntries)
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
message.AppendLine();
|
||||
|
||||
message.AppendLine("Missing expectations:");
|
||||
foreach (var logline in missingExpectations)
|
||||
{
|
||||
message.AppendLine($" - \"{logline.Description}\"");
|
||||
}
|
||||
|
||||
message.AppendLine("Entries meeting expectations:");
|
||||
foreach (var logline in expectationsMet)
|
||||
{
|
||||
message.AppendLine($" \"{logline}\"");
|
||||
}
|
||||
|
||||
message.AppendLine("Additional entries:");
|
||||
foreach (var logline in additionalEntries)
|
||||
{
|
||||
message.AppendLine($" + \"{logline}\"");
|
||||
}
|
||||
|
||||
Assert.Fail(message.ToString());
|
||||
}
|
||||
|
||||
private void HandleHttpRequests(HttpListenerContext ctx)
|
||||
{
|
||||
OnRequestReceived(ctx);
|
||||
|
||||
if (ctx.Request.RawUrl.Equals("/v1/metrics", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (ShouldDeserializeMetrics)
|
||||
var metricsMessage = ExportMetricsServiceRequest.Parser.ParseFrom(ctx.Request.InputStream);
|
||||
if (metricsMessage.ResourceMetrics != null)
|
||||
{
|
||||
var metricsMessage = ExportMetricsServiceRequest.Parser.ParseFrom(ctx.Request.InputStream);
|
||||
OnRequestDeserialized(metricsMessage);
|
||||
|
||||
lock (_syncRoot)
|
||||
foreach (var metrics in metricsMessage.ResourceMetrics)
|
||||
{
|
||||
MetricsMessages = MetricsMessages.Add(metricsMessage);
|
||||
RequestHeaders = RequestHeaders.Add(new NameValueCollection(ctx.Request.Headers));
|
||||
_metrics.Add(metrics);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,4 +234,25 @@ public class MockMetricsCollector : IDisposable
|
|||
const string name = nameof(MockMetricsCollector);
|
||||
_output.WriteLine($"[{name}]: {msg}");
|
||||
}
|
||||
|
||||
private class Expectation
|
||||
{
|
||||
public string InstrumentationScopeName { get; set; }
|
||||
|
||||
public Func<global::OpenTelemetry.Proto.Metrics.V1.Metric, bool> Predicate { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
private class Collected
|
||||
{
|
||||
public string InstrumentationScopeName { get; set; }
|
||||
|
||||
public global::OpenTelemetry.Proto.Metrics.V1.Metric Metric { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"InstrumentationScopeName = {InstrumentationScopeName}, Metric = {Metric}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,7 @@ using System;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using DotNet.Testcontainers.Builders;
|
||||
using DotNet.Testcontainers.Containers;
|
||||
using FluentAssertions;
|
||||
using IntegrationTests.Helpers.Models;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace IntegrationTests.Helpers;
|
||||
|
|
@ -65,75 +61,6 @@ public abstract class TestHelper
|
|||
#endif
|
||||
}
|
||||
|
||||
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 networkName = DockerNetworkHelper.IntegrationTestsNetworkName;
|
||||
string networkId = await DockerNetworkHelper.SetupIntegrationTestsNetworkAsync();
|
||||
|
||||
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");
|
||||
|
||||
Directory.CreateDirectory(logPath);
|
||||
|
||||
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}-{agentPort}-{webPort}")
|
||||
.WithNetwork(networkId, networkName)
|
||||
.WithPortBinding(webPort, 80)
|
||||
.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");
|
||||
}
|
||||
|
||||
foreach (var env in EnvironmentHelper.CustomEnvironmentVariables)
|
||||
{
|
||||
builder = builder.WithEnvironment(env.Key, env.Value);
|
||||
}
|
||||
|
||||
var container = builder.Build();
|
||||
var wasStarted = container.StartAsync().Wait(TimeSpan.FromMinutes(5));
|
||||
|
||||
wasStarted.Should().BeTrue($"Container based on {testApplicationName} has to be operational for the test.");
|
||||
|
||||
Output.WriteLine($"Container was started successfully.");
|
||||
|
||||
PowershellHelper.RunCommand($"docker exec {container.Name} curl -v {agentHealthzUrl}", Output);
|
||||
|
||||
var webAppHealthzUrl = $"http://localhost:{webPort}/healthz";
|
||||
var webAppHealthzResult = await HealthzHelper.TestHealtzAsync(webAppHealthzUrl, "IIS WebApp", Output);
|
||||
|
||||
webAppHealthzResult.Should().BeTrue("IIS WebApp health check never returned OK.");
|
||||
|
||||
Output.WriteLine($"IIS WebApp was started successfully.");
|
||||
|
||||
return new Container(container);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// StartTestApplication starts the test application
|
||||
/// and returns the Process instance for further interaction.
|
||||
|
|
|
|||
|
|
@ -126,6 +126,12 @@ public class TestHttpListener : IDisposable
|
|||
{
|
||||
// we don't care about any exception when listener is stopped
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// somethig unexpected happened
|
||||
// log instead of crashing the thread
|
||||
WriteOutput(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,49 +93,13 @@ public class HttpTests : TestHelper
|
|||
[Trait("Category", "EndToEnd")]
|
||||
public async Task SubmitMetrics()
|
||||
{
|
||||
const int expectedMetricRequests = 1;
|
||||
|
||||
using var collector = await MockMetricsCollector.Start(Output);
|
||||
collector.Expect("OpenTelemetry.Instrumentation.Http");
|
||||
collector.Expect("OpenTelemetry.Instrumentation.AspNetCore");
|
||||
|
||||
RunTestApplication(metricsAgentPort: collector.Port, enableClrProfiler: !IsCoreClr());
|
||||
var metricRequests = collector.WaitForMetrics(expectedMetricRequests);
|
||||
|
||||
using (new AssertionScope())
|
||||
{
|
||||
metricRequests.Count.Should().Be(expectedMetricRequests);
|
||||
|
||||
var resourceMetrics = metricRequests.Single().ResourceMetrics.Single();
|
||||
|
||||
var expectedServiceNameAttribute = new KeyValue { Key = "service.name", Value = new AnyValue { StringValue = ServiceName } };
|
||||
resourceMetrics.Resource.Attributes.Should().ContainEquivalentOf(expectedServiceNameAttribute);
|
||||
|
||||
var httpclientScope = resourceMetrics.ScopeMetrics.Single(rm => rm.Scope.Name.Equals("OpenTelemetry.Instrumentation.Http", StringComparison.OrdinalIgnoreCase));
|
||||
var aspnetcoreScope = resourceMetrics.ScopeMetrics.Single(rm => rm.Scope.Name.Equals("OpenTelemetry.Instrumentation.AspNetCore", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var httpClientDurationMetric = httpclientScope.Metrics.FirstOrDefault(m => m.Name.Equals("http.client.duration", StringComparison.OrdinalIgnoreCase));
|
||||
var httpServerDurationMetric = aspnetcoreScope.Metrics.FirstOrDefault(m => m.Name.Equals("http.server.duration", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
httpClientDurationMetric.Should().NotBeNull();
|
||||
httpServerDurationMetric.Should().NotBeNull();
|
||||
|
||||
httpClientDurationMetric.DataCase.Should().Be(OpenTelemetry.Proto.Metrics.V1.Metric.DataOneofCase.Histogram);
|
||||
httpServerDurationMetric.DataCase.Should().Be(OpenTelemetry.Proto.Metrics.V1.Metric.DataOneofCase.Histogram);
|
||||
|
||||
var httpClientDurationAttributes = httpClientDurationMetric.Histogram.DataPoints.Single().Attributes;
|
||||
var httpServerDurationAttributes = httpServerDurationMetric.Histogram.DataPoints.Single().Attributes;
|
||||
|
||||
httpClientDurationAttributes.Count.Should().Be(4);
|
||||
httpClientDurationAttributes.Single(a => a.Key == "http.method").Value.StringValue.Should().Be("GET");
|
||||
httpClientDurationAttributes.Single(a => a.Key == "http.scheme").Value.StringValue.Should().Be("http");
|
||||
httpClientDurationAttributes.Single(a => a.Key == "http.flavor").Value.StringValue.Should().Be("1.1");
|
||||
httpClientDurationAttributes.Single(a => a.Key == "http.status_code").Value.IntValue.Should().Be(200);
|
||||
|
||||
httpServerDurationAttributes.Count.Should().Be(5);
|
||||
httpServerDurationAttributes.Single(a => a.Key == "http.method").Value.StringValue.Should().Be("GET");
|
||||
httpClientDurationAttributes.Single(a => a.Key == "http.scheme").Value.StringValue.Should().Be("http");
|
||||
httpClientDurationAttributes.Single(a => a.Key == "http.flavor").Value.StringValue.Should().Be("1.1");
|
||||
httpServerDurationAttributes.Single(a => a.Key == "http.host").Value.StringValue.Should().StartWith("localhost");
|
||||
httpClientDurationAttributes.Single(a => a.Key == "http.status_code").Value.IntValue.Should().Be(200);
|
||||
}
|
||||
collector.AssertExpectations();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using IntegrationTests.Helpers;
|
||||
|
|
@ -48,13 +46,12 @@ public class PluginsTests : TestHelper
|
|||
[Trait("Category", "EndToEnd")]
|
||||
public async Task SubmitMetrics()
|
||||
{
|
||||
SetEnvironmentVariable("OTEL_DOTNET_AUTO_PLUGINS", "TestApplication.Plugins.Plugin, TestApplication.Plugins, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
|
||||
|
||||
using var collector = await MockMetricsCollector.Start(Output);
|
||||
RunTestApplication(metricsAgentPort: collector.Port);
|
||||
var metricRequests = collector.WaitForMetrics(1);
|
||||
collector.Expect("MyCompany.MyProduct.MyLibrary");
|
||||
|
||||
var metrics = metricRequests.Should().NotBeEmpty().And.Subject.First().ResourceMetrics.Should().ContainSingle().Subject.ScopeMetrics;
|
||||
metrics.Should().Contain(x => x.Scope.Name == "MyCompany.MyProduct.MyLibrary");
|
||||
SetEnvironmentVariable("OTEL_DOTNET_AUTO_PLUGINS", "TestApplication.Plugins.Plugin, TestApplication.Plugins, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
|
||||
RunTestApplication(metricsAgentPort: collector.Port);
|
||||
|
||||
collector.AssertExpectations();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,7 @@
|
|||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Extensions;
|
||||
using IntegrationTests.Helpers;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
|
@ -39,20 +35,12 @@ public class RuntimeTests : TestHelper
|
|||
public async Task SubmitMetrics()
|
||||
{
|
||||
using var collector = await MockMetricsCollector.Start(Output);
|
||||
using var process = StartTestApplication(metricsAgentPort: collector.Port, enableClrProfiler: !IsCoreClr());
|
||||
collector.Expect("OpenTelemetry.Instrumentation.Runtime");
|
||||
|
||||
using var process = StartTestApplication(metricsAgentPort: collector.Port, enableClrProfiler: !IsCoreClr());
|
||||
try
|
||||
{
|
||||
var assert = () =>
|
||||
{
|
||||
var metricRequests = collector.WaitForMetrics(1);
|
||||
var metrics = metricRequests.SelectMany(r => r.ResourceMetrics).Where(s => s.ScopeMetrics.Count > 0).FirstOrDefault();
|
||||
metrics.ScopeMetrics.Should().ContainSingle(x => x.Scope.Name == "OpenTelemetry.Instrumentation.Runtime");
|
||||
};
|
||||
|
||||
assert.Should().NotThrowAfter(
|
||||
waitTime: 30.Seconds(),
|
||||
pollInterval: 1.Seconds());
|
||||
collector.AssertExpectations();
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
|
@ -22,12 +21,10 @@ using System.Net;
|
|||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Execution;
|
||||
using FluentAssertions.Extensions;
|
||||
using IntegrationTests.Helpers;
|
||||
using IntegrationTests.Helpers.Mocks;
|
||||
using IntegrationTests.Helpers.Models;
|
||||
using OpenTelemetry.Proto.Common.V1;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
|
|
@ -121,32 +118,13 @@ public class SmokeTests : TestHelper
|
|||
[Trait("Category", "EndToEnd")]
|
||||
public async Task SubmitMetrics()
|
||||
{
|
||||
SetEnvironmentVariable("OTEL_DOTNET_AUTO_METRICS_ADDITIONAL_SOURCES", "MyCompany.MyProduct.MyLibrary");
|
||||
const int expectedMetricRequests = 1;
|
||||
|
||||
using var collector = await MockMetricsCollector.Start(Output);
|
||||
collector.Expect("MyCompany.MyProduct.MyLibrary", metric => metric.Name == "MyFruitCounter");
|
||||
|
||||
SetEnvironmentVariable("OTEL_DOTNET_AUTO_METRICS_ADDITIONAL_SOURCES", "MyCompany.MyProduct.MyLibrary");
|
||||
RunTestApplication(metricsAgentPort: collector.Port);
|
||||
var metricRequests = collector.WaitForMetrics(expectedMetricRequests);
|
||||
|
||||
using (new AssertionScope())
|
||||
{
|
||||
metricRequests.Count.Should().Be(expectedMetricRequests);
|
||||
|
||||
var resourceMetrics = metricRequests.Single().ResourceMetrics.Single();
|
||||
|
||||
var expectedServiceNameAttribute = new KeyValue { Key = "service.name", Value = new AnyValue { StringValue = ServiceName } };
|
||||
resourceMetrics.Resource.Attributes.Should().ContainEquivalentOf(expectedServiceNameAttribute);
|
||||
|
||||
var customClientScope = resourceMetrics.ScopeMetrics.Single(rm => rm.Scope.Name.Equals("MyCompany.MyProduct.MyLibrary", StringComparison.OrdinalIgnoreCase));
|
||||
var myFruitCounterMetric = customClientScope.Metrics.FirstOrDefault(m => m.Name.Equals("MyFruitCounter", StringComparison.OrdinalIgnoreCase));
|
||||
myFruitCounterMetric.Should().NotBeNull();
|
||||
myFruitCounterMetric.DataCase.Should().Be(OpenTelemetry.Proto.Metrics.V1.Metric.DataOneofCase.Sum);
|
||||
myFruitCounterMetric.Sum.DataPoints.Count.Should().Be(1);
|
||||
|
||||
var myFruitCounterAttributes = myFruitCounterMetric.Sum.DataPoints[0].Attributes;
|
||||
myFruitCounterAttributes.Count.Should().Be(1);
|
||||
myFruitCounterAttributes.Single(a => a.Key == "name").Value.StringValue.Should().Be("apple");
|
||||
}
|
||||
collector.AssertExpectations();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Reference in New Issue