diff --git a/src/Hosting/Hosting/src/Internal/HostingApplication.cs b/src/Hosting/Hosting/src/Internal/HostingApplication.cs index 487cf97820c2..b610173be531 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplication.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplication.cs @@ -162,7 +162,7 @@ public void Reset() HasDiagnosticListener = false; MetricsEnabled = false; EventLogEnabled = false; - MetricsTagsFeature?.TagsList.Clear(); + MetricsTagsFeature?.Reset(); } } } diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index 9b473ac08547..713c1573021e 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -60,6 +60,10 @@ public void BeginRequest(HttpContext httpContext, HostingApplication.Context con context.MetricsTagsFeature ??= new HttpMetricsTagsFeature(); httpContext.Features.Set(context.MetricsTagsFeature); + context.MetricsTagsFeature.Method = httpContext.Request.Method; + context.MetricsTagsFeature.Protocol = httpContext.Request.Protocol; + context.MetricsTagsFeature.Scheme = httpContext.Request.Scheme; + startTimestamp = Stopwatch.GetTimestamp(); // To keep the hot path short we defer logging in this function to non-inlines @@ -152,19 +156,18 @@ public void RequestEnd(HttpContext httpContext, Exception? exception, HostingApp { var endpoint = HttpExtensions.GetOriginalEndpoint(httpContext); var route = endpoint?.Metadata.GetMetadata()?.Route; - var customTags = context.MetricsTagsFeature?.TagsList; + + Debug.Assert(context.MetricsTagsFeature != null, "MetricsTagsFeature should be set if MetricsEnabled is true."); _metrics.RequestEnd( - httpContext.Request.Protocol, - httpContext.Request.IsHttps, - httpContext.Request.Scheme, - httpContext.Request.Method, - httpContext.Request.Host, + context.MetricsTagsFeature.Protocol!, + context.MetricsTagsFeature.Scheme!, + context.MetricsTagsFeature.Method!, route, httpContext.Response.StatusCode, reachedPipelineEnd, exception, - customTags, + context.MetricsTagsFeature.TagsList, startTimestamp, currentTimestamp); } @@ -372,7 +375,7 @@ private void RecordRequestStartEventLog(HttpContext httpContext) [MethodImpl(MethodImplOptions.NoInlining)] private void RecordRequestStartMetrics(HttpContext httpContext) { - _metrics.RequestStart(httpContext.Request.IsHttps, httpContext.Request.Scheme, httpContext.Request.Method, httpContext.Request.Host); + _metrics.RequestStart(httpContext.Request.Scheme, httpContext.Request.Method); } [MethodImpl(MethodImplOptions.NoInlining)] diff --git a/src/Hosting/Hosting/src/Internal/HostingMetrics.cs b/src/Hosting/Hosting/src/Internal/HostingMetrics.cs index 3b313cddf89e..b72f0c6d5f03 100644 --- a/src/Hosting/Hosting/src/Internal/HostingMetrics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingMetrics.cs @@ -33,18 +33,18 @@ public HostingMetrics(IMeterFactory meterFactory) } // Note: Calling code checks whether counter is enabled. - public void RequestStart(bool isHttps, string scheme, string method, HostString host) + public void RequestStart(string scheme, string method) { // Tags must match request end. var tags = new TagList(); - InitializeRequestTags(ref tags, isHttps, scheme, method, host); + InitializeRequestTags(ref tags, scheme, method); _activeRequestsCounter.Add(1, tags); } - public void RequestEnd(string protocol, bool isHttps, string scheme, string method, HostString host, string? route, int statusCode, bool unhandledRequest, Exception? exception, List>? customTags, long startTimestamp, long currentTimestamp) + public void RequestEnd(string protocol, string scheme, string method, string? route, int statusCode, bool unhandledRequest, Exception? exception, List>? customTags, long startTimestamp, long currentTimestamp) { var tags = new TagList(); - InitializeRequestTags(ref tags, isHttps, scheme, method, host); + InitializeRequestTags(ref tags, scheme, method); // Tags must match request start. if (_activeRequestsCounter.Enabled) @@ -100,30 +100,10 @@ public void Dispose() public bool IsEnabled() => _activeRequestsCounter.Enabled || _requestDuration.Enabled; - private static void InitializeRequestTags(ref TagList tags, bool isHttps, string scheme, string method, HostString host) + private static void InitializeRequestTags(ref TagList tags, string scheme, string method) { tags.Add("url.scheme", scheme); tags.Add("http.request.method", ResolveHttpMethod(method)); - - _ = isHttps; - _ = host; - // TODO: Support configuration for enabling host header annotations - /* - if (host.HasValue) - { - tags.Add("server.address", host.Host); - - // Port is parsed each time it's accessed. Store part in local variable. - if (host.Port is { } port) - { - // Add port tag when not the default value for the current scheme - if ((isHttps && port != 443) || (!isHttps && port != 80)) - { - tags.Add("server.port", port); - } - } - } - */ } private static readonly object[] BoxedStatusCodes = new object[512]; diff --git a/src/Hosting/Hosting/src/Internal/HttpMetricsTagsFeature.cs b/src/Hosting/Hosting/src/Internal/HttpMetricsTagsFeature.cs index ea3a559185d6..e556210b9309 100644 --- a/src/Hosting/Hosting/src/Internal/HttpMetricsTagsFeature.cs +++ b/src/Hosting/Hosting/src/Internal/HttpMetricsTagsFeature.cs @@ -10,4 +10,19 @@ internal sealed class HttpMetricsTagsFeature : IHttpMetricsTagsFeature ICollection> IHttpMetricsTagsFeature.Tags => TagsList; public List> TagsList { get; } = new List>(); + + // Cache request values when request starts. These are used when writing metrics when the request ends. + // This ensures that the tags match between the start and end of the request. Important for up/down counters. + public string? Method { get; set; } + public string? Scheme { get; set; } + public string? Protocol { get; set; } + + public void Reset() + { + TagsList.Clear(); + + Method = null; + Scheme = null; + Protocol = null; + } } diff --git a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs index 78d0fd542589..83a2efbe45e7 100644 --- a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs @@ -174,6 +174,65 @@ public void MetricsEnabled() Assert.False(context.EventLogEnabled); } + [Fact] + public void Metrics_RequestChanges_OriginalValuesUsed() + { + // Arrange + var hostingEventSource = new HostingEventSource(Guid.NewGuid().ToString()); + + var testMeterFactory = new TestMeterFactory(); + using var activeRequestsCollector = new MetricCollector(testMeterFactory, HostingMetrics.MeterName, "http.server.active_requests"); + using var requestDurationCollector = new MetricCollector(testMeterFactory, HostingMetrics.MeterName, "http.server.request.duration"); + + // Act + var hostingApplication = CreateApplication(out var features, eventSource: hostingEventSource, meterFactory: testMeterFactory, configure: c => + { + c.Request.Protocol = "1.1"; + c.Request.Scheme = "http"; + c.Request.Method = "POST"; + c.Request.Host = new HostString("localhost"); + c.Request.Path = "/hello"; + c.Request.ContentType = "text/plain"; + c.Request.ContentLength = 1024; + }); + var context = hostingApplication.CreateContext(features); + + Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(), + m => + { + Assert.Equal(1, m.Value); + Assert.Equal("http", m.Tags["url.scheme"]); + Assert.Equal("POST", m.Tags["http.request.method"]); + }); + + context.HttpContext.Request.Protocol = "HTTP/2"; + context.HttpContext.Request.Method = "PUT"; + context.HttpContext.Request.Scheme = "https"; + context.HttpContext.Features.GetRequiredFeature().Tags.Add(new KeyValuePair("custom.tag", "custom.value")); + + hostingApplication.DisposeContext(context, null); + + // Assert + Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(), + m => + { + Assert.Equal(1, m.Value); + Assert.Equal("http", m.Tags["url.scheme"]); + Assert.Equal("POST", m.Tags["http.request.method"]); + }, + m => + { + Assert.Equal(-1, m.Value); + Assert.Equal("http", m.Tags["url.scheme"]); + Assert.Equal("POST", m.Tags["http.request.method"]); + }); + + Assert.Empty(context.MetricsTagsFeature.TagsList); + Assert.Null(context.MetricsTagsFeature.Scheme); + Assert.Null(context.MetricsTagsFeature.Method); + Assert.Null(context.MetricsTagsFeature.Protocol); + } + [Fact] public void DisposeContextDoesNotThrowWhenContextScopeIsNull() {