[Exporter.Prometheus] - enable analysis (#6171)

Co-authored-by: Rajkumar Rangaraj <rajrang@microsoft.com>
This commit is contained in:
Piotr Kiełkowicz 2025-03-13 22:43:18 +01:00 committed by GitHub
parent 5635e6b8d6
commit a8f1076e4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 133 additions and 101 deletions

View File

@ -160,6 +160,9 @@ dotnet_diagnostic.RS0041.severity = suggestion
# CA1515: Disable making types internal for Tests classes. It is required by xunit
dotnet_diagnostic.CA1515.severity = none
# CA2007: Disable Consider calling ConfigureAwait on the awaited task. It is not working with xunit
dotnet_diagnostic.CA2007.severity = none
[**/obj/**.cs]
generated_code = true

View File

@ -7,6 +7,7 @@
<PackageTags>$(PackageTags);prometheus;metrics</PackageTags>
<MinVerTagPrefix>coreunstable-</MinVerTagPrefix>
<DefineConstants>$(DefineConstants);PROMETHEUS_ASPNETCORE</DefineConstants>
<AnalysisLevel>latest-all</AnalysisLevel>
</PropertyGroup>
<ItemGroup Condition="'$(RunningDotNetPack)' != 'true'">

View File

@ -99,6 +99,8 @@ public static class PrometheusExporterApplicationBuilderExtensions
Action<IApplicationBuilder>? configureBranchedPipeline,
string? optionsName)
{
Guard.ThrowIfNull(app);
// Note: Order is important here. MeterProvider is accessed before
// GetOptions<PrometheusAspNetCoreOptions> so that any changes made to
// PrometheusAspNetCoreOptions in deferred AddPrometheusExporter
@ -114,7 +116,7 @@ public static class PrometheusExporterApplicationBuilderExtensions
path = options.ScrapeEndpointPath ?? PrometheusAspNetCoreOptions.DefaultScrapeEndpointPath;
}
if (!path.StartsWith("/"))
if (!path.StartsWith('/'))
{
path = $"/{path}";
}

View File

@ -69,6 +69,8 @@ public static class PrometheusExporterEndpointRouteBuilderExtensions
Action<IApplicationBuilder>? configureBranchedPipeline,
string? optionsName)
{
Guard.ThrowIfNull(endpoints);
var builder = endpoints.CreateApplicationBuilder();
// Note: Order is important here. MeterProvider is accessed before
@ -84,7 +86,7 @@ public static class PrometheusExporterEndpointRouteBuilderExtensions
path = options.ScrapeEndpointPath ?? PrometheusAspNetCoreOptions.DefaultScrapeEndpointPath;
}
if (!path.StartsWith("/"))
if (!path.StartsWith('/'))
{
path = $"/{path}";
}

View File

@ -62,9 +62,11 @@ public static class PrometheusExporterMeterProviderBuilderExtensions
});
}
private static MetricReader BuildPrometheusExporterMetricReader(PrometheusAspNetCoreOptions options)
private static BaseExportingMetricReader BuildPrometheusExporterMetricReader(PrometheusAspNetCoreOptions options)
{
#pragma warning disable CA2000 // Dispose objects before losing scope
var exporter = new PrometheusExporter(options.ExporterOptions);
#pragma warning restore CA2000 // Dispose objects before losing scope
return new BaseExportingMetricReader(exporter)
{

View File

@ -74,7 +74,7 @@ internal sealed class PrometheusExporterMiddleware
? "application/openmetrics-text; version=1.0.0; charset=utf-8"
: "text/plain; charset=utf-8; version=0.0.4";
await response.Body.WriteAsync(dataView.Array!, 0, dataView.Count).ConfigureAwait(false);
await response.Body.WriteAsync(dataView.Array.AsMemory(0, dataView.Count)).ConfigureAwait(false);
}
else
{

View File

@ -34,8 +34,8 @@ internal sealed class PrometheusCollectionManager
this.exporter = exporter;
this.scrapeResponseCacheDurationMilliseconds = this.exporter.ScrapeResponseCacheDurationMilliseconds;
this.onCollectRef = this.OnCollect;
this.metricsCache = new Dictionary<Metric, PrometheusMetric>();
this.scopes = new HashSet<string>();
this.metricsCache = [];
this.scopes = [];
}
#if NET
@ -68,10 +68,7 @@ internal sealed class PrometheusCollectionManager
// If a collection is already running, return a task to wait on the result.
if (this.collectionRunning)
{
if (this.collectionTcs == null)
{
this.collectionTcs = new TaskCompletionSource<CollectionResponse>(TaskCreationOptions.RunContinuationsAsynchronously);
}
this.collectionTcs ??= new TaskCompletionSource<CollectionResponse>(TaskCreationOptions.RunContinuationsAsynchronously);
Interlocked.Increment(ref this.readerCount);
this.ExitGlobalLock();
@ -148,6 +145,22 @@ internal sealed class PrometheusCollectionManager
Interlocked.Decrement(ref this.readerCount);
}
private static bool IncreaseBufferSize(ref byte[] buffer)
{
var newBufferSize = buffer.Length * 2;
if (newBufferSize > 100 * 1024 * 1024)
{
return false;
}
var newBuffer = new byte[newBufferSize];
buffer.CopyTo(newBuffer, 0);
buffer = newBuffer;
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnterGlobalLock()
{
@ -230,7 +243,7 @@ internal sealed class PrometheusCollectionManager
}
catch (IndexOutOfRangeException)
{
if (!this.IncreaseBufferSize(ref buffer))
if (!IncreaseBufferSize(ref buffer))
{
// there are two cases we might run into the following condition:
// 1. we have many metrics to be exported - in this case we probably want
@ -268,7 +281,7 @@ internal sealed class PrometheusCollectionManager
}
catch (IndexOutOfRangeException)
{
if (!this.IncreaseBufferSize(ref buffer))
if (!IncreaseBufferSize(ref buffer))
{
throw;
}
@ -285,7 +298,7 @@ internal sealed class PrometheusCollectionManager
}
catch (IndexOutOfRangeException)
{
if (!this.IncreaseBufferSize(ref buffer))
if (!IncreaseBufferSize(ref buffer))
{
throw;
}
@ -307,11 +320,11 @@ internal sealed class PrometheusCollectionManager
{
if (this.exporter.OpenMetricsRequested)
{
this.previousOpenMetricsDataView = new ArraySegment<byte>(Array.Empty<byte>(), 0, 0);
this.previousOpenMetricsDataView = new ArraySegment<byte>([], 0, 0);
}
else
{
this.previousPlainTextDataView = new ArraySegment<byte>(Array.Empty<byte>(), 0, 0);
this.previousPlainTextDataView = new ArraySegment<byte>([], 0, 0);
}
return ExportResult.Failure;
@ -332,7 +345,7 @@ internal sealed class PrometheusCollectionManager
}
catch (IndexOutOfRangeException)
{
if (!this.IncreaseBufferSize(ref buffer))
if (!IncreaseBufferSize(ref buffer))
{
throw;
}
@ -343,22 +356,6 @@ internal sealed class PrometheusCollectionManager
return this.targetInfoBufferLength;
}
private bool IncreaseBufferSize(ref byte[] buffer)
{
var newBufferSize = buffer.Length * 2;
if (newBufferSize > 100 * 1024 * 1024)
{
return false;
}
var newBuffer = new byte[newBufferSize];
buffer.CopyTo(newBuffer, 0);
buffer = newBuffer;
return true;
}
private PrometheusMetric GetPrometheusMetric(Metric metric)
{
// Optimize writing metrics with bounded cache that has pre-calculated Prometheus names.

View File

@ -16,10 +16,10 @@ internal sealed class PrometheusMetric
UpDownCounter becomes gauge
* https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#otlp-metric-points-to-prometheus
*/
private static readonly PrometheusType[] MetricTypes = new PrometheusType[]
{
private static readonly PrometheusType[] MetricTypes =
[
PrometheusType.Untyped, PrometheusType.Counter, PrometheusType.Gauge, PrometheusType.Summary, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Histogram, PrometheusType.Gauge,
};
];
public PrometheusMetric(string name, string unit, PrometheusType type, bool disableTotalNameSuffixForCounters)
{
@ -40,7 +40,7 @@ internal sealed class PrometheusMetric
// [OpenMetrics UNIT metadata](https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#metricfamily)
// and as a suffix to the metric name. The unit suffix comes before any type-specific suffixes.
// https://github.com/open-telemetry/opentelemetry-specification/blob/3dfb383fe583e3b74a2365c5a1d90256b273ee76/specification/compatibility/prometheus_and_openmetrics.md#metric-metadata-1
if (!sanitizedName.EndsWith(sanitizedUnit))
if (!sanitizedName.EndsWith(sanitizedUnit, StringComparison.Ordinal))
{
sanitizedName += $"_{sanitizedUnit}";
openMetricsName += $"_{sanitizedUnit}";
@ -51,14 +51,14 @@ internal sealed class PrometheusMetric
// Exporters SHOULD provide a configuration option to disable the addition of `_total` suffixes.
// https://github.com/open-telemetry/opentelemetry-specification/blob/b2f923fb1650dde1f061507908b834035506a796/specification/compatibility/prometheus_and_openmetrics.md#L286
// Note that we no longer append '_ratio' for units that are '1', see: https://github.com/open-telemetry/opentelemetry-specification/issues/4058
if (type == PrometheusType.Counter && !sanitizedName.EndsWith("_total") && !disableTotalNameSuffixForCounters)
if (type == PrometheusType.Counter && !sanitizedName.EndsWith("_total", StringComparison.Ordinal) && !disableTotalNameSuffixForCounters)
{
sanitizedName += "_total";
}
// For counters requested using OpenMetrics format, the MetricFamily name MUST be suffixed with '_total', regardless of the setting to disable the 'total' suffix.
// https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#counter-1
if (type == PrometheusType.Counter && !openMetricsName.EndsWith("_total"))
if (type == PrometheusType.Counter && !openMetricsName.EndsWith("_total", StringComparison.Ordinal))
{
openMetricsName += "_total";
}
@ -127,7 +127,7 @@ internal sealed class PrometheusMetric
return sb?.ToString() ?? metricName;
static StringBuilder CreateStringBuilder(string name) => new StringBuilder(name.Length);
static StringBuilder CreateStringBuilder(string name) => new(name.Length);
}
internal static string RemoveAnnotations(string unit)
@ -179,7 +179,7 @@ internal sealed class PrometheusMetric
private static string SanitizeOpenMetricsName(string metricName)
{
if (metricName.EndsWith("_total"))
if (metricName.EndsWith("_total", StringComparison.Ordinal))
{
return metricName.Substring(0, metricName.Length - 6);
}

View File

@ -17,8 +17,6 @@ internal static partial class PrometheusSerializer
{
#pragma warning disable SA1310 // Field name should not contain an underscore
private const byte ASCII_QUOTATION_MARK = 0x22; // '"'
private const byte ASCII_FULL_STOP = 0x2E; // '.'
private const byte ASCII_HYPHEN_MINUS = 0x2D; // '-'
private const byte ASCII_REVERSE_SOLIDUS = 0x5C; // '\\'
private const byte ASCII_LINEFEED = 0x0A; // `\n`
#pragma warning restore SA1310 // Field name should not contain an underscore

View File

@ -5,6 +5,7 @@
<Description>Stand-alone HttpListener for hosting OpenTelemetry .NET Prometheus Exporter</Description>
<PackageTags>$(PackageTags);prometheus;metrics</PackageTags>
<MinVerTagPrefix>coreunstable-</MinVerTagPrefix>
<AnalysisLevel>latest-all</AnalysisLevel>
</PropertyGroup>
<ItemGroup Condition="'$(RunningDotNetPack)' != 'true'">

View File

@ -30,12 +30,20 @@ internal sealed class PrometheusHttpListener : IDisposable
string path = options.ScrapeEndpointPath ?? PrometheusHttpListenerOptions.DefaultScrapeEndpointPath;
if (!path.StartsWith("/"))
#if NET
if (!path.StartsWith('/'))
#else
if (!path.StartsWith("/", StringComparison.Ordinal))
#endif
{
path = $"/{path}";
}
if (!path.EndsWith("/"))
#if NET
if (!path.EndsWith('/'))
#else
if (!path.EndsWith("/", StringComparison.Ordinal))
#endif
{
path = $"{path}/";
}
@ -164,7 +172,11 @@ internal sealed class PrometheusHttpListener : IDisposable
? "application/openmetrics-text; version=1.0.0; charset=utf-8"
: "text/plain; charset=utf-8; version=0.0.4";
await context.Response.OutputStream.WriteAsync(dataView.Array!, 0, dataView.Count).ConfigureAwait(false);
#if NET
await context.Response.OutputStream.WriteAsync(dataView.Array.AsMemory(0, dataView.Count)).ConfigureAwait(false);
#else
await context.Response.OutputStream.WriteAsync(dataView.Array, 0, dataView.Count).ConfigureAwait(false);
#endif
}
else
{

View File

@ -62,7 +62,7 @@ public static class PrometheusHttpListenerMeterProviderBuilderExtensions
});
}
private static MetricReader BuildPrometheusHttpListenerMetricReader(
private static BaseExportingMetricReader BuildPrometheusHttpListenerMetricReader(
PrometheusHttpListenerOptions options)
{
var exporter = new PrometheusExporter(new PrometheusExporterOptions
@ -78,8 +78,10 @@ public static class PrometheusHttpListenerMeterProviderBuilderExtensions
try
{
#pragma warning disable CA2000 // Dispose objects before losing scope
var listener = new PrometheusHttpListener(exporter, options);
exporter.OnDispose = () => listener.Dispose();
#pragma warning restore CA2000 // Dispose objects before losing scope
exporter.OnDispose = listener.Dispose;
listener.Start();
}
catch (Exception ex)

View File

@ -17,7 +17,6 @@ public class BaseExportingMetricReader : MetricReader
/// </summary>
protected readonly BaseExporter<Metric> exporter;
private readonly ExportModes supportedExportModes = ExportModes.Push | ExportModes.Pull;
private readonly string exportCalledMessage;
private readonly string exportSucceededMessage;
private readonly string exportFailedMessage;
@ -38,12 +37,12 @@ public class BaseExportingMetricReader : MetricReader
if (attributes.Length > 0)
{
var attr = (ExportModesAttribute)attributes[attributes.Length - 1];
this.supportedExportModes = attr.Supported;
this.SupportedExportModes = attr.Supported;
}
if (exporter is IPullMetricExporter pullExporter)
{
if (this.supportedExportModes.HasFlag(ExportModes.Push))
if (this.SupportedExportModes.HasFlag(ExportModes.Push))
{
pullExporter.Collect = this.Collect;
}
@ -69,7 +68,7 @@ public class BaseExportingMetricReader : MetricReader
/// <summary>
/// Gets the supported <see cref="ExportModes"/>.
/// </summary>
protected ExportModes SupportedExportModes => this.supportedExportModes;
protected ExportModes SupportedExportModes { get; } = ExportModes.Push | ExportModes.Pull;
internal override void SetParentProvider(BaseProvider parentProvider)
{
@ -106,12 +105,12 @@ public class BaseExportingMetricReader : MetricReader
/// <inheritdoc />
protected override bool OnCollect(int timeoutMilliseconds)
{
if (this.supportedExportModes.HasFlag(ExportModes.Push))
if (this.SupportedExportModes.HasFlag(ExportModes.Push))
{
return base.OnCollect(timeoutMilliseconds);
}
if (this.supportedExportModes.HasFlag(ExportModes.Pull) && PullMetricScope.IsPullAllowed)
if (this.SupportedExportModes.HasFlag(ExportModes.Pull) && PullMetricScope.IsPullAllowed)
{
return base.OnCollect(timeoutMilliseconds);
}

View File

@ -4,6 +4,7 @@
<Description>Unit test project for Prometheus Exporter AspNetCore for OpenTelemetry</Description>
<TargetFrameworks>$(TargetFrameworksForAspNetCoreTests)</TargetFrameworks>
<DefineConstants>$(DefineConstants);PROMETHEUS_ASPNETCORE</DefineConstants>
<AnalysisLevel>latest-all</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
@ -28,7 +29,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Exporter.Prometheus.HttpListener.Tests\EventSourceTest.cs" Link="Includes\EventSourceTest.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Exporter.Prometheus.HttpListener.Tests\EventSourceTests.cs" Link="Includes\EventSourceTests.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Exporter.Prometheus.HttpListener.Tests\PrometheusCollectionManagerTests.cs" Link="Includes\PrometheusCollectionManagerTests.cs" />
<Compile Include="$(RepoRoot)\test\OpenTelemetry.Exporter.Prometheus.HttpListener.Tests\PrometheusSerializerTests.cs" Link="Includes\PrometheusSerializerTests.cs" />

View File

@ -3,6 +3,7 @@
#if !NETFRAMEWORK
using System.Diagnostics.Metrics;
using System.Globalization;
using System.Net;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Builder;
@ -112,7 +113,7 @@ public sealed class PrometheusExporterMiddlewareTests
{
if (!rsp.Headers.TryGetValues("X-MiddlewareExecuted", out IEnumerable<string>? headers))
{
headers = Array.Empty<string>();
headers = [];
}
Assert.Equal("true", headers.FirstOrDefault());
@ -139,7 +140,7 @@ public sealed class PrometheusExporterMiddlewareTests
{
if (!rsp.Headers.TryGetValues("X-MiddlewareExecuted", out IEnumerable<string>? headers))
{
headers = Array.Empty<string>();
headers = [];
}
Assert.Equal("true", headers.FirstOrDefault());
@ -314,7 +315,7 @@ public sealed class PrometheusExporterMiddlewareTests
using var client = host.GetTestClient();
using var response = await client.GetAsync("/metrics");
using var response = await client.GetAsync(new Uri("/metrics", UriKind.Relative));
var text = await response.Content.ReadAsStringAsync();
Assert.NotEmpty(text);
@ -372,7 +373,7 @@ public sealed class PrometheusExporterMiddlewareTests
string acceptHeader = "application/openmetrics-text",
KeyValuePair<string, object?>[]? meterTags = null)
{
var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text");
var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text", StringComparison.Ordinal);
using var host = await StartTestHostAsync(configure, configureServices, registerMeterProvider, configureOptions);
@ -400,7 +401,7 @@ public sealed class PrometheusExporterMiddlewareTests
client.DefaultRequestHeaders.Add("Accept", acceptHeader);
}
using var response = await client.GetAsync(path);
using var response = await client.GetAsync(new Uri(path, UriKind.Relative));
var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
@ -432,7 +433,7 @@ public sealed class PrometheusExporterMiddlewareTests
Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType!.ToString());
}
var additionalTags = meterTags != null && meterTags.Any()
var additionalTags = meterTags is { Length: > 0 }
? $"{string.Join(",", meterTags.Select(x => $"{x.Key}=\"{x.Value}\""))},"
: string.Empty;
@ -464,7 +465,7 @@ public sealed class PrometheusExporterMiddlewareTests
Assert.True(matches.Count == 1, content);
var timestamp = long.Parse(matches[0].Groups[1].Value.Replace(".", string.Empty));
var timestamp = long.Parse(matches[0].Groups[1].Value.Replace(".", string.Empty, StringComparison.Ordinal), CultureInfo.InvariantCulture);
Assert.True(beginTimestamp <= timestamp && timestamp <= endTimestamp, $"{beginTimestamp} {timestamp} {endTimestamp}");
}

View File

@ -6,7 +6,7 @@ using Xunit;
namespace OpenTelemetry.Exporter.Prometheus.Tests;
public class EventSourceTest
public class EventSourceTests
{
[Fact]
public void EventSourceTest_PrometheusExporterEventSource()

View File

@ -4,9 +4,10 @@
<Description>Unit test project for Prometheus Exporter HttpListener for OpenTelemetry</Description>
<TargetFrameworks>$(TargetFrameworksForTests)</TargetFrameworks>
<DefineConstants>$(DefineConstants);PROMETHEUS_HTTP_LISTENER</DefineConstants>
<AnalysisLevel>latest-all</AnalysisLevel>
</PropertyGroup>
<ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="All" />

View File

@ -31,7 +31,9 @@ public sealed class PrometheusCollectionManagerTests
#endif
.Build())
{
#pragma warning disable CA2000 // Dispose objects before losing scope
if (!provider.TryFindExporter(out PrometheusExporter? exporter))
#pragma warning restore CA2000 // Dispose objects before losing scope
{
throw new InvalidOperationException("PrometheusExporter could not be found on MeterProvider.");
}
@ -60,7 +62,7 @@ public sealed class PrometheusCollectionManagerTests
return new Response
{
CollectionResponse = response,
ViewPayload = openMetricsRequested ? response.OpenMetricsView.ToArray() : response.PlainTextView.ToArray(),
ViewPayload = openMetricsRequested ? [.. response.OpenMetricsView] : [.. response.PlainTextView],
};
}
finally
@ -110,7 +112,10 @@ public sealed class PrometheusCollectionManagerTests
exporter.CollectionManager.ExitCollect();
}
#pragma warning disable CA1849 // 'Thread.Sleep(int)' synchronously blocks. Use await instead.
// Changing to await Task.Delay leads to test instability.
Thread.Sleep(exporter.ScrapeResponseCacheDurationMilliseconds);
#pragma warning restore CA1849 // 'Thread.Sleep(int)' synchronously blocks. Use await instead.
counter.Add(100);
@ -118,13 +123,13 @@ public sealed class PrometheusCollectionManagerTests
{
collectTasks[i] = Task.Run(async () =>
{
var response = await exporter.CollectionManager.EnterCollect(openMetricsRequested);
var collectionResponse = await exporter.CollectionManager.EnterCollect(openMetricsRequested);
try
{
return new Response
{
CollectionResponse = response,
ViewPayload = openMetricsRequested ? response.OpenMetricsView.ToArray() : response.PlainTextView.ToArray(),
CollectionResponse = collectionResponse,
ViewPayload = openMetricsRequested ? [.. collectionResponse.OpenMetricsView] : [.. collectionResponse.PlainTextView],
};
}
finally
@ -152,7 +157,7 @@ public sealed class PrometheusCollectionManagerTests
}
}
private class Response
private sealed class Response
{
public PrometheusCollectionManager.CollectionResponse CollectionResponse;

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics.Metrics;
using System.Globalization;
using System.Net;
#if NETFRAMEWORK
using System.Net.Http;
@ -47,7 +48,7 @@ public class PrometheusHttpListenerTests
{
Assert.Throws<ArgumentException>(() =>
{
TestPrometheusHttpListenerUriPrefixOptions(new string[] { });
TestPrometheusHttpListenerUriPrefixOptions([]);
});
}
@ -63,25 +64,25 @@ public class PrometheusHttpListenerTests
[Fact]
public async Task PrometheusExporterHttpServerIntegration()
{
await this.RunPrometheusExporterHttpServerIntegrationTest();
await RunPrometheusExporterHttpServerIntegrationTest();
}
[Fact]
public async Task PrometheusExporterHttpServerIntegration_NoMetrics()
{
await this.RunPrometheusExporterHttpServerIntegrationTest(skipMetrics: true);
await RunPrometheusExporterHttpServerIntegrationTest(skipMetrics: true);
}
[Fact]
public async Task PrometheusExporterHttpServerIntegration_NoOpenMetrics()
{
await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty);
await RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty);
}
[Fact]
public async Task PrometheusExporterHttpServerIntegration_UseOpenMetricsVersionHeader()
{
await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0");
await RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0");
}
[Fact]
@ -93,7 +94,7 @@ public class PrometheusHttpListenerTests
new("meter2", "value2"),
};
await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty, meterTags: tags);
await RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: string.Empty, meterTags: tags);
}
[Fact]
@ -105,7 +106,7 @@ public class PrometheusHttpListenerTests
new("meter2", "value2"),
};
await this.RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0", meterTags: tags);
await RunPrometheusExporterHttpServerIntegrationTest(acceptHeader: "application/openmetrics-text; version=1.0.0", meterTags: tags);
}
[Fact]
@ -113,7 +114,6 @@ public class PrometheusHttpListenerTests
{
Random random = new Random();
int retryAttempts = 5;
int port = 0;
string? address = null;
PrometheusExporter? exporter = null;
@ -122,7 +122,9 @@ public class PrometheusHttpListenerTests
// Step 1: Start a listener on a random port.
while (retryAttempts-- != 0)
{
port = random.Next(2000, 5000);
#pragma warning disable CA5394 // Do not use insecure randomness
int port = random.Next(2000, 5000);
#pragma warning restore CA5394 // Do not use insecure randomness
address = $"http://localhost:{port}/";
try
@ -179,7 +181,7 @@ public class PrometheusHttpListenerTests
var oneKb = new string('A', 1024);
for (var x = 0; x < 8500; x++)
{
attributes.Add(new KeyValuePair<string, object>(x.ToString(), oneKb));
attributes.Add(new KeyValuePair<string, object>(x.ToString(CultureInfo.InvariantCulture), oneKb));
}
var provider = BuildMeterProvider(meter, attributes, out var address);
@ -197,11 +199,11 @@ public class PrometheusHttpListenerTests
client.DefaultRequestHeaders.Add("Accept", acceptHeader);
}
using var response = await client.GetAsync($"{address}metrics");
using var response = await client.GetAsync(new Uri($"{address}metrics"));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("counter_double_999", content);
Assert.Contains("counter_double_999", content, StringComparison.Ordinal);
Assert.DoesNotContain('\0', content);
provider.Dispose();
@ -222,13 +224,14 @@ public class PrometheusHttpListenerTests
{
Random random = new Random();
int retryAttempts = 5;
int port = 0;
string? generatedAddress = null;
MeterProvider? provider = null;
while (retryAttempts-- != 0)
{
port = random.Next(2000, 5000);
#pragma warning disable CA5394 // Do not use insecure randomness
int port = random.Next(2000, 5000);
#pragma warning restore CA5394 // Do not use insecure randomness
generatedAddress = $"http://localhost:{port}/";
try
@ -252,17 +255,12 @@ public class PrometheusHttpListenerTests
address = generatedAddress!;
if (provider == null)
{
throw new InvalidOperationException("HttpListener could not be started");
}
return provider;
return provider ?? throw new InvalidOperationException("HttpListener could not be started");
}
private async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text", KeyValuePair<string, object?>[]? meterTags = null)
private static async Task RunPrometheusExporterHttpServerIntegrationTest(bool skipMetrics = false, string acceptHeader = "application/openmetrics-text", KeyValuePair<string, object?>[]? meterTags = null)
{
var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text");
var requestOpenMetrics = acceptHeader.StartsWith("application/openmetrics-text", StringComparison.Ordinal);
using var meter = new Meter(MeterName, MeterVersion, meterTags);
@ -288,7 +286,7 @@ public class PrometheusHttpListenerTests
client.DefaultRequestHeaders.Add("Accept", acceptHeader);
}
using var response = await client.GetAsync($"{address}metrics");
using var response = await client.GetAsync(new Uri($"{address}metrics"));
if (!skipMetrics)
{
@ -304,7 +302,7 @@ public class PrometheusHttpListenerTests
Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType!.ToString());
}
var additionalTags = meterTags != null && meterTags.Any()
var additionalTags = meterTags is { Length: > 0 }
? $"{string.Join(",", meterTags.Select(x => $"{x.Key}='{x.Value}'"))},"
: string.Empty;

View File

@ -170,7 +170,7 @@ public sealed class LogRecordSharedPoolTests
using BatchLogRecordExportProcessor processor = new(new NoopExporter());
List<Task> tasks = new();
List<Task> tasks = [];
for (int i = 0; i < Environment.ProcessorCount; i++)
{
@ -230,7 +230,7 @@ public sealed class LogRecordSharedPoolTests
var pool = LogRecordSharedPool.Current;
List<Task> tasks = new();
List<Task> tasks = [];
for (int i = 0; i < Environment.ProcessorCount; i++)
{

View File

@ -4,6 +4,7 @@
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Reflection;
using Xunit.Sdk;
namespace OpenTelemetry.Tests;
@ -34,11 +35,11 @@ internal static class EventSourceTestHelper
actualEvent = listener.Messages.FirstOrDefault(x => x.EventId == 0);
if (actualEvent != null)
{
throw new Exception(actualEvent.Message);
throw new InvalidOperationException(actualEvent.Message);
}
// give up
throw new Exception("Listener failed to collect event.");
throw new InvalidOperationException("Listener failed to collect event.");
}
VerifyEventId(eventMethod, actualEvent);
@ -49,7 +50,7 @@ internal static class EventSourceTestHelper
{
var name = eventMethod.DeclaringType?.Name + "." + eventMethod.Name;
throw new Exception("Method '" + name + "' is implemented incorrectly.", e);
throw new InvalidOperationException("Method '" + name + "' is implemented incorrectly.", e);
}
finally
{
@ -116,7 +117,7 @@ internal static class EventSourceTestHelper
methodName,
expected,
actual);
throw new Exception(errorMessage);
throw EqualException.ForMismatchedValuesWithError(expected, actual, banner: errorMessage);
}
}
@ -128,6 +129,6 @@ internal static class EventSourceTestHelper
private static IEnumerable<MethodInfo> GetEventMethods(EventSource eventSource)
{
MethodInfo[] methods = eventSource.GetType().GetMethods();
return methods.Where(m => m.GetCustomAttributes(typeof(EventAttribute), false).Any());
return methods.Where(m => m.GetCustomAttributes(typeof(EventAttribute), false).Length > 0);
}
}

View File

@ -8,7 +8,7 @@ namespace OpenTelemetry.Tests;
/// <summary>
/// Event listener for testing event sources.
/// </summary>
internal class TestEventListener : EventListener
internal sealed class TestEventListener : EventListener
{
/// <summary>Unique Id used to identify events from the test thread.</summary>
private readonly Guid activityId;
@ -66,6 +66,12 @@ internal class TestEventListener : EventListener
this.events.Clear();
}
public override void Dispose()
{
this.eventWritten.Dispose();
base.Dispose();
}
/// <summary>Handler for event source writes.</summary>
/// <param name="eventData">The event data that was written.</param>
protected override void OnEventWritten(EventWrittenEventArgs eventData)

View File

@ -6,7 +6,7 @@ using System.Runtime.CompilerServices;
namespace OpenTelemetry.Tests;
internal class Utils
internal static class Utils
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static string GetCurrentMethodName()