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:
Robert Pająk 2022-09-27 01:51:09 +02:00 committed by GitHub
parent 146709ac16
commit ad16e9c11e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 284 additions and 279 deletions

View File

@ -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

View File

@ -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))

View File

@ -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}";
}
}
}

View File

@ -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.

View File

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

View File

@ -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

View File

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

View File

@ -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
{

View File

@ -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]