diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs index 6c527187d91..64fb588a208 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs @@ -14,6 +14,7 @@ // limitations under the License. // +#if !NET8_0_OR_GREATER using System.Diagnostics.Metrics; using System.Reflection; using OpenTelemetry.Instrumentation.AspNetCore.Implementation; @@ -59,3 +60,4 @@ public void Dispose() this.meter?.Dispose(); } } +#endif diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs index e61558d5b8b..0710a9e4fe9 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetricsInstrumentationOptions.cs @@ -76,3 +76,4 @@ internal AspNetCoreMetricsInstrumentationOptions(IConfiguration configuration) /// public AspNetCoreMetricEnrichmentFunc Enrich { get; set; } } + diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md index e1439a9c231..10e074b856c 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md @@ -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 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs index 3f67896c167..f8c846cba9f 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/AspNetCoreInstrumentationEventSource.cs @@ -92,4 +92,10 @@ public void UnknownErrorProcessingEvent(string handlerName, string eventName, st { 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); + } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs index 9a8b35040c6..db7fa2a5272 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs @@ -14,6 +14,7 @@ // limitations under the License. // +#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 @@ public void OnEventWritten_New(string name, object payload) 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(SemanticConventions.AttributeNetworkProtocolName, NetworkProtocolName)); tags.Add(new KeyValuePair(SemanticConventions.AttributeNetworkProtocolVersion, HttpTagHelper.GetFlavorTagValueFromProtocol(context.Request.Protocol))); tags.Add(new KeyValuePair(SemanticConventions.AttributeUrlScheme, context.Request.Scheme)); tags.Add(new KeyValuePair(SemanticConventions.AttributeHttpRequestMethod, context.Request.Method)); @@ -214,3 +217,4 @@ public void OnEventWritten_New(string name, object payload) this.httpServerRequestDuration.Record(Activity.Current.Duration.TotalSeconds, tags); } } +#endif diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs index 99330ac220a..80c071a2304 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs @@ -14,8 +14,10 @@ // limitations under the License. // +#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 /// The instance of to chain the calls. 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 + } /// /// Enables the incoming requests automatic data collection for ASP.NET Core. @@ -61,6 +71,11 @@ public static MeterProviderBuilder AddAspNetCoreInstrumentation( { 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 MeterProviderBuilder AddAspNetCoreInstrumentation( }); 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"); } } diff --git a/src/Shared/SemanticConventions.cs b/src/Shared/SemanticConventions.cs index 3f0ce13a20b..d48f7d6bca1 100644 --- a/src/Shared/SemanticConventions.cs +++ b/src/Shared/SemanticConventions.cs @@ -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) diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs index 86e31d863ed..0d9acad0ca9 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs @@ -65,6 +65,7 @@ void ConfigureTestServices(IServiceCollection services) Assert.True(optionsPickedFromDI); } +#if !NET8_0_OR_GREATER [Theory] [InlineData(null)] [InlineData("CustomName")] @@ -95,4 +96,5 @@ void ConfigureTestServices(IServiceCollection services) Assert.True(optionsPickedFromDI); } +#endif } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs index 8b179278a49..1d15fa4d9d2 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs @@ -14,11 +14,22 @@ // limitations under the License. // +#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,11 +61,139 @@ public void AddAspNetCoreInstrumentation_BadArgs() Assert.Throws(() => builder.AddAspNetCoreInstrumentation()); } +#if NET8_0_OR_GREATER [Fact] - public async Task RequestMetricIsCaptured_Old() + public async Task ValidateNet8MetricsAsync() + { + var metricItems = new List(); + + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation() + .AddInMemoryExporter(metricItems) + .Build(); + + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + var app = builder.Build(); + + app.MapGet("/", () => "Hello"); + + _ = 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 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(); + + 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 requestLeaseDurationMetric = metricItems. + Where(item => item.Name == "aspnetcore.rate_limiting.request_lease.duration") + .ToArray(); + + 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() { var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = null }) + .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http" }) .Build(); var metricItems = new List(); @@ -87,25 +226,27 @@ public async Task RequestMetricIsCaptured_Old() this.meterProvider.Dispose(); var requestMetrics = metricItems - .Where(item => item.Name == "http.server.duration") + .Where(item => item.Name == "http.server.request.duration") .ToArray(); var metric = Assert.Single(requestMetrics); - Assert.Equal("ms", metric.Unit); + + Assert.Equal("s", metric.Unit); var metricPoints = GetMetricPoints(metric); Assert.Equal(2, metricPoints.Count); - AssertMetricPoints_Old( + AssertMetricPoints_New( metricPoints: metricPoints, expectedRoutes: new List { "api/Values", "api/Values/{id}" }, expectedTagsCount: 6); } +#if !NET8_0_OR_GREATER [Fact] - public async Task RequestMetricIsCaptured_New() + public async Task RequestMetricIsCaptured_Old() { var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = "http" }) + .AddInMemoryCollection(new Dictionary { [SemanticConventionOptInKeyName] = null }) .Build(); var metricItems = new List(); @@ -138,19 +279,18 @@ public async Task RequestMetricIsCaptured_New() this.meterProvider.Dispose(); var requestMetrics = metricItems - .Where(item => item.Name == "http.server.request.duration") + .Where(item => item.Name == "http.server.duration") .ToArray(); var metric = Assert.Single(requestMetrics); - - Assert.Equal("s", metric.Unit); + Assert.Equal("ms", metric.Unit); var metricPoints = GetMetricPoints(metric); Assert.Equal(2, metricPoints.Count); - AssertMetricPoints_New( + AssertMetricPoints_Old( metricPoints: metricPoints, expectedRoutes: new List { "api/Values", "api/Values/{id}" }, - expectedTagsCount: 5); + expectedTagsCount: 6); } [Fact] @@ -218,7 +358,7 @@ public async Task RequestMetricIsCaptured_Dup() AssertMetricPoints_New( metricPoints: metricPoints, expectedRoutes: new List { "api/Values", "api/Values/{id}" }, - expectedTagsCount: 5); + expectedTagsCount: 6); } [Fact] @@ -323,6 +463,7 @@ void ConfigureTestServices(IServiceCollection services) Assert.Contains(tagsToAdd[0], tags); Assert.Contains(tagsToAdd[1], tags); } +#endif public void Dispose() {