File: ComponentsMetrics.cs
Web Access
Project: src\src\Components\Components\src\Microsoft.AspNetCore.Components.csproj (Microsoft.AspNetCore.Components)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http;
 
namespace Microsoft.AspNetCore.Components;
 
internal sealed class ComponentsMetrics : IDisposable
{
    public const string MeterName = "Microsoft.AspNetCore.Components";
    public const string LifecycleMeterName = "Microsoft.AspNetCore.Components.Lifecycle";
    private readonly Meter _meter;
    private readonly Meter _lifeCycleMeter;
 
    private readonly Counter<long> _navigationCount;
 
    private readonly Histogram<double> _eventDuration;
    private readonly Histogram<double> _parametersDuration;
    private readonly Histogram<double> _batchDuration;
 
    public bool IsNavigationEnabled => _navigationCount.Enabled;
 
    public bool IsEventEnabled => _eventDuration.Enabled;
 
    public bool IsParametersEnabled => _parametersDuration.Enabled;
 
    public bool IsBatchEnabled => _batchDuration.Enabled;
 
    public ComponentsMetrics(IMeterFactory meterFactory)
    {
        Debug.Assert(meterFactory != null);
 
        _meter = meterFactory.Create(MeterName);
        _lifeCycleMeter = meterFactory.Create(LifecycleMeterName);
 
        _navigationCount = _meter.CreateCounter<long>(
            "aspnetcore.components.navigation",
            unit: "{route}",
            description: "Total number of route changes.");
 
        _eventDuration = _meter.CreateHistogram(
            "aspnetcore.components.event_handler",
            unit: "s",
            description: "Duration of processing browser event.  It includes business logic of the component but not affected child components.",
            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
 
        _parametersDuration = _lifeCycleMeter.CreateHistogram(
            "aspnetcore.components.update_parameters",
            unit: "s",
            description: "Duration of processing component parameters. It includes business logic of the component.",
            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries });
 
        _batchDuration = _lifeCycleMeter.CreateHistogram(
            "aspnetcore.components.render_diff",
            unit: "s",
            description: "Duration of rendering component tree and producing HTML diff. It includes business logic of the changed components.",
            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries });
    }
 
    public void Navigation(string componentType, string route)
    {
        var tags = new TagList
        {
            { "aspnetcore.components.type", componentType ?? "unknown" },
            { "aspnetcore.components.route", route ?? "unknown" },
        };
 
        _navigationCount.Add(1, tags);
    }
 
    public async Task CaptureEventDuration(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
    {
        var tags = new TagList
        {
            { "aspnetcore.components.type", componentType ?? "unknown" },
            { "aspnetcore.components.method", methodName ?? "unknown" },
            { "aspnetcore.components.attribute.name", attributeName ?? "unknown" }
        };
 
        try
        {
            await task;
        }
        catch (Exception ex)
        {
            tags.Add("error.type", ex.GetType().FullName ?? "unknown");
        }
        var duration = Stopwatch.GetElapsedTime(startTimestamp);
        _eventDuration.Record(duration.TotalSeconds, tags);
    }
 
    public void FailEventSync(Exception ex, long startTimestamp, string? componentType, string? methodName, string? attributeName)
    {
        var tags = new TagList
        {
            { "aspnetcore.components.type", componentType ?? "unknown" },
            { "aspnetcore.components.method", methodName ?? "unknown" },
            { "aspnetcore.components.attribute.name", attributeName ?? "unknown" },
            { "error.type", ex.GetType().FullName ?? "unknown" }
        };
        var duration = Stopwatch.GetElapsedTime(startTimestamp);
        _eventDuration.Record(duration.TotalSeconds, tags);
    }
 
    public async Task CaptureParametersDuration(Task task, long startTimestamp, string? componentType)
    {
        var tags = new TagList
        {
            { "aspnetcore.components.type", componentType ?? "unknown" },
        };
 
        try
        {
            await task;
        }
        catch(Exception ex)
        {
            tags.Add("error.type", ex.GetType().FullName ?? "unknown");
        }
        var duration = Stopwatch.GetElapsedTime(startTimestamp);
        _parametersDuration.Record(duration.TotalSeconds, tags);
    }
 
    public void FailParametersSync(Exception ex, long startTimestamp, string? componentType)
    {
        var duration = Stopwatch.GetElapsedTime(startTimestamp);
        var tags = new TagList
        {
            { "aspnetcore.components.type", componentType ?? "unknown" },
            { "error.type", ex.GetType().FullName ?? "unknown" }
        };
        _parametersDuration.Record(duration.TotalSeconds, tags);
    }
 
    public async Task CaptureBatchDuration(Task task, long startTimestamp, int diffLength)
    {
        var tags = new TagList
        {
            { "aspnetcore.components.diff.length", BucketDiffLength(diffLength) }
        };
 
        try
        {
            await task;
        }
        catch (Exception ex)
        {
            tags.Add("error.type", ex.GetType().FullName ?? "unknown");
        }
        var duration = Stopwatch.GetElapsedTime(startTimestamp);
        _batchDuration.Record(duration.TotalSeconds, tags);
    }
 
    public void FailBatchSync(Exception ex, long startTimestamp)
    {
        var duration = Stopwatch.GetElapsedTime(startTimestamp);
        var tags = new TagList
            {
                { "aspnetcore.components.diff.length", 0 },
                { "error.type", ex.GetType().FullName ?? "unknown" }
            };
        _batchDuration.Record(duration.TotalSeconds, tags);
    }
 
    private static int BucketDiffLength(int diffLength)
    {
        return diffLength switch
        {
            <= 1 => 1,
            <= 2 => 2,
            <= 5 => 5,
            <= 10 => 10,
            <= 20 => 20,
            <= 50 => 50,
            <= 100 => 100,
            <= 500 => 500,
            <= 1000 => 1000,
            <= 10000 => 10000,
            _ => 10001,
        };
    }
 
    public void Dispose()
    {
        _meter.Dispose();
        _lifeCycleMeter.Dispose();
    }
}