File: HostingMetricsTests.cs
Web Access
Project: src\src\Hosting\Hosting\test\Microsoft.AspNetCore.Hosting.Tests.csproj (Microsoft.AspNetCore.Hosting.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.Metrics.Testing;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Microsoft.AspNetCore.Hosting.Tests;
 
public class HostingMetricsTests
{
    [Fact]
    public void MultipleRequests()
    {
        // Arrange
        var meterFactory = new TestMeterFactory();
        var hostingApplication = CreateApplication(meterFactory: meterFactory);
        var httpContext = new DefaultHttpContext();
        var meter = meterFactory.Meters.Single();
 
        using var requestDurationCollector = new MetricCollector<double>(meterFactory, HostingMetrics.MeterName, "http.server.request.duration");
        using var activeRequestsCollector = new MetricCollector<long>(meterFactory, HostingMetrics.MeterName, "http.server.active_requests");
 
        // Act/Assert
        Assert.Equal(HostingMetrics.MeterName, meter.Name);
        Assert.Null(meter.Version);
 
        // Request 1 (after success)
        httpContext.Request.Protocol = HttpProtocol.Http11;
        var context1 = hostingApplication.CreateContext(httpContext.Features);
        context1.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
        hostingApplication.DisposeContext(context1, null);
 
        Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
            m => Assert.Equal(1, m.Value),
            m => Assert.Equal(-1, m.Value));
        Assert.Collection(requestDurationCollector.GetMeasurementSnapshot(),
            m => AssertRequestDuration(m, "1.1", StatusCodes.Status200OK));
 
        // Request 2 (after failure)
        httpContext.Request.Protocol = HttpProtocol.Http2;
        var context2 = hostingApplication.CreateContext(httpContext.Features);
        context2.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
        hostingApplication.DisposeContext(context2, new InvalidOperationException("Test error"));
 
        Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
            m => Assert.Equal(1, m.Value),
            m => Assert.Equal(-1, m.Value),
            m => Assert.Equal(1, m.Value),
            m => Assert.Equal(-1, m.Value));
        Assert.Collection(requestDurationCollector.GetMeasurementSnapshot(),
            m => AssertRequestDuration(m, "1.1", StatusCodes.Status200OK),
            m => AssertRequestDuration(m, "2", StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"));
 
        // Request 3
        httpContext.Request.Protocol = HttpProtocol.Http3;
        var context3 = hostingApplication.CreateContext(httpContext.Features);
        context3.HttpContext.Items["__RequestUnhandled"] = true;
        context3.HttpContext.Response.StatusCode = StatusCodes.Status404NotFound;
 
        Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
            m => Assert.Equal(1, m.Value),
            m => Assert.Equal(-1, m.Value),
            m => Assert.Equal(1, m.Value),
            m => Assert.Equal(-1, m.Value),
            m => Assert.Equal(1, m.Value));
        Assert.Collection(requestDurationCollector.GetMeasurementSnapshot(),
            m => AssertRequestDuration(m, "1.1", StatusCodes.Status200OK),
            m => AssertRequestDuration(m, "2", StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"));
 
        hostingApplication.DisposeContext(context3, null);
 
        Assert.Collection(activeRequestsCollector.GetMeasurementSnapshot(),
            m => Assert.Equal(1, m.Value),
            m => Assert.Equal(-1, m.Value),
            m => Assert.Equal(1, m.Value),
            m => Assert.Equal(-1, m.Value),
            m => Assert.Equal(1, m.Value),
            m => Assert.Equal(-1, m.Value));
        Assert.Collection(requestDurationCollector.GetMeasurementSnapshot(),
            m => AssertRequestDuration(m, "1.1", StatusCodes.Status200OK),
            m => AssertRequestDuration(m, "2", StatusCodes.Status500InternalServerError, exceptionName: "System.InvalidOperationException"),
            m => AssertRequestDuration(m, "3", StatusCodes.Status404NotFound, unhandledRequest: true));
 
        static void AssertRequestDuration(CollectedMeasurement<double> measurement, string httpVersion, int statusCode, string exceptionName = null, bool? unhandledRequest = null)
        {
            Assert.True(measurement.Value > 0);
            Assert.Equal(httpVersion, (string)measurement.Tags["network.protocol.version"]);
            Assert.Equal(statusCode, (int)measurement.Tags["http.response.status_code"]);
            if (exceptionName == null)
            {
                Assert.False(measurement.Tags.ContainsKey("error.type"));
            }
            else
            {
                Assert.Equal(exceptionName, (string)measurement.Tags["error.type"]);
            }
            if (unhandledRequest ?? false)
            {
                Assert.True((bool)measurement.Tags["aspnetcore.request.is_unhandled"]);
            }
            else
            {
                Assert.False(measurement.Tags.ContainsKey("aspnetcore.request.is_unhandled"));
            }
        }
    }
 
    [Fact]
    public async Task StartListeningDuringRequest_NotMeasured()
    {
        // Arrange
        var syncPoint = new SyncPoint();
        var meterFactory = new TestMeterFactory();
        var hostingApplication = CreateApplication(meterFactory: meterFactory, requestDelegate: async ctx =>
        {
            await syncPoint.WaitToContinue();
        });
        var httpContext = new DefaultHttpContext();
        var meter = meterFactory.Meters.Single();
 
        // Act/Assert
        Assert.Equal(HostingMetrics.MeterName, meter.Name);
        Assert.Null(meter.Version);
 
        // Request 1 (after success)
        httpContext.Request.Protocol = HttpProtocol.Http11;
        var context1 = hostingApplication.CreateContext(httpContext.Features);
        var processRequestTask = hostingApplication.ProcessRequestAsync(context1);
 
        await syncPoint.WaitForSyncPoint().DefaultTimeout();
 
        using var requestDurationCollector = new MetricCollector<double>(meterFactory, HostingMetrics.MeterName, "http.server.request.duration");
        using var currentRequestsCollector = new MetricCollector<long>(meterFactory, HostingMetrics.MeterName, "http.server.active_requests");
        context1.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
 
        syncPoint.Continue();
        await processRequestTask.DefaultTimeout();
 
        hostingApplication.DisposeContext(context1, null);
 
        Assert.Empty(currentRequestsCollector.GetMeasurementSnapshot());
        Assert.Empty(requestDurationCollector.GetMeasurementSnapshot());
    }
 
    [Fact]
    public void IHttpMetricsTagsFeatureNotUsedFromFeatureCollection()
    {
        // Arrange
        var meterFactory = new TestMeterFactory();
        var hostingApplication = CreateApplication(meterFactory: meterFactory);
        var httpContext = new DefaultHttpContext();
        var meter = meterFactory.Meters.Single();
 
        using var requestDurationCollector = new MetricCollector<double>(meterFactory, HostingMetrics.MeterName, "http.server.request.duration");
        using var currentRequestsCollector = new MetricCollector<long>(meterFactory, HostingMetrics.MeterName, "http.server.active_requests");
 
        // Act/Assert
        Assert.Equal(HostingMetrics.MeterName, meter.Name);
        Assert.Null(meter.Version);
 
        // This feature will be overidden by hosting. Hosting is the owner of the feature and is resposible for setting it.
        var overridenFeature = new TestHttpMetricsTagsFeature();
        httpContext.Features.Set<IHttpMetricsTagsFeature>(overridenFeature);
 
        var context = hostingApplication.CreateContext(httpContext.Features);
        var contextFeature = httpContext.Features.Get<IHttpMetricsTagsFeature>();
 
        Assert.NotNull(contextFeature);
        Assert.NotEqual(overridenFeature, contextFeature);
    }
 
    private sealed class TestHttpMetricsTagsFeature : IHttpMetricsTagsFeature
    {
        public ICollection<KeyValuePair<string, object>> Tags { get; } = new Collection<KeyValuePair<string, object>>();
        public bool MetricsDisabled { get; set; }
    }
 
    private static HostingApplication CreateApplication(IHttpContextFactory httpContextFactory = null, bool useHttpContextAccessor = false,
        ActivitySource activitySource = null, IMeterFactory meterFactory = null, RequestDelegate requestDelegate = null)
    {
        var services = new ServiceCollection();
        services.AddOptions();
        if (useHttpContextAccessor)
        {
            services.AddHttpContextAccessor();
        }
 
        httpContextFactory ??= new DefaultHttpContextFactory(services.BuildServiceProvider());
        requestDelegate ??= ctx => Task.CompletedTask;
 
        var hostingApplication = new HostingApplication(
            requestDelegate,
            NullLogger.Instance,
            new DiagnosticListener("Microsoft.AspNetCore"),
            activitySource ?? new ActivitySource("Microsoft.AspNetCore"),
            DistributedContextPropagator.CreateDefaultPropagator(),
            httpContextFactory,
            HostingEventSource.Log,
            new HostingMetrics(meterFactory ?? new TestMeterFactory()));
 
        return hostingApplication;
    }
}