[ASP.NET Core] Add support for .NET8.0 specific metrics (#4934)
This commit is contained in:
parent
50d3af0aff
commit
38e21a99bb
|
|
@ -14,6 +14,7 @@
|
|||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Reflection;
|
||||
using OpenTelemetry.Instrumentation.AspNetCore.Implementation;
|
||||
|
|
@ -59,3 +60,4 @@ internal sealed class AspNetCoreMetrics : IDisposable
|
|||
this.meter?.Dispose();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -76,3 +76,4 @@ public class AspNetCoreMetricsInstrumentationOptions
|
|||
/// </summary>
|
||||
public AspNetCoreMetricEnrichmentFunc Enrich { get; set; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,61 @@
|
|||
and
|
||||
[metrics](https://github.com/open-telemetry/semantic-conventions/blob/2bad9afad58fbd6b33cc683d1ad1f006e35e4a5d/docs/http/http-metrics.md).
|
||||
|
||||
* Following metrics will now be enabled by default when targeting `.NET8.0` or
|
||||
newer framework:
|
||||
|
||||
* **Meter** : `Microsoft.AspNetCore.Hosting`
|
||||
* `http.server.request.duration`
|
||||
* `http.server.active_requests`
|
||||
|
||||
* **Meter** : `Microsoft.AspNetCore.Server.Kestrel`
|
||||
* `kestrel.active_connections`
|
||||
* `kestrel.connection.duration`
|
||||
* `kestrel.rejected_connections`
|
||||
* `kestrel.queued_connections`
|
||||
* `kestrel.queued_requests`
|
||||
* `kestrel.upgraded_connections`
|
||||
* `kestrel.tls_handshake.duration`
|
||||
* `kestrel.active_tls_handshakes`
|
||||
|
||||
* **Meter** : `Microsoft.AspNetCore.Http.Connections`
|
||||
* `signalr.server.connection.duration`
|
||||
* `signalr.server.active_connections`
|
||||
|
||||
* **Meter** : `Microsoft.AspNetCore.Routing`
|
||||
* `aspnetcore.routing.match_attempts`
|
||||
|
||||
* **Meter** : `Microsoft.AspNetCore.Diagnostics`
|
||||
* `aspnetcore.diagnostics.exceptions`
|
||||
|
||||
* **Meter** : `Microsoft.AspNetCore.RateLimiting`
|
||||
* `aspnetcore.rate_limiting.active_request_leases`
|
||||
* `aspnetcore.rate_limiting.request_lease.duration`
|
||||
* `aspnetcore.rate_limiting.queued_requests`
|
||||
* `aspnetcore.rate_limiting.request.time_in_queue`
|
||||
* `aspnetcore.rate_limiting.requests`
|
||||
|
||||
For details about each individual metric check [ASP.NET Core
|
||||
docs
|
||||
page](https://learn.microsoft.com/dotnet/core/diagnostics/built-in-metrics-aspnetcore).
|
||||
|
||||
**NOTES**:
|
||||
* When targeting `.NET8.0` framework or newer, `http.server.request.duration` metric
|
||||
will only follow
|
||||
[v1.22.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.22.0/docs/http/http-metrics.md#metric-httpclientrequestduration)
|
||||
semantic conventions specification. Ability to switch behavior to older
|
||||
conventions using `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable is
|
||||
not available.
|
||||
* Users can opt-out of metrics that are not required using
|
||||
[views](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/docs/metrics/customizing-the-sdk#drop-an-instrument).
|
||||
|
||||
([#4934](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4934))
|
||||
|
||||
* Added `network.protocol.name` dimension to `http.server.request.duration`
|
||||
metric. This change only affects users setting `OTEL_SEMCONV_STABILITY_OPT_IN`
|
||||
to `http` or `http/dup`.
|
||||
([#4934](https://github.com/open-telemetry/opentelemetry-dotnet/pull/4934))
|
||||
|
||||
## 1.5.1-beta.1
|
||||
|
||||
Released 2023-Jul-20
|
||||
|
|
|
|||
|
|
@ -92,4 +92,10 @@ internal sealed class AspNetCoreInstrumentationEventSource : EventSource
|
|||
{
|
||||
this.WriteEvent(5, handlerName, eventName, ex);
|
||||
}
|
||||
|
||||
[Event(6, Message = "'{0}' is not supported for .NET8.0 and above targets", Level = EventLevel.Warning)]
|
||||
public void UnsupportedOption(string optionName)
|
||||
{
|
||||
this.WriteEvent(6, optionName);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -32,6 +33,7 @@ internal sealed class HttpInMetricsListener : ListenerHandler
|
|||
|
||||
private const string OnStopEvent = "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop";
|
||||
private const string EventName = "OnStopActivity";
|
||||
private const string NetworkProtocolName = "http";
|
||||
|
||||
private readonly Meter meter;
|
||||
private readonly AspNetCoreMetricsInstrumentationOptions options;
|
||||
|
|
@ -184,6 +186,7 @@ internal sealed class HttpInMetricsListener : ListenerHandler
|
|||
TagList tags = default;
|
||||
|
||||
// see the spec https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/docs/http/http-spans.md
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetworkProtocolName, NetworkProtocolName));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol)));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeUrlScheme, context.Request.Scheme));
|
||||
tags.Add(new KeyValuePair<string, object>(SemanticConventions.AttributeHttpRequestMethod, context.Request.Method));
|
||||
|
|
@ -214,3 +217,4 @@ internal sealed class HttpInMetricsListener : ListenerHandler
|
|||
this.httpServerRequestDuration.Record(Activity.Current.Duration.TotalSeconds, tags);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@
|
|||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
#endif
|
||||
using OpenTelemetry.Instrumentation.AspNetCore;
|
||||
using OpenTelemetry.Instrumentation.AspNetCore.Implementation;
|
||||
using OpenTelemetry.Internal;
|
||||
|
|
@ -34,7 +36,15 @@ public static class MeterProviderBuilderExtensions
|
|||
/// <returns>The instance of <see cref="MeterProviderBuilder"/> to chain the calls.</returns>
|
||||
public static MeterProviderBuilder AddAspNetCoreInstrumentation(
|
||||
this MeterProviderBuilder builder)
|
||||
=> AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions: null);
|
||||
{
|
||||
Guard.ThrowIfNull(builder);
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
return builder.ConfigureMeters();
|
||||
#else
|
||||
return AddAspNetCoreInstrumentation(builder, name: null, configureAspNetCoreInstrumentationOptions: null);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the incoming requests automatic data collection for ASP.NET Core.
|
||||
|
|
@ -61,6 +71,11 @@ public static class MeterProviderBuilderExtensions
|
|||
{
|
||||
Guard.ThrowIfNull(builder);
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
AspNetCoreInstrumentationEventSource.Log.UnsupportedOption(nameof(AspNetCoreMetricsInstrumentationOptions));
|
||||
return builder.ConfigureMeters();
|
||||
#else
|
||||
|
||||
// Note: Warm-up the status code mapping.
|
||||
_ = TelemetryHelper.BoxedStatusCodes;
|
||||
|
||||
|
|
@ -90,5 +105,17 @@ public static class MeterProviderBuilderExtensions
|
|||
});
|
||||
|
||||
return builder;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static MeterProviderBuilder ConfigureMeters(this MeterProviderBuilder builder)
|
||||
{
|
||||
return builder
|
||||
.AddMeter("Microsoft.AspNetCore.Hosting")
|
||||
.AddMeter("Microsoft.AspNetCore.Server.Kestrel")
|
||||
.AddMeter("Microsoft.AspNetCore.Http.Connections")
|
||||
.AddMeter("Microsoft.AspNetCore.Routing")
|
||||
.AddMeter("Microsoft.AspNetCore.Diagnostics")
|
||||
.AddMeter("Microsoft.AspNetCore.RateLimiting");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ internal static class SemanticConventions
|
|||
public const string AttributeHttpRequestMethod = "http.request.method"; // replaces: "http.method" (AttributeHttpMethod)
|
||||
public const string AttributeHttpResponseStatusCode = "http.response.status_code"; // replaces: "http.status_code" (AttributeHttpStatusCode)
|
||||
public const string AttributeNetworkProtocolVersion = "network.protocol.version"; // replaces: "http.flavor" (AttributeHttpFlavor)
|
||||
public const string AttributeNetworkProtocolName = "network.protocol.name";
|
||||
public const string AttributeServerAddress = "server.address"; // replaces: "net.host.name" (AttributeNetHostName) and "net.peer.name" (AttributeNetPeerName)
|
||||
public const string AttributeServerPort = "server.port"; // replaces: "net.host.port" (AttributeNetHostPort) and "net.peer.port" (AttributeNetPeerPort)
|
||||
public const string AttributeServerSocketAddress = "server.socket.address"; // replaces: "net.peer.ip" (AttributeNetPeerIp)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ public class DependencyInjectionConfigTests
|
|||
Assert.True(optionsPickedFromDI);
|
||||
}
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("CustomName")]
|
||||
|
|
@ -95,4 +96,5 @@ public class DependencyInjectionConfigTests
|
|||
|
||||
Assert.True(optionsPickedFromDI);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,11 +14,22 @@
|
|||
// limitations under the License.
|
||||
// </copyright>
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
using System.Diagnostics;
|
||||
#endif
|
||||
#if NET8_0_OR_GREATER
|
||||
using System.Threading.RateLimiting;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
#endif
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
#if NET8_0_OR_GREATER
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
#endif
|
||||
#if !NET8_0_OR_GREATER
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
#endif
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -50,56 +61,133 @@ public class MetricTests
|
|||
Assert.Throws<ArgumentNullException>(() => builder.AddAspNetCoreInstrumentation());
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
[Fact]
|
||||
public async Task RequestMetricIsCaptured_Old()
|
||||
public async Task ValidateNet8MetricsAsync()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = null })
|
||||
.Build();
|
||||
|
||||
var metricItems = new List<Metric>();
|
||||
|
||||
this.meterProvider = Sdk.CreateMeterProviderBuilder()
|
||||
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddInMemoryExporter(metricItems)
|
||||
.Build();
|
||||
|
||||
using (var client = this.factory
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
|
||||
})
|
||||
.CreateClient())
|
||||
{
|
||||
using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false);
|
||||
using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false);
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.Logging.ClearProviders();
|
||||
var app = builder.Build();
|
||||
|
||||
response1.EnsureSuccessStatusCode();
|
||||
response2.EnsureSuccessStatusCode();
|
||||
}
|
||||
app.MapGet("/", () => "Hello");
|
||||
|
||||
// We need to let End callback execute as it is executed AFTER response was returned.
|
||||
_ = app.RunAsync();
|
||||
|
||||
using var client = new HttpClient();
|
||||
var res = await client.GetStringAsync("http://localhost:5000/").ConfigureAwait(false);
|
||||
Assert.NotNull(res);
|
||||
|
||||
// We need to let metric callback execute as it is executed AFTER response was returned.
|
||||
// In unit tests environment there may be a lot of parallel unit tests executed, so
|
||||
// giving some breezing room for the End callback to complete
|
||||
// giving some breezing room for the callbacks to complete
|
||||
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||||
|
||||
this.meterProvider.Dispose();
|
||||
|
||||
var requestMetrics = metricItems
|
||||
.Where(item => item.Name == "http.server.duration")
|
||||
var requestDurationMetric = metricItems
|
||||
.Count(item => item.Name == "http.server.request.duration");
|
||||
|
||||
var activeRequestsMetric = metricItems.
|
||||
Count(item => item.Name == "http.server.active_requests");
|
||||
|
||||
var routeMatchingMetric = metricItems.
|
||||
Count(item => item.Name == "aspnetcore.routing.match_attempts");
|
||||
|
||||
var kestrelActiveConnectionsMetric = metricItems.
|
||||
Count(item => item.Name == "kestrel.active_connections");
|
||||
|
||||
var kestrelQueuedConnectionMetric = metricItems.
|
||||
Count(item => item.Name == "kestrel.queued_connections");
|
||||
|
||||
Assert.Equal(1, requestDurationMetric);
|
||||
Assert.Equal(1, activeRequestsMetric);
|
||||
Assert.Equal(1, routeMatchingMetric);
|
||||
Assert.Equal(1, kestrelActiveConnectionsMetric);
|
||||
Assert.Equal(1, kestrelQueuedConnectionMetric);
|
||||
|
||||
// TODO
|
||||
// kestrel.queued_requests
|
||||
// kestrel.upgraded_connections
|
||||
// kestrel.rejected_connections
|
||||
// kestrel.tls_handshake.duration
|
||||
// kestrel.active_tls_handshakes
|
||||
|
||||
await app.DisposeAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateNet8RateLimitingMetricsAsync()
|
||||
{
|
||||
var metricItems = new List<Metric>();
|
||||
|
||||
this.meterProvider = Sdk.CreateMeterProviderBuilder()
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddInMemoryExporter(metricItems)
|
||||
.Build();
|
||||
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.Services.AddRateLimiter(_ => _
|
||||
.AddFixedWindowLimiter(policyName: "fixed", options =>
|
||||
{
|
||||
options.PermitLimit = 4;
|
||||
options.Window = TimeSpan.FromSeconds(12);
|
||||
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
||||
options.QueueLimit = 2;
|
||||
}));
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseRateLimiter();
|
||||
|
||||
static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
|
||||
|
||||
app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))
|
||||
.RequireRateLimiting("fixed");
|
||||
|
||||
_ = app.RunAsync();
|
||||
|
||||
using var client = new HttpClient();
|
||||
var res = await client.GetStringAsync("http://localhost:5000/").ConfigureAwait(false);
|
||||
Assert.NotNull(res);
|
||||
|
||||
// We need to let metric callback execute as it is executed AFTER response was returned.
|
||||
// In unit tests environment there may be a lot of parallel unit tests executed, so
|
||||
// giving some breezing room for the callbacks to complete
|
||||
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||||
|
||||
this.meterProvider.Dispose();
|
||||
|
||||
var activeRequestleasesMetric = metricItems
|
||||
.Where(item => item.Name == "aspnetcore.rate_limiting.active_request_leases")
|
||||
.ToArray();
|
||||
|
||||
var metric = Assert.Single(requestMetrics);
|
||||
Assert.Equal("ms", metric.Unit);
|
||||
var metricPoints = GetMetricPoints(metric);
|
||||
Assert.Equal(2, metricPoints.Count);
|
||||
var requestLeaseDurationMetric = metricItems.
|
||||
Where(item => item.Name == "aspnetcore.rate_limiting.request_lease.duration")
|
||||
.ToArray();
|
||||
|
||||
AssertMetricPoints_Old(
|
||||
metricPoints: metricPoints,
|
||||
expectedRoutes: new List<string> { "api/Values", "api/Values/{id}" },
|
||||
expectedTagsCount: 6);
|
||||
var limitingRequestsMetric = metricItems.
|
||||
Where(item => item.Name == "aspnetcore.rate_limiting.requests")
|
||||
.ToArray();
|
||||
|
||||
Assert.Single(activeRequestleasesMetric);
|
||||
Assert.Single(requestLeaseDurationMetric);
|
||||
Assert.Single(limitingRequestsMetric);
|
||||
|
||||
// TODO
|
||||
// aspnetcore.rate_limiting.request.time_in_queue
|
||||
// aspnetcore.rate_limiting.queued_requests
|
||||
|
||||
await app.DisposeAsync();
|
||||
}
|
||||
#endif
|
||||
|
||||
[Fact]
|
||||
public async Task RequestMetricIsCaptured_New()
|
||||
|
|
@ -150,7 +238,59 @@ public class MetricTests
|
|||
AssertMetricPoints_New(
|
||||
metricPoints: metricPoints,
|
||||
expectedRoutes: new List<string> { "api/Values", "api/Values/{id}" },
|
||||
expectedTagsCount: 5);
|
||||
expectedTagsCount: 6);
|
||||
}
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
[Fact]
|
||||
public async Task RequestMetricIsCaptured_Old()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = null })
|
||||
.Build();
|
||||
|
||||
var metricItems = new List<Metric>();
|
||||
|
||||
this.meterProvider = Sdk.CreateMeterProviderBuilder()
|
||||
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddInMemoryExporter(metricItems)
|
||||
.Build();
|
||||
|
||||
using (var client = this.factory
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
|
||||
})
|
||||
.CreateClient())
|
||||
{
|
||||
using var response1 = await client.GetAsync("/api/values").ConfigureAwait(false);
|
||||
using var response2 = await client.GetAsync("/api/values/2").ConfigureAwait(false);
|
||||
|
||||
response1.EnsureSuccessStatusCode();
|
||||
response2.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// We need to let End callback execute as it is executed AFTER response was returned.
|
||||
// In unit tests environment there may be a lot of parallel unit tests executed, so
|
||||
// giving some breezing room for the End callback to complete
|
||||
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
|
||||
|
||||
this.meterProvider.Dispose();
|
||||
|
||||
var requestMetrics = metricItems
|
||||
.Where(item => item.Name == "http.server.duration")
|
||||
.ToArray();
|
||||
|
||||
var metric = Assert.Single(requestMetrics);
|
||||
Assert.Equal("ms", metric.Unit);
|
||||
var metricPoints = GetMetricPoints(metric);
|
||||
Assert.Equal(2, metricPoints.Count);
|
||||
|
||||
AssertMetricPoints_Old(
|
||||
metricPoints: metricPoints,
|
||||
expectedRoutes: new List<string> { "api/Values", "api/Values/{id}" },
|
||||
expectedTagsCount: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -218,7 +358,7 @@ public class MetricTests
|
|||
AssertMetricPoints_New(
|
||||
metricPoints: metricPoints,
|
||||
expectedRoutes: new List<string> { "api/Values", "api/Values/{id}" },
|
||||
expectedTagsCount: 5);
|
||||
expectedTagsCount: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -323,6 +463,7 @@ public class MetricTests
|
|||
Assert.Contains(tagsToAdd[0], tags);
|
||||
Assert.Contains(tagsToAdd[1], tags);
|
||||
}
|
||||
#endif
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue