Repository: dotnet/aspnetcore
Pull request #61609: Blazor - rendering metrics and tracing
Author: login: pavelsavara
Labels: name: area-blazor
description: Includes: Blazor, Razor Components
Created: 2025-04-22T10:32:13Z | Merged: 2025-05-19T10:05:57Z

### Blazor metrics
- new meter `Microsoft.AspNetCore.Components`
`aspnetcore.components.navigation` - Total number of route changes.
`aspnetcore.components.event_handler` - Duration of processing browser event including business logic.

- new meter `Microsoft.AspNetCore.Components.Lifecycle`
`aspnetcore.components.update_parameters` - Duration of processing component parameters including business logic.
`aspnetcore.components.render_batch` - Duration of rendering batch including browser round-trip.

- meter `Microsoft.AspNetCore.Components.Server.Circuits`
`aspnetcore.components.circuit.active` - Number of active circuits in memory.
`aspnetcore.components.circuit.connected` - Number of circuits connected to client.
`aspnetcore.components.circuit.duration` - Duration of circuit lifetime and their total count.

### Blazor activity tracing
- new activity source `Microsoft.AspNetCore.Components`
- `Microsoft.AspNetCore.Components.CircuitStart`: `Circuit {circuitId}`
    - tags: `aspnetcore.components.circuit.id`
    - links: HTTP activity
- `Microsoft.AspNetCore.Components.RouteChange`: `Route {route} -> {componentType}`
    - tags: `aspnetcore.components.circuit.id`, `aspnetcore.components.type`, `aspnetcore.components.route`
    - links: HTTP trace, circuit trace
- `Microsoft.AspNetCore.Components.HandleEvent`: `Event {attributeName} -> {componentType}.{methodName}`
    - tags: `aspnetcore.components.circuit.id`, `aspnetcore.components.type`, `aspnetcore.components.method`, `aspnetcore.components.attribute.name`, `error.type`
    - links: HTTP trace, circuit trace, router trace

![image](https://github.com/user-attachments/assets/e301a6d2-6adb-451b-a720-43b0a0681451)

![image](https://github.com/user-attachments/assets/392875ba-bb3e-4860-858e-5d9dbc1a135c)

![image](https://github.com/user-attachments/assets/d2ea09e0-6891-474e-a0b8-008ac661422f)


```cs
builder.Services.ConfigureOpenTelemetryMeterProvider(meterProvider =>
{
    meterProvider.AddMeter("Microsoft.AspNetCore.Components");
    meterProvider.AddMeter("Microsoft.AspNetCore.Components.Server.Circuits");
});
builder.Services.ConfigureOpenTelemetryTracerProvider(tracerProvider =>
{
    tracerProvider.AddSource("Microsoft.AspNetCore.Components");
    //tracerProvider.AddSource("Microsoft.AspNetCore.SignalR.Server");
});
```

### Feedback

- always expect the `IMeterFactory` to be available in DI
- make buckets different size [per feedback](https://github.com/dotnet/aspnetcore/pull/61516#discussion_r2052263412)
- optimize work with tags
- make metrics in seconds [per feedback](https://github.com/dotnet/aspnetcore/pull/61516#discussion_r2052340016)
- unit test [delay feedback](https://github.com/dotnet/aspnetcore/pull/61516#discussion_r2052266880)

TODO - Metrics need to be documented at https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/built-in

Out of scope
- WASM


Contributes to https://github.com/dotnet/aspnetcore/issues/53613
Contributes to https://github.com/dotnet/aspnetcore/issues/29846
Feedback for https://github.com/dotnet/aspnetcore/pull/61516
Related https://github.com/open-telemetry/semantic-conventions/pull/2235


Diff:
diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs
new file mode 100644
index 000000000000..7079bf7743a4
--- /dev/null
+++ b/src/Components/Components/src/ComponentsActivitySource.cs
@@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"Circuit {circuitId ?? ""}";
+            activity.Start();
+            _circuitContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public void FailCircuitActivity(Activity? activity, Exception ex)
+    {
+        _circuitContext = default;
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public Activity? StartRouteActivity(string componentType, string route)
+    {
+        if (_httpContext == default)
+        {
+            _httpContext = CaptureHttpContext();
+        }
+
+        var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("aspnetcore.components.type", componentType);
+                }
+                if (route != null)
+                {
+                    activity.SetTag("aspnetcore.components.route", route);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+            }
+
+            activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}";
+            activity.Start();
+            _routeContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName)
+    {
+        var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("aspnetcore.components.type", componentType);
+                }
+                if (methodName != null)
+                {
+                    activity.SetTag("aspnetcore.components.method", methodName);
+                }
+                if (attributeName != null)
+                {
+                    activity.SetTag("aspnetcore.components.attribute.name", attributeName);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+                if (_routeContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_routeContext));
+                }
+            }
+
+            activity.DisplayName = $"Event {attributeName ?? "[unknown attribute]"} -> {componentType ?? "[unknown component]"}.{methodName ?? "[unknown method]"}";
+            activity.Start();
+        }
+        return activity;
+    }
+
+    public static void FailEventActivity(Activity? activity, Exception ex)
+    {
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public static async Task CaptureEventStopAsync(Task task, Activity? activity)
+    {
+        try
+        {
+            await task;
+            activity?.Stop();
+        }
+        catch (Exception ex)
+        {
+            FailEventActivity(activity, ex);
+        }
+    }
+}
diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs
new file mode 100644
index 000000000000..712d9e395b4c
--- /dev/null
+++ b/src/Components/Components/src/ComponentsMetrics.cs
@@ -0,0 +1,191 @@
+// 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();
+    }
+}
diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
index ca3286f8b6c2..07fcc360fd7e 100644
--- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
+++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
@@ -79,6 +79,7 @@
 
   <ItemGroup>
     <InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Web" />
+    <InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Server" />
     <InternalsVisibleTo Include="Microsoft.AspNetCore.Blazor.Build.Tests" />
     <InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Authorization.Tests" />
     <InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Forms.Tests" />
diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt
index b99e9fd7f216..ce563e15a7cb 100644
--- a/src/Components/Components/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Components/src/PublicAPI.Unshipped.txt
@@ -1,7 +1,7 @@
 #nullable enable
-
 Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.get -> System.Type!
 Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.set -> void
+Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions
 Microsoft.AspNetCore.Components.NavigationManager.OnNotFound -> System.EventHandler<Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs!>!
 Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void
 Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, System.Func<string!, System.Threading.Tasks.Task!>! onNavigateTo) -> void
@@ -14,5 +14,7 @@ Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttri
 Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttribute.SupplyParameterFromPersistentComponentStateAttribute() -> void
 Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions
 static Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions.AddPersistentServiceRegistration<TService>(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.AspNetCore.Components.IComponentRenderMode! componentRenderMode) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
+static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
+static Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
 static Microsoft.Extensions.DependencyInjection.SupplyParameterFromPersistentComponentStateProviderServiceCollectionExtensions.AddSupplyValueFromPersistentComponentStateProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
 virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.SignalRendererToFinishRendering() -> void
\ No newline at end of file
diff --git a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs
new file mode 100644
index 000000000000..e1e224e5f71b
--- /dev/null
+++ b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs
@@ -0,0 +1,57 @@
+// 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.Metrics;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.AspNetCore.Components.Infrastructure;
+
+/// <summary>
+/// Infrastructure APIs for registering diagnostic metrics.
+/// </summary>
+public static class ComponentsMetricsServiceCollectionExtensions
+{
+    /// <summary>
+    /// Registers component rendering metrics
+    /// </summary>
+    /// <param name="services">The <see cref="IServiceCollection"/>.</param>
+    /// <returns>The <see cref="IServiceCollection"/>.</returns>
+    public static IServiceCollection AddComponentsMetrics(
+        IServiceCollection services)
+    {
+        // do not register IConfigureOptions<StartupValidatorOptions> multiple times
+        if (!IsMeterFactoryRegistered(services))
+        {
+            services.AddMetrics();
+        }
+        services.TryAddSingleton<ComponentsMetrics>();
+
+        return services;
+    }
+
+    /// <summary>
+    /// Registers component rendering traces
+    /// </summary>
+    /// <param name="services">The <see cref="IServiceCollection"/>.</param>
+    /// <returns>The <see cref="IServiceCollection"/>.</returns>
+    public static IServiceCollection AddComponentsTracing(
+        IServiceCollection services)
+    {
+        services.TryAddScoped<ComponentsActivitySource>();
+
+        return services;
+    }
+
+    private static bool IsMeterFactoryRegistered(IServiceCollection services)
+    {
+        foreach (var service in services)
+        {
+            if (service.ServiceType == typeof(IMeterFactory))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/Components/Components/src/RenderHandle.cs b/src/Components/Components/src/RenderHandle.cs
index 6ac2b7b3cdec..edcb644bddfb 100644
--- a/src/Components/Components/src/RenderHandle.cs
+++ b/src/Components/Components/src/RenderHandle.cs
@@ -21,6 +21,9 @@ internal RenderHandle(Renderer renderer, int componentId)
         _componentId = componentId;
     }
 
+    internal ComponentsMetrics? ComponentMetrics => _renderer?.ComponentMetrics;
+    internal ComponentsActivitySource? ComponentActivitySource => _renderer?.ComponentActivitySource;
+
     /// <summary>
     /// Gets the <see cref="Components.Dispatcher" /> associated with the component.
     /// </summary>
diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs
index 5ba977930a46..177eccf9bf50 100644
--- a/src/Components/Components/src/RenderTree/Renderer.cs
+++ b/src/Components/Components/src/RenderTree/Renderer.cs
@@ -5,7 +5,6 @@
 
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
-using System.Diagnostics.Metrics;
 using System.Linq;
 using Microsoft.AspNetCore.Components.HotReload;
 using Microsoft.AspNetCore.Components.Reflection;
@@ -25,16 +24,20 @@ namespace Microsoft.AspNetCore.Components.RenderTree;
 // dispatching events to them, and notifying when the user interface is being updated.
 public abstract partial class Renderer : IDisposable, IAsyncDisposable
 {
+    internal static readonly Task CanceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true));
+
     private readonly object _lockObject = new();
     private readonly IServiceProvider _serviceProvider;
     private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
     private readonly Dictionary<IComponent, ComponentState> _componentStateByComponent = new Dictionary<IComponent, ComponentState>();
     private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
-    private readonly Dictionary<ulong, (int RenderedByComponentId, EventCallback Callback)> _eventBindings = new();
+    private readonly Dictionary<ulong, (int RenderedByComponentId, EventCallback Callback, string? attributeName)> _eventBindings = new();
     private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
     private readonly ILogger _logger;
     private readonly ComponentFactory _componentFactory;
-    private readonly RenderingMetrics? _renderingMetrics;
+    private readonly ComponentsMetrics? _componentsMetrics;
+    private readonly ComponentsActivitySource? _componentsActivitySource;
+
     private Dictionary<int, ParameterView>? _rootComponentsLatestParameters;
     private Task? _ongoingQuiescenceTask;
 
@@ -92,16 +95,17 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
         // logger name in here as a string literal.
         _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer");
         _componentFactory = new ComponentFactory(componentActivator, this);
-
-        // TODO register RenderingMetrics as singleton in DI
-        var meterFactory = serviceProvider.GetService<IMeterFactory>();
-        _renderingMetrics = meterFactory != null ? new RenderingMetrics(meterFactory) : null;
+        _componentsMetrics = serviceProvider.GetService<ComponentsMetrics>();
+        _componentsActivitySource = serviceProvider.GetService<ComponentsActivitySource>();
 
         ServiceProviderCascadingValueSuppliers = serviceProvider.GetService<ICascadingValueSupplier>() is null
             ? Array.Empty<ICascadingValueSupplier>()
             : serviceProvider.GetServices<ICascadingValueSupplier>().ToArray();
     }
 
+    internal ComponentsMetrics? ComponentMetrics => _componentsMetrics;
+    internal ComponentsActivitySource? ComponentActivitySource => _componentsActivitySource;
+
     internal ICascadingValueSupplier[] ServiceProviderCascadingValueSuppliers { get; }
 
     internal HotReloadManager HotReloadManager { get; set; } = HotReloadManager.Default;
@@ -442,7 +446,20 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
             _pendingTasks ??= new();
         }
 
-        var (renderedByComponentId, callback) = GetRequiredEventBindingEntry(eventHandlerId);
+        var (renderedByComponentId, callback, attributeName) = GetRequiredEventBindingEntry(eventHandlerId);
+
+        // collect trace
+        Activity? activity = null;
+        string receiverName = null;
+        string methodName = null;
+        if (ComponentActivitySource != null)
+        {
+            receiverName ??= (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName;
+            methodName ??= callback.Delegate.Method?.Name;
+            activity = ComponentActivitySource.StartEventActivity(receiverName, methodName, attributeName);
+        }
+
+        var eventStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsEventEnabled ? Stopwatch.GetTimestamp() : 0;
 
         // If this event attribute was rendered by a component that's since been disposed, don't dispatch the event at all.
         // This can occur because event handler disposal is deferred, so event handler IDs can outlive their components.
@@ -484,9 +501,35 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
             _isBatchInProgress = true;
 
             task = callback.InvokeAsync(eventArgs);
+
+            // collect metrics
+            if (ComponentMetrics != null && ComponentMetrics.IsEventEnabled)
+            {
+                receiverName ??= (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName;
+                methodName ??= callback.Delegate.Method?.Name;
+                _ = ComponentMetrics.CaptureEventDuration(task, eventStartTimestamp, receiverName, methodName, attributeName);
+            }
+
+            // stop activity/trace
+            if (ComponentActivitySource != null && activity != null)
+            {
+                _ = ComponentsActivitySource.CaptureEventStopAsync(task, activity);
+            }
         }
         catch (Exception e)
         {
+            if (ComponentMetrics != null && ComponentMetrics.IsEventEnabled)
+            {
+                receiverName ??= (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName;
+                methodName ??= callback.Delegate.Method?.Name;
+                ComponentMetrics.FailEventSync(e, eventStartTimestamp, receiverName, methodName, attributeName);
+            }
+
+            if (ComponentActivitySource != null && activity != null)
+            {
+                ComponentsActivitySource.FailEventActivity(activity, e);
+            }
+
             HandleExceptionViaErrorBoundary(e, receiverComponentState);
             return Task.CompletedTask;
         }
@@ -638,7 +681,7 @@ internal void AssignEventHandlerId(int renderedByComponentId, ref RenderTreeFram
             //
             // When that happens we intentionally box the EventCallback because we need to hold on to
             // the receiver.
-            _eventBindings.Add(id, (renderedByComponentId, callback));
+            _eventBindings.Add(id, (renderedByComponentId, callback, frame.AttributeName));
         }
         else if (frame.AttributeValueField is MulticastDelegate @delegate)
         {
@@ -646,7 +689,7 @@ internal void AssignEventHandlerId(int renderedByComponentId, ref RenderTreeFram
             // is the same as delegate.Target. In this case since the receiver is implicit we can
             // avoid boxing the EventCallback object and just re-hydrate it on the other side of the
             // render tree.
-            _eventBindings.Add(id, (renderedByComponentId, new EventCallback(@delegate.Target as IHandleEvent, @delegate)));
+            _eventBindings.Add(id, (renderedByComponentId, new EventCallback(@delegate.Target as IHandleEvent, @delegate), frame.AttributeName));
         }
 
         // NOTE: we do not to handle EventCallback<T> here. EventCallback<T> is only used when passing
@@ -696,7 +739,7 @@ internal void TrackReplacedEventHandlerId(ulong oldEventHandlerId, ulong newEven
         _eventHandlerIdReplacements.Add(oldEventHandlerId, newEventHandlerId);
     }
 
-    private (int RenderedByComponentId, EventCallback Callback) GetRequiredEventBindingEntry(ulong eventHandlerId)
+    private (int RenderedByComponentId, EventCallback Callback, string? attributeName) GetRequiredEventBindingEntry(ulong eventHandlerId)
     {
         if (!_eventBindings.TryGetValue(eventHandlerId, out var entry))
         {
@@ -770,6 +813,7 @@ private void ProcessRenderQueue()
 
         _isBatchInProgress = true;
         var updateDisplayTask = Task.CompletedTask;
+        var batchStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsBatchEnabled ? Stopwatch.GetTimestamp() : 0;
 
         try
         {
@@ -801,9 +845,19 @@ private void ProcessRenderQueue()
             // Fire off the execution of OnAfterRenderAsync, but don't wait for it
             // if there is async work to be done.
             _ = InvokeRenderCompletedCalls(batch.UpdatedComponents, updateDisplayTask);
+
+            if (ComponentMetrics != null && ComponentMetrics.IsBatchEnabled)
+            {
+                _ = ComponentMetrics.CaptureBatchDuration(updateDisplayTask, batchStartTimestamp, batch.UpdatedComponents.Count);
+            }
         }
         catch (Exception e)
         {
+            if (ComponentMetrics != null && ComponentMetrics.IsBatchEnabled)
+            {
+                ComponentMetrics.FailBatchSync(e, batchStartTimestamp);
+            }
+
             // Ensure we catch errors while running the render functions of the components.
             HandleException(e);
             return;
@@ -947,15 +1001,13 @@ private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
     {
         var componentState = renderQueueEntry.ComponentState;
         Log.RenderingComponent(_logger, componentState);
-        var startTime = (_renderingMetrics != null && _renderingMetrics.IsDurationEnabled()) ? Stopwatch.GetTimestamp() : 0;
-        _renderingMetrics?.RenderStart(componentState.Component.GetType().FullName);
+
         componentState.RenderIntoBatch(_batchBuilder, renderQueueEntry.RenderFragment, out var renderFragmentException);
         if (renderFragmentException != null)
         {
             // If this returns, the error was handled by an error boundary. Otherwise it throws.
             HandleExceptionViaErrorBoundary(renderFragmentException, componentState);
         }
-        _renderingMetrics?.RenderEnd(componentState.Component.GetType().FullName, renderFragmentException, startTime, Stopwatch.GetTimestamp());
 
         // Process disposal queue now in case it causes further component renders to be enqueued
         ProcessDisposalQueueInExistingBatch();
diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs
index c7be2643edd9..c1ee3c2f1646 100644
--- a/src/Components/Components/src/Rendering/ComponentState.cs
+++ b/src/Components/Components/src/Rendering/ComponentState.cs
@@ -23,6 +23,7 @@ public class ComponentState : IAsyncDisposable
     private RenderTreeBuilder _nextRenderTree;
     private ArrayBuilder<RenderTreeFrame>? _latestDirectParametersSnapshot; // Lazily instantiated
     private bool _componentWasDisposed;
+    private readonly string? _componentTypeName;
 
     /// <summary>
     /// Constructs an instance of <see cref="ComponentState"/>.
@@ -51,6 +52,11 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
             _hasCascadingParameters = true;
             _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions();
         }
+
+        if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersEnabled)
+        {
+            _componentTypeName = component.GetType().FullName;
+        }
     }
 
     private static ComponentState? GetSectionOutletLogicalParent(Renderer renderer, SectionOutlet sectionOutlet)
@@ -231,14 +237,27 @@ internal void NotifyCascadingValueChanged(in ParameterViewLifetime lifetime)
     // a consistent set to the recipient.
     private void SupplyCombinedParameters(ParameterView directAndCascadingParameters)
     {
-        // Normalise sync and async exceptions into a Task
+        var parametersStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersEnabled ? Stopwatch.GetTimestamp() : 0;
+
+        // Normalize sync and async exceptions into a Task
         Task setParametersAsyncTask;
         try
         {
             setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters);
+
+            // collect metrics
+            if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersEnabled)
+            {
+                _ = _renderer.ComponentMetrics.CaptureParametersDuration(setParametersAsyncTask, parametersStartTimestamp, _componentTypeName);
+            }
         }
         catch (Exception ex)
         {
+            if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersEnabled)
+            {
+                _renderer.ComponentMetrics.FailParametersSync(ex, parametersStartTimestamp, _componentTypeName);
+            }
+
             setParametersAsyncTask = Task.FromException(ex);
         }
 
diff --git a/src/Components/Components/src/Rendering/RenderingMetrics.cs b/src/Components/Components/src/Rendering/RenderingMetrics.cs
deleted file mode 100644
index 54b32a793cc7..000000000000
--- a/src/Components/Components/src/Rendering/RenderingMetrics.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-// 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.Http;
-
-namespace Microsoft.AspNetCore.Components.Rendering;
-
-internal sealed class RenderingMetrics : IDisposable
-{
-    public const string MeterName = "Microsoft.AspNetCore.Components.Rendering";
-
-    private readonly Meter _meter;
-    private readonly Counter<long> _renderTotalCounter;
-    private readonly UpDownCounter<long> _renderActiveCounter;
-    private readonly Histogram<double> _renderDuration;
-
-    public RenderingMetrics(IMeterFactory meterFactory)
-    {
-        Debug.Assert(meterFactory != null);
-
-        _meter = meterFactory.Create(MeterName);
-
-        _renderTotalCounter = _meter.CreateCounter<long>(
-            "aspnetcore.components.rendering.count",
-            unit: "{renders}",
-            description: "Number of component renders performed.");
-
-        _renderActiveCounter = _meter.CreateUpDownCounter<long>(
-            "aspnetcore.components.rendering.active_renders",
-            unit: "{renders}",
-            description: "Number of component renders performed.");
-
-        _renderDuration = _meter.CreateHistogram<double>(
-            "aspnetcore.components.rendering.duration",
-            unit: "ms",
-            description: "Duration of component rendering operations per component.",
-            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
-    }
-
-    public void RenderStart(string componentType)
-    {
-        var tags = new TagList();
-        tags = InitializeRequestTags(componentType, tags);
-
-        if (_renderActiveCounter.Enabled)
-        {
-            _renderActiveCounter.Add(1, tags);
-        }
-        if (_renderTotalCounter.Enabled)
-        {
-            _renderTotalCounter.Add(1, tags);
-        }
-    }
-
-    public void RenderEnd(string componentType, Exception? exception, long startTimestamp, long currentTimestamp)
-    {
-        // Tags must match request start.
-        var tags = new TagList();
-        tags = InitializeRequestTags(componentType, tags);
-
-        if (_renderActiveCounter.Enabled)
-        {
-            _renderActiveCounter.Add(-1, tags);
-        }
-
-        if (_renderDuration.Enabled)
-        {
-            if (exception != null)
-            {
-                TryAddTag(ref tags, "error.type", exception.GetType().FullName);
-            }
-
-            var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
-            _renderDuration.Record(duration.TotalMilliseconds, tags);
-        }
-    }
-
-    private static TagList InitializeRequestTags(string componentType, TagList tags)
-    {
-        tags.Add("component.type", componentType);
-        return tags;
-    }
-
-    public bool IsDurationEnabled() => _renderDuration.Enabled;
-
-    public void Dispose()
-    {
-        _meter.Dispose();
-    }
-
-    private static bool TryAddTag(ref TagList tags, string name, object? value)
-    {
-        for (var i = 0; i < tags.Count; i++)
-        {
-            if (tags[i].Key == name)
-            {
-                return false;
-            }
-        }
-
-        tags.Add(new KeyValuePair<string, object?>(name, value));
-        return true;
-    }
-}
diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs
index d562b94bb639..2d2afadc3d39 100644
--- a/src/Components/Components/src/Routing/Router.cs
+++ b/src/Components/Components/src/Routing/Router.cs
@@ -3,6 +3,7 @@
 
 #nullable disable warnings
 
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
 using System.Reflection.Metadata;
@@ -10,8 +11,8 @@
 using Microsoft.AspNetCore.Components.HotReload;
 using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.AspNetCore.Internal;
-using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
 
 namespace Microsoft.AspNetCore.Components.Routing;
 
@@ -222,10 +223,13 @@ internal virtual void Refresh(bool isNavigationIntercepted)
         var relativePath = NavigationManager.ToBaseRelativePath(_locationAbsolute.AsSpan());
         var locationPathSpan = TrimQueryOrHash(relativePath);
         var locationPath = $"/{locationPathSpan}";
+        Activity? activity = null;
 
         // In order to avoid routing twice we check for RouteData
         if (RoutingStateProvider?.RouteData is { } endpointRouteData)
         {
+            activity = RecordDiagnostics(endpointRouteData.PageType.FullName, endpointRouteData.Template);
+
             // Other routers shouldn't provide RouteData, this is specific to our router component
             // and must abide by our syntax and behaviors.
             // Other routers must create their own abstractions to flow data from their SSR routing
@@ -236,6 +240,8 @@ internal virtual void Refresh(bool isNavigationIntercepted)
             // - Convert constrained parameters with (int, double, etc) to the target type.
             endpointRouteData = RouteTable.ProcessParameters(endpointRouteData);
             _renderHandle.Render(Found(endpointRouteData));
+
+            activity?.Stop();
             return;
         }
 
@@ -252,6 +258,8 @@ internal virtual void Refresh(bool isNavigationIntercepted)
                     $"does not implement {typeof(IComponent).FullName}.");
             }
 
+            activity = RecordDiagnostics(context.Handler.FullName, context.Entry.RoutePattern.RawText);
+
             Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri);
 
             var routeData = new RouteData(
@@ -271,6 +279,8 @@ internal virtual void Refresh(bool isNavigationIntercepted)
         {
             if (!isNavigationIntercepted)
             {
+                activity = RecordDiagnostics("NotFound", "NotFound");
+
                 Log.DisplayingNotFound(_logger, locationPath, _baseUri);
 
                 // We did not find a Component that matches the route.
@@ -280,10 +290,30 @@ internal virtual void Refresh(bool isNavigationIntercepted)
             }
             else
             {
+                activity = RecordDiagnostics("External", "External");
+
                 Log.NavigatingToExternalUri(_logger, _locationAbsolute, locationPath, _baseUri);
                 NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true);
             }
         }
+        activity?.Stop();
+
+    }
+
+    private Activity? RecordDiagnostics(string componentType, string template)
+    {
+        Activity? activity = null;
+        if (_renderHandle.ComponentActivitySource != null)
+        {
+            activity = _renderHandle.ComponentActivitySource.StartRouteActivity(componentType, template);
+        }
+
+        if (_renderHandle.ComponentMetrics != null && _renderHandle.ComponentMetrics.IsNavigationEnabled)
+        {
+            _renderHandle.ComponentMetrics.Navigation(componentType, template);
+        }
+
+        return activity;
     }
 
     private static void DefaultNotFoundContent(RenderTreeBuilder builder)
diff --git a/src/Components/Components/test/ComponentsActivitySourceTest.cs b/src/Components/Components/test/ComponentsActivitySourceTest.cs
new file mode 100644
index 000000000000..660bf9428264
--- /dev/null
+++ b/src/Components/Components/test/ComponentsActivitySourceTest.cs
@@ -0,0 +1,251 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+public class ComponentsActivitySourceTest
+{
+    private readonly ActivityListener _listener;
+    private readonly List<Activity> _activities;
+
+    public ComponentsActivitySourceTest()
+    {
+        _activities = new List<Activity>();
+        _listener = new ActivityListener
+        {
+            ShouldListenTo = source => source.Name == ComponentsActivitySource.Name,
+            Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
+            ActivityStarted = activity => _activities.Add(activity),
+            ActivityStopped = activity => { }
+        };
+        ActivitySource.AddActivityListener(_listener);
+    }
+
+    [Fact]
+    public void Constructor_CreatesActivitySourceCorrectly()
+    {
+        // Arrange & Act
+        var componentsActivitySource = new ComponentsActivitySource();
+
+        // Assert
+        Assert.NotNull(componentsActivitySource);
+    }
+
+    [Fact]
+    public void CaptureHttpContext_ReturnsDefault_WhenNoCurrentActivity()
+    {
+        // Arrange
+        Activity.Current = null;
+
+        // Act
+        var result = ComponentsActivitySource.CaptureHttpContext();
+
+        // Assert
+        Assert.Equal(default, result);
+    }
+
+    [Fact]
+    public void CaptureHttpContext_ReturnsDefault_WhenActivityHasWrongName()
+    {
+        // Arrange
+        using var activity = new ActivitySource("Test").StartActivity("WrongName");
+        Activity.Current = activity;
+
+        // Act
+        var result = ComponentsActivitySource.CaptureHttpContext();
+
+        // Assert
+        Assert.Equal(default, result);
+    }
+
+    [Fact]
+    public void StartCircuitActivity_CreatesAndStartsActivity()
+    {
+        // Arrange
+        var componentsActivitySource = new ComponentsActivitySource();
+        var circuitId = "test-circuit-id";
+        var httpContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded);
+
+        // Act
+        var activity = componentsActivitySource.StartCircuitActivity(circuitId, httpContext);
+
+        // Assert
+        Assert.NotNull(activity);
+        Assert.Equal(ComponentsActivitySource.OnCircuitName, activity.OperationName);
+        Assert.Equal($"Circuit {circuitId}", activity.DisplayName);
+        Assert.Equal(ActivityKind.Internal, activity.Kind);
+        Assert.True(activity.IsAllDataRequested);
+        Assert.Equal(circuitId, activity.GetTagItem("aspnetcore.components.circuit.id"));
+        Assert.Contains(activity.Links, link => link.Context == httpContext);
+        Assert.False(activity.IsStopped);
+    }
+
+    [Fact]
+    public void FailCircuitActivity_SetsErrorStatusAndStopsActivity()
+    {
+        // Arrange
+        var componentsActivitySource = new ComponentsActivitySource();
+        var circuitId = "test-circuit-id";
+        var httpContext = default(ActivityContext);
+        var activity = componentsActivitySource.StartCircuitActivity(circuitId, httpContext);
+        var exception = new InvalidOperationException("Test exception");
+
+        // Act
+        componentsActivitySource.FailCircuitActivity(activity, exception);
+
+        // Assert
+        Assert.True(activity!.IsStopped);
+        Assert.Equal(ActivityStatusCode.Error, activity.Status);
+        Assert.Equal(exception.GetType().FullName, activity.GetTagItem("error.type"));
+    }
+
+    [Fact]
+    public void StartRouteActivity_CreatesAndStartsActivity()
+    {
+        // Arrange
+        var componentsActivitySource = new ComponentsActivitySource();
+        var componentType = "TestComponent";
+        var route = "/test-route";
+
+        // First set up a circuit context
+        componentsActivitySource.StartCircuitActivity("test-circuit-id", default);
+
+        // Act
+        var activity = componentsActivitySource.StartRouteActivity(componentType, route);
+
+        // Assert
+        Assert.NotNull(activity);
+        Assert.Equal(ComponentsActivitySource.OnRouteName, activity.OperationName);
+        Assert.Equal($"Route {route} -> {componentType}", activity.DisplayName);
+        Assert.Equal(ActivityKind.Internal, activity.Kind);
+        Assert.True(activity.IsAllDataRequested);
+        Assert.Equal(componentType, activity.GetTagItem("aspnetcore.components.type"));
+        Assert.Equal(route, activity.GetTagItem("aspnetcore.components.route"));
+        Assert.Equal("test-circuit-id", activity.GetTagItem("aspnetcore.components.circuit.id"));
+        Assert.False(activity.IsStopped);
+    }
+
+    [Fact]
+    public void StartEventActivity_CreatesAndStartsActivity()
+    {
+        // Arrange
+        var componentsActivitySource = new ComponentsActivitySource();
+        var componentType = "TestComponent";
+        var methodName = "OnClick";
+        var attributeName = "onclick";
+
+        // First set up a circuit and route context
+        componentsActivitySource.StartCircuitActivity("test-circuit-id", default);
+        componentsActivitySource.StartRouteActivity("ParentComponent", "/parent");
+
+        // Act
+        var activity = componentsActivitySource.StartEventActivity(componentType, methodName, attributeName);
+
+        // Assert
+        Assert.NotNull(activity);
+        Assert.Equal(ComponentsActivitySource.OnEventName, activity.OperationName);
+        Assert.Equal($"Event {attributeName} -> {componentType}.{methodName}", activity.DisplayName);
+        Assert.Equal(ActivityKind.Internal, activity.Kind);
+        Assert.True(activity.IsAllDataRequested);
+        Assert.Equal(componentType, activity.GetTagItem("aspnetcore.components.type"));
+        Assert.Equal(methodName, activity.GetTagItem("aspnetcore.components.method"));
+        Assert.Equal(attributeName, activity.GetTagItem("aspnetcore.components.attribute.name"));
+        Assert.Equal("test-circuit-id", activity.GetTagItem("aspnetcore.components.circuit.id"));
+        Assert.False(activity.IsStopped);
+    }
+
+    [Fact]
+    public void FailEventActivity_SetsErrorStatusAndStopsActivity()
+    {
+        // Arrange
+        var componentsActivitySource = new ComponentsActivitySource();
+        var activity = componentsActivitySource.StartEventActivity("TestComponent", "OnClick", "onclick");
+        var exception = new InvalidOperationException("Test exception");
+
+        // Act
+        ComponentsActivitySource.FailEventActivity(activity, exception);
+
+        // Assert
+        Assert.True(activity!.IsStopped);
+        Assert.Equal(ActivityStatusCode.Error, activity.Status);
+        Assert.Equal(exception.GetType().FullName, activity.GetTagItem("error.type"));
+    }
+
+    [Fact]
+    public async Task CaptureEventStopAsync_StopsActivityOnSuccessfulTask()
+    {
+        // Arrange
+        var componentsActivitySource = new ComponentsActivitySource();
+        var activity = componentsActivitySource.StartEventActivity("TestComponent", "OnClick", "onclick");
+        var task = Task.CompletedTask;
+
+        // Act
+        await ComponentsActivitySource.CaptureEventStopAsync(task, activity);
+
+        // Assert
+        Assert.True(activity!.IsStopped);
+        Assert.Equal(ActivityStatusCode.Unset, activity.Status);
+    }
+
+    [Fact]
+    public async Task CaptureEventStopAsync_FailsActivityOnException()
+    {
+        // Arrange
+        var componentsActivitySource = new ComponentsActivitySource();
+        var activity = componentsActivitySource.StartEventActivity("TestComponent", "OnClick", "onclick");
+        var exception = new InvalidOperationException("Test exception");
+        var task = Task.FromException(exception);
+
+        // Act
+        await ComponentsActivitySource.CaptureEventStopAsync(task, activity);
+
+        // Assert
+        Assert.True(activity!.IsStopped);
+        Assert.Equal(ActivityStatusCode.Error, activity.Status);
+        Assert.Equal(exception.GetType().FullName, activity.GetTagItem("error.type"));
+    }
+
+    [Fact]
+    public void StartCircuitActivity_HandlesNullValues()
+    {
+        // Arrange
+        var componentsActivitySource = new ComponentsActivitySource();
+
+        // Act
+        var activity = componentsActivitySource.StartCircuitActivity(null, default);
+
+        // Assert
+        Assert.NotNull(activity);
+        Assert.Equal("Circuit ", activity.DisplayName);
+    }
+
+    [Fact]
+    public void StartRouteActivity_HandlesNullValues()
+    {
+        // Arrange
+        var componentsActivitySource = new ComponentsActivitySource();
+
+        // Act
+        var activity = componentsActivitySource.StartRouteActivity(null, null);
+
+        // Assert
+        Assert.NotNull(activity);
+        Assert.Equal("Route [unknown path] -> [unknown component]", activity.DisplayName);
+    }
+
+    [Fact]
+    public void StartEventActivity_HandlesNullValues()
+    {
+        // Arrange
+        var componentsActivitySource = new ComponentsActivitySource();
+
+        // Act
+        var activity = componentsActivitySource.StartEventActivity(null, null, null);
+
+        // Assert
+        Assert.NotNull(activity);
+        Assert.Equal("Event [unknown attribute] -> [unknown component].[unknown method]", activity.DisplayName);
+    }
+}
diff --git a/src/Components/Components/test/ComponentsMetricsTest.cs b/src/Components/Components/test/ComponentsMetricsTest.cs
new file mode 100644
index 000000000000..e8b466d5e45d
--- /dev/null
+++ b/src/Components/Components/test/ComponentsMetricsTest.cs
@@ -0,0 +1,402 @@
+// 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.Extensions.Diagnostics.Metrics.Testing;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.InternalTesting;
+using Moq;
+
+namespace Microsoft.AspNetCore.Components;
+
+public class ComponentsMetricsTest
+{
+    private readonly TestMeterFactory _meterFactory;
+
+    public ComponentsMetricsTest()
+    {
+        _meterFactory = new TestMeterFactory();
+    }
+
+    [Fact]
+    public void Constructor_CreatesMetersCorrectly()
+    {
+        // Arrange & Act
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+
+        // Assert
+        Assert.Equal(2, _meterFactory.Meters.Count);
+        Assert.Contains(_meterFactory.Meters, m => m.Name == ComponentsMetrics.MeterName);
+        Assert.Contains(_meterFactory.Meters, m => m.Name == ComponentsMetrics.LifecycleMeterName);
+    }
+
+    [Fact]
+    public void Navigation_RecordsMetric()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var navigationCounter = new MetricCollector<long>(_meterFactory,
+            ComponentsMetrics.MeterName, "aspnetcore.components.navigation");
+
+        // Act
+        componentsMetrics.Navigation("TestComponent", "/test-route");
+
+        // Assert
+        var measurements = navigationCounter.GetMeasurementSnapshot();
+
+        Assert.Single(measurements);
+        Assert.Equal(1, measurements[0].Value);
+        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
+        Assert.Equal("/test-route", Assert.Contains("aspnetcore.components.route", measurements[0].Tags));
+    }
+
+    [Fact]
+    public void IsNavigationEnabled_ReturnsCorrectState()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+
+        // Create a collector to ensure the meter is enabled
+        using var navigationCounter = new MetricCollector<long>(_meterFactory,
+            ComponentsMetrics.MeterName, "aspnetcore.components.navigation");
+
+        // Act & Assert
+        Assert.True(componentsMetrics.IsNavigationEnabled);
+    }
+
+    [Fact]
+    public async Task CaptureEventDuration_RecordsSuccessMetric()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
+
+        // Act
+        var startTimestamp = Stopwatch.GetTimestamp();
+        await Task.Delay(10); // Small delay to ensure measureable duration
+        await componentsMetrics.CaptureEventDuration(Task.CompletedTask, startTimestamp,
+            "TestComponent", "OnClick", "onclick");
+
+        // Assert
+        var measurements = eventDurationHistogram.GetMeasurementSnapshot();
+
+        Assert.Single(measurements);
+        Assert.True(measurements[0].Value > 0);
+        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
+        Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", measurements[0].Tags));
+        Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", measurements[0].Tags));
+        Assert.DoesNotContain("error.type", measurements[0].Tags);
+    }
+
+    [Fact]
+    public async Task CaptureEventDuration_RecordsErrorMetric()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
+
+        // Act
+        var startTimestamp = Stopwatch.GetTimestamp();
+        await Task.Delay(10); // Small delay to ensure measureable duration
+        await componentsMetrics.CaptureEventDuration(Task.FromException(new InvalidOperationException()),
+            startTimestamp, "TestComponent", "OnClick", "onclick");
+
+        // Assert
+        var measurements = eventDurationHistogram.GetMeasurementSnapshot();
+
+        Assert.Single(measurements);
+        Assert.True(measurements[0].Value > 0);
+        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
+        Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", measurements[0].Tags));
+        Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", measurements[0].Tags));
+        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
+    }
+
+    [Fact]
+    public void FailEventSync_RecordsErrorMetric()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
+        var exception = new InvalidOperationException();
+
+        // Act
+        var startTimestamp = Stopwatch.GetTimestamp();
+        componentsMetrics.FailEventSync(exception, startTimestamp,
+            "TestComponent", "OnClick", "onclick");
+
+        // Assert
+        var measurements = eventDurationHistogram.GetMeasurementSnapshot();
+
+        Assert.Single(measurements);
+        Assert.True(measurements[0].Value > 0);
+        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
+        Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", measurements[0].Tags));
+        Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", measurements[0].Tags));
+        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
+    }
+
+    [Fact]
+    public void IsEventEnabled_ReturnsCorrectState()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+
+        // Create a collector to ensure the meter is enabled
+        using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
+
+        // Act & Assert
+        Assert.True(componentsMetrics.IsEventEnabled);
+    }
+
+    [Fact]
+    public async Task CaptureParametersDuration_RecordsSuccessMetric()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
+
+        // Act
+        var startTimestamp = Stopwatch.GetTimestamp();
+        await Task.Delay(10); // Small delay to ensure measureable duration
+        await componentsMetrics.CaptureParametersDuration(Task.CompletedTask, startTimestamp, "TestComponent");
+
+        // Assert
+        var measurements = parametersDurationHistogram.GetMeasurementSnapshot();
+
+        Assert.Single(measurements);
+        Assert.True(measurements[0].Value > 0);
+        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
+        Assert.DoesNotContain("error.type", measurements[0].Tags);
+    }
+
+    [Fact]
+    public async Task CaptureParametersDuration_RecordsErrorMetric()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
+
+        // Act
+        var startTimestamp = Stopwatch.GetTimestamp();
+        await Task.Delay(10); // Small delay to ensure measureable duration
+        await componentsMetrics.CaptureParametersDuration(Task.FromException(new InvalidOperationException()),
+            startTimestamp, "TestComponent");
+
+        // Assert
+        var measurements = parametersDurationHistogram.GetMeasurementSnapshot();
+
+        Assert.Single(measurements);
+        Assert.True(measurements[0].Value > 0);
+        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
+        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
+    }
+
+    [Fact]
+    public void FailParametersSync_RecordsErrorMetric()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
+        var exception = new InvalidOperationException();
+
+        // Act
+        var startTimestamp = Stopwatch.GetTimestamp();
+        componentsMetrics.FailParametersSync(exception, startTimestamp, "TestComponent");
+
+        // Assert
+        var measurements = parametersDurationHistogram.GetMeasurementSnapshot();
+
+        Assert.Single(measurements);
+        Assert.True(measurements[0].Value > 0);
+        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", measurements[0].Tags));
+        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
+    }
+
+    [Fact]
+    public void IsParametersEnabled_ReturnsCorrectState()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+
+        // Create a collector to ensure the meter is enabled
+        using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
+
+        // Act & Assert
+        Assert.True(componentsMetrics.IsParametersEnabled);
+    }
+
+    [Fact]
+    public async Task CaptureBatchDuration_RecordsSuccessMetric()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
+
+        // Act
+        var startTimestamp = Stopwatch.GetTimestamp();
+        await Task.Delay(10); // Small delay to ensure measureable duration
+        await componentsMetrics.CaptureBatchDuration(Task.CompletedTask, startTimestamp, 25);
+
+        // Assert
+        var measurements = batchDurationHistogram.GetMeasurementSnapshot();
+
+        Assert.Single(measurements);
+        Assert.True(measurements[0].Value > 0);
+        Assert.Equal(50, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags));
+        Assert.DoesNotContain("error.type", measurements[0].Tags);
+    }
+
+    [Fact]
+    public async Task CaptureBatchDuration_RecordsErrorMetric()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
+
+        // Act
+        var startTimestamp = Stopwatch.GetTimestamp();
+        await Task.Delay(10); // Small delay to ensure measureable duration
+        await componentsMetrics.CaptureBatchDuration(Task.FromException(new InvalidOperationException()),
+            startTimestamp, 25);
+
+        // Assert
+        var measurements = batchDurationHistogram.GetMeasurementSnapshot();
+
+        Assert.Single(measurements);
+        Assert.True(measurements[0].Value > 0);
+        Assert.Equal(50, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags));
+        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
+    }
+
+    [Fact]
+    public void FailBatchSync_RecordsErrorMetric()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
+        var exception = new InvalidOperationException();
+
+        // Act
+        var startTimestamp = Stopwatch.GetTimestamp();
+        componentsMetrics.FailBatchSync(exception, startTimestamp);
+
+        // Assert
+        var measurements = batchDurationHistogram.GetMeasurementSnapshot();
+
+        Assert.Single(measurements);
+        Assert.True(measurements[0].Value > 0);
+        Assert.Equal(0, Assert.Contains("aspnetcore.components.diff.length", measurements[0].Tags));
+        Assert.Equal("System.InvalidOperationException", Assert.Contains("error.type", measurements[0].Tags));
+    }
+
+    [Fact]
+    public void IsBatchEnabled_ReturnsCorrectState()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+
+        // Create a collector to ensure the meter is enabled
+        using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
+
+        // Act & Assert
+        Assert.True(componentsMetrics.IsBatchEnabled);
+    }
+
+    [Fact]
+    public async Task ComponentLifecycle_RecordsAllMetricsCorrectly()
+    {
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+        using var navigationCounter = new MetricCollector<long>(_meterFactory,
+            ComponentsMetrics.MeterName, "aspnetcore.components.navigation");
+        using var eventDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.MeterName, "aspnetcore.components.event_handler");
+        using var parametersDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters");
+        using var batchDurationHistogram = new MetricCollector<double>(_meterFactory,
+            ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff");
+
+        // Act - Simulate a component lifecycle
+        // 1. Navigation
+        componentsMetrics.Navigation("TestComponent", "/test-route");
+
+        // 2. Parameters update
+        var startTimestamp1 = Stopwatch.GetTimestamp();
+        await Task.Delay(10);
+        await componentsMetrics.CaptureParametersDuration(Task.CompletedTask, startTimestamp1, "TestComponent");
+
+        // 3. Event handler
+        var startTimestamp2 = Stopwatch.GetTimestamp();
+        await Task.Delay(10);
+        await componentsMetrics.CaptureEventDuration(Task.CompletedTask, startTimestamp2,
+            "TestComponent", "OnClick", "onclick");
+
+        // 4. Rendering batch
+        var startTimestamp3 = Stopwatch.GetTimestamp();
+        await Task.Delay(10);
+        await componentsMetrics.CaptureBatchDuration(Task.CompletedTask, startTimestamp3, 15);
+
+        // Assert
+        var navigationMeasurements = navigationCounter.GetMeasurementSnapshot();
+        var eventMeasurements = eventDurationHistogram.GetMeasurementSnapshot();
+        var parametersMeasurements = parametersDurationHistogram.GetMeasurementSnapshot();
+        var batchMeasurements = batchDurationHistogram.GetMeasurementSnapshot();
+
+        Assert.Single(navigationMeasurements);
+        Assert.Single(eventMeasurements);
+        Assert.Single(parametersMeasurements);
+        Assert.Single(batchMeasurements);
+
+        // Check navigation
+        Assert.Equal(1, navigationMeasurements[0].Value);
+        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", navigationMeasurements[0].Tags));
+        Assert.Equal("/test-route", Assert.Contains("aspnetcore.components.route", navigationMeasurements[0].Tags));
+
+        // Check event duration
+        Assert.True(eventMeasurements[0].Value > 0);
+        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", eventMeasurements[0].Tags));
+        Assert.Equal("OnClick", Assert.Contains("aspnetcore.components.method", eventMeasurements[0].Tags));
+        Assert.Equal("onclick", Assert.Contains("aspnetcore.components.attribute.name", eventMeasurements[0].Tags));
+
+        // Check parameters duration
+        Assert.True(parametersMeasurements[0].Value > 0);
+        Assert.Equal("TestComponent", Assert.Contains("aspnetcore.components.type", parametersMeasurements[0].Tags));
+
+        // Check batch duration
+        Assert.True(batchMeasurements[0].Value > 0);
+        Assert.Equal(20, Assert.Contains("aspnetcore.components.diff.length", batchMeasurements[0].Tags));
+    }
+
+    [Fact]
+    public void Dispose_DisposesAllMeters()
+    {
+        // This test verifies that disposing ComponentsMetrics properly disposes its meters
+        // Since we can't easily test disposal directly, we'll verify meters are created and assume
+        // the dispose method works as expected
+
+        // Arrange
+        var componentsMetrics = new ComponentsMetrics(_meterFactory);
+
+        // Act - We're not actually asserting anything here, just ensuring no exceptions are thrown
+        componentsMetrics.Dispose();
+
+        // Assert - MeterFactory.Create was called twice in constructor
+        Assert.Equal(2, _meterFactory.Meters.Count);
+    }
+}
diff --git a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs
deleted file mode 100644
index 7339ebbf5dec..000000000000
--- a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs
+++ /dev/null
@@ -1,238 +0,0 @@
-// 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.Extensions.Diagnostics.Metrics.Testing;
-using Microsoft.Extensions.Logging.Abstractions;
-using Microsoft.Extensions.Options;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.AspNetCore.InternalTesting;
-using Moq;
-
-namespace Microsoft.AspNetCore.Components.Rendering;
-
-public class RenderingMetricsTest
-{
-    private readonly TestMeterFactory _meterFactory;
-
-    public RenderingMetricsTest()
-    {
-        _meterFactory = new TestMeterFactory();
-    }
-
-    [Fact]
-    public void Constructor_CreatesMetersCorrectly()
-    {
-        // Arrange & Act
-        var renderingMetrics = new RenderingMetrics(_meterFactory);
-
-        // Assert
-        Assert.Single(_meterFactory.Meters);
-        Assert.Equal(RenderingMetrics.MeterName, _meterFactory.Meters[0].Name);
-    }
-
-    [Fact]
-    public void RenderStart_IncreasesCounters()
-    {
-        // Arrange
-        var renderingMetrics = new RenderingMetrics(_meterFactory);
-        using var totalCounter = new MetricCollector<long>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.count");
-        using var activeCounter = new MetricCollector<long>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
-
-        var componentType = "TestComponent";
-
-        // Act
-        renderingMetrics.RenderStart(componentType);
-
-        // Assert
-        var totalMeasurements = totalCounter.GetMeasurementSnapshot();
-        var activeMeasurements = activeCounter.GetMeasurementSnapshot();
-
-        Assert.Single(totalMeasurements);
-        Assert.Equal(1, totalMeasurements[0].Value);
-        Assert.Equal(componentType, totalMeasurements[0].Tags["component.type"]);
-
-        Assert.Single(activeMeasurements);
-        Assert.Equal(1, activeMeasurements[0].Value);
-        Assert.Equal(componentType, activeMeasurements[0].Tags["component.type"]);
-    }
-
-    [Fact]
-    public void RenderEnd_DecreasesActiveCounterAndRecordsDuration()
-    {
-        // Arrange
-        var renderingMetrics = new RenderingMetrics(_meterFactory);
-        using var activeCounter = new MetricCollector<long>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
-        using var durationCollector = new MetricCollector<double>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration");
-
-        var componentType = "TestComponent";
-
-        // Act
-        var startTime = Stopwatch.GetTimestamp();
-        Thread.Sleep(10); // Add a small delay to ensure a measurable duration
-        var endTime = Stopwatch.GetTimestamp();
-        renderingMetrics.RenderEnd(componentType, null, startTime, endTime);
-
-        // Assert
-        var activeMeasurements = activeCounter.GetMeasurementSnapshot();
-        var durationMeasurements = durationCollector.GetMeasurementSnapshot();
-
-        Assert.Single(activeMeasurements);
-        Assert.Equal(-1, activeMeasurements[0].Value);
-        Assert.Equal(componentType, activeMeasurements[0].Tags["component.type"]);
-
-        Assert.Single(durationMeasurements);
-        Assert.True(durationMeasurements[0].Value > 0);
-        Assert.Equal(componentType, durationMeasurements[0].Tags["component.type"]);
-    }
-
-    [Fact]
-    public void RenderEnd_AddsErrorTypeTag_WhenExceptionIsProvided()
-    {
-        // Arrange
-        var renderingMetrics = new RenderingMetrics(_meterFactory);
-        using var durationCollector = new MetricCollector<double>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration");
-
-        var componentType = "TestComponent";
-        var exception = new InvalidOperationException("Test exception");
-
-        // Act
-        var startTime = Stopwatch.GetTimestamp();
-        Thread.Sleep(10);
-        var endTime = Stopwatch.GetTimestamp();
-        renderingMetrics.RenderEnd(componentType, exception, startTime, endTime);
-
-        // Assert
-        var durationMeasurements = durationCollector.GetMeasurementSnapshot();
-
-        Assert.Single(durationMeasurements);
-        Assert.True(durationMeasurements[0].Value > 0);
-        Assert.Equal(componentType, durationMeasurements[0].Tags["component.type"]);
-        Assert.Equal(exception.GetType().FullName, durationMeasurements[0].Tags["error.type"]);
-    }
-
-    [Fact]
-    public void IsDurationEnabled_ReturnsMeterEnabledState()
-    {
-        // Arrange
-        var renderingMetrics = new RenderingMetrics(_meterFactory);
-
-        // Create a collector to ensure the meter is enabled
-        using var durationCollector = new MetricCollector<double>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration");
-
-        // Act & Assert
-        Assert.True(renderingMetrics.IsDurationEnabled());
-    }
-
-    [Fact]
-    public void FullRenderingLifecycle_RecordsAllMetricsCorrectly()
-    {
-        // Arrange
-        var renderingMetrics = new RenderingMetrics(_meterFactory);
-        using var totalCounter = new MetricCollector<long>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.count");
-        using var activeCounter = new MetricCollector<long>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
-        using var durationCollector = new MetricCollector<double>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration");
-
-        var componentType = "TestComponent";
-
-        // Act - Simulating a full rendering lifecycle
-        var startTime = Stopwatch.GetTimestamp();
-
-        // 1. Component render starts
-        renderingMetrics.RenderStart(componentType);
-
-        // 2. Component render ends
-        Thread.Sleep(10); // Add a small delay to ensure a measurable duration
-        var endTime = Stopwatch.GetTimestamp();
-        renderingMetrics.RenderEnd(componentType, null, startTime, endTime);
-
-        // Assert
-        var totalMeasurements = totalCounter.GetMeasurementSnapshot();
-        var activeMeasurements = activeCounter.GetMeasurementSnapshot();
-        var durationMeasurements = durationCollector.GetMeasurementSnapshot();
-
-        // Total render count should have 1 measurement with value 1
-        Assert.Single(totalMeasurements);
-        Assert.Equal(1, totalMeasurements[0].Value);
-        Assert.Equal(componentType, totalMeasurements[0].Tags["component.type"]);
-
-        // Active render count should have 2 measurements (1 for start, -1 for end)
-        Assert.Equal(2, activeMeasurements.Count);
-        Assert.Equal(1, activeMeasurements[0].Value);
-        Assert.Equal(-1, activeMeasurements[1].Value);
-        Assert.Equal(componentType, activeMeasurements[0].Tags["component.type"]);
-        Assert.Equal(componentType, activeMeasurements[1].Tags["component.type"]);
-
-        // Duration should have 1 measurement with a positive value
-        Assert.Single(durationMeasurements);
-        Assert.True(durationMeasurements[0].Value > 0);
-        Assert.Equal(componentType, durationMeasurements[0].Tags["component.type"]);
-    }
-
-    [Fact]
-    public void MultipleRenders_TracksMetricsIndependently()
-    {
-        // Arrange
-        var renderingMetrics = new RenderingMetrics(_meterFactory);
-        using var totalCounter = new MetricCollector<long>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.count");
-        using var activeCounter = new MetricCollector<long>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
-        using var durationCollector = new MetricCollector<double>(_meterFactory,
-            RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration");
-
-        var componentType1 = "TestComponent1";
-        var componentType2 = "TestComponent2";
-
-        // Act
-        // First component render
-        var startTime1 = Stopwatch.GetTimestamp();
-        renderingMetrics.RenderStart(componentType1);
-
-        // Second component render starts while first is still rendering
-        var startTime2 = Stopwatch.GetTimestamp();
-        renderingMetrics.RenderStart(componentType2);
-
-        // First component render ends
-        Thread.Sleep(5);
-        var endTime1 = Stopwatch.GetTimestamp();
-        renderingMetrics.RenderEnd(componentType1, null, startTime1, endTime1);
-
-        // Second component render ends
-        Thread.Sleep(5);
-        var endTime2 = Stopwatch.GetTimestamp();
-        renderingMetrics.RenderEnd(componentType2, null, startTime2, endTime2);
-
-        // Assert
-        var totalMeasurements = totalCounter.GetMeasurementSnapshot();
-        var activeMeasurements = activeCounter.GetMeasurementSnapshot();
-        var durationMeasurements = durationCollector.GetMeasurementSnapshot();
-
-        // Should have 2 total render counts (one for each component)
-        Assert.Equal(2, totalMeasurements.Count);
-        Assert.Contains(totalMeasurements, m => m.Value == 1 && m.Tags["component.type"] as string == componentType1);
-        Assert.Contains(totalMeasurements, m => m.Value == 1 && m.Tags["component.type"] as string == componentType2);
-
-        // Should have 4 active render counts (start and end for each component)
-        Assert.Equal(4, activeMeasurements.Count);
-        Assert.Contains(activeMeasurements, m => m.Value == 1 && m.Tags["component.type"] as string == componentType1);
-        Assert.Contains(activeMeasurements, m => m.Value == 1 && m.Tags["component.type"] as string == componentType2);
-        Assert.Contains(activeMeasurements, m => m.Value == -1 && m.Tags["component.type"] as string == componentType1);
-        Assert.Contains(activeMeasurements, m => m.Value == -1 && m.Tags["component.type"] as string == componentType2);
-
-        // Should have 2 duration measurements (one for each component)
-        Assert.Equal(2, durationMeasurements.Count);
-        Assert.Contains(durationMeasurements, m => m.Value > 0 && m.Tags["component.type"] as string == componentType1);
-        Assert.Contains(durationMeasurements, m => m.Value > 0 && m.Tags["component.type"] as string == componentType2);
-    }
-}
diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs
index 302dec7dcb16..de2e1f4707e0 100644
--- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs
+++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs
@@ -76,6 +76,9 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
 
         services.TryAddScoped<ResourceCollectionProvider>();
 
+        ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(services);
+        ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(services);
+
         // Form handling
         services.AddSupplyValueFromFormProvider();
         services.TryAddScoped<AntiforgeryStateProvider, EndpointAntiforgeryStateProvider>();
diff --git a/src/Components/Server/src/Circuits/CircuitFactory.cs b/src/Components/Server/src/Circuits/CircuitFactory.cs
index cb8573bd81b1..6683c2e20d75 100644
--- a/src/Components/Server/src/Circuits/CircuitFactory.cs
+++ b/src/Components/Server/src/Circuits/CircuitFactory.cs
@@ -66,6 +66,7 @@ public async ValueTask<CircuitHost> CreateCircuitHostAsync(
         {
             navigationManager.Initialize(baseUri, uri);
         }
+        var componentsActivitySource = scope.ServiceProvider.GetService<ComponentsActivitySource>();
 
         if (components.Count > 0)
         {
@@ -109,6 +110,7 @@ public async ValueTask<CircuitHost> CreateCircuitHostAsync(
             navigationManager,
             circuitHandlers,
             _circuitMetrics,
+            componentsActivitySource,
             _loggerFactory.CreateLogger<CircuitHost>());
         Log.CreatedCircuit(_logger, circuitHost);
 
diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs
index b8bc2b05e158..38b50461ce3e 100644
--- a/src/Components/Server/src/Circuits/CircuitHost.cs
+++ b/src/Components/Server/src/Circuits/CircuitHost.cs
@@ -25,6 +25,7 @@ internal partial class CircuitHost : IAsyncDisposable
     private readonly RemoteNavigationManager _navigationManager;
     private readonly ILogger _logger;
     private readonly CircuitMetrics? _circuitMetrics;
+    private readonly ComponentsActivitySource? _componentsActivitySource;
     private Func<Func<Task>, Task> _dispatchInboundActivity;
     private CircuitHandler[] _circuitHandlers;
     private bool _initialized;
@@ -51,6 +52,7 @@ public CircuitHost(
         RemoteNavigationManager navigationManager,
         CircuitHandler[] circuitHandlers,
         CircuitMetrics? circuitMetrics,
+        ComponentsActivitySource? componentsActivitySource,
         ILogger logger)
     {
         CircuitId = circuitId;
@@ -69,6 +71,7 @@ public CircuitHost(
         _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
         _circuitHandlers = circuitHandlers ?? throw new ArgumentNullException(nameof(circuitHandlers));
         _circuitMetrics = circuitMetrics;
+        _componentsActivitySource = componentsActivitySource;
         _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 
         Services = scope.ServiceProvider;
@@ -105,7 +108,7 @@ public CircuitHost(
 
     // InitializeAsync is used in a fire-and-forget context, so it's responsible for its own
     // error handling.
-    public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, CancellationToken cancellationToken)
+    public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, ActivityContext httpContext, CancellationToken cancellationToken)
     {
         Log.InitializationStarted(_logger);
 
@@ -115,13 +118,17 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, C
             {
                 throw new InvalidOperationException("The circuit host is already initialized.");
             }
+            Activity? activity = null;
 
             try
             {
                 _initialized = true; // We're ready to accept incoming JSInterop calls from here on
 
+                activity = _componentsActivitySource?.StartCircuitActivity(CircuitId.Id, httpContext);
+                _startTime = (_circuitMetrics != null && _circuitMetrics.IsDurationEnabled()) ? Stopwatch.GetTimestamp() : 0;
+
                 // We only run the handlers in case we are in a Blazor Server scenario, which renders
-                // the components inmediately during start.
+                // the components immediately during start.
                 // On Blazor Web scenarios we delay running these handlers until the first UpdateRootComponents call
                 // We do this so that the handlers can have access to the restored application state.
                 if (Descriptors.Count > 0)
@@ -162,9 +169,13 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, C
                 _isFirstUpdate = Descriptors.Count == 0;
 
                 Log.InitializationSucceeded(_logger);
+
+                activity?.Stop();
             }
             catch (Exception ex)
             {
+                _componentsActivitySource?.FailCircuitActivity(activity, ex);
+
                 // Report errors asynchronously. InitializeAsync is designed not to throw.
                 Log.InitializationFailed(_logger, ex);
                 UnhandledException?.Invoke(this, new UnhandledExceptionEventArgs(ex, isTerminating: false));
@@ -235,7 +246,6 @@ await Renderer.Dispatcher.InvokeAsync(async () =>
     private async Task OnCircuitOpenedAsync(CancellationToken cancellationToken)
     {
         Log.CircuitOpened(_logger, CircuitId);
-        _startTime = (_circuitMetrics != null && _circuitMetrics.IsDurationEnabled()) ? Stopwatch.GetTimestamp() : 0;
         _circuitMetrics?.OnCircuitOpened();
 
         Renderer.Dispatcher.AssertAccess();
diff --git a/src/Components/Server/src/Circuits/CircuitMetrics.cs b/src/Components/Server/src/Circuits/CircuitMetrics.cs
index fb772c119f51..9432c6d18678 100644
--- a/src/Components/Server/src/Circuits/CircuitMetrics.cs
+++ b/src/Components/Server/src/Circuits/CircuitMetrics.cs
@@ -12,7 +12,6 @@ internal sealed class CircuitMetrics : IDisposable
     public const string MeterName = "Microsoft.AspNetCore.Components.Server.Circuits";
 
     private readonly Meter _meter;
-    private readonly Counter<long> _circuitTotalCounter;
     private readonly UpDownCounter<long> _circuitActiveCounter;
     private readonly UpDownCounter<long> _circuitConnectedCounter;
     private readonly Histogram<double> _circuitDuration;
@@ -23,81 +22,63 @@ public CircuitMetrics(IMeterFactory meterFactory)
 
         _meter = meterFactory.Create(MeterName);
 
-        _circuitTotalCounter = _meter.CreateCounter<long>(
-            "aspnetcore.components.circuits.count",
-            unit: "{circuits}",
-            description: "Number of active circuits.");
-
         _circuitActiveCounter = _meter.CreateUpDownCounter<long>(
-            "aspnetcore.components.circuits.active_circuits",
-            unit: "{circuits}",
-            description: "Number of active circuits.");
+            "aspnetcore.components.circuit.active",
+            unit: "{circuit}",
+            description: "Number of active circuits in memory.");
 
         _circuitConnectedCounter = _meter.CreateUpDownCounter<long>(
-            "aspnetcore.components.circuits.connected_circuits",
-            unit: "{circuits}",
-            description: "Number of disconnected circuits.");
+            "aspnetcore.components.circuit.connected",
+            unit: "{circuit}",
+            description: "Number of circuits connected to client.");
 
         _circuitDuration = _meter.CreateHistogram<double>(
-            "aspnetcore.components.circuits.duration",
+            "aspnetcore.components.circuit.duration",
             unit: "s",
-            description: "Duration of circuit.",
-            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.VeryLongSecondsBucketBoundaries });
+            description: "Duration of circuit lifetime and their total count.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.BlazorCircuitSecondsBucketBoundaries });
     }
 
     public void OnCircuitOpened()
     {
-        var tags = new TagList();
-
         if (_circuitActiveCounter.Enabled)
         {
-            _circuitActiveCounter.Add(1, tags);
-        }
-        if (_circuitTotalCounter.Enabled)
-        {
-            _circuitTotalCounter.Add(1, tags);
+            _circuitActiveCounter.Add(1);
         }
     }
 
     public void OnConnectionUp()
     {
-        var tags = new TagList();
-
         if (_circuitConnectedCounter.Enabled)
         {
-            _circuitConnectedCounter.Add(1, tags);
+            _circuitConnectedCounter.Add(1);
         }
     }
 
     public void OnConnectionDown()
     {
-        var tags = new TagList();
-
         if (_circuitConnectedCounter.Enabled)
         {
-            _circuitConnectedCounter.Add(-1, tags);
+            _circuitConnectedCounter.Add(-1);
         }
     }
 
     public void OnCircuitDown(long startTimestamp, long currentTimestamp)
     {
-        // Tags must match request start.
-        var tags = new TagList();
-
         if (_circuitActiveCounter.Enabled)
         {
-            _circuitActiveCounter.Add(-1, tags);
+            _circuitActiveCounter.Add(-1);
         }
 
         if (_circuitConnectedCounter.Enabled)
         {
-            _circuitConnectedCounter.Add(-1, tags);
+            _circuitConnectedCounter.Add(-1);
         }
 
         if (_circuitDuration.Enabled)
         {
             var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp);
-            _circuitDuration.Record(duration.TotalSeconds, tags);
+            _circuitDuration.Record(duration.TotalSeconds);
         }
     }
 
diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs
index 31b29206212b..7f2345bff74a 100644
--- a/src/Components/Server/src/Circuits/RemoteRenderer.cs
+++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs
@@ -60,11 +60,11 @@ public RemoteRenderer(
 
     public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
 
-    protected override ResourceAssetCollection Assets => _resourceCollection ?? base.Assets;
+    protected internal override ResourceAssetCollection Assets => _resourceCollection ?? base.Assets;
 
-    protected override RendererInfo RendererInfo => _componentPlatform;
+    protected internal override RendererInfo RendererInfo => _componentPlatform;
 
-    protected override IComponentRenderMode? GetComponentRenderMode(IComponent component) => RenderMode.InteractiveServer;
+    protected internal override IComponentRenderMode? GetComponentRenderMode(IComponent component) => RenderMode.InteractiveServer;
 
     public Task AddComponentAsync(Type componentType, ParameterView parameters, string domElementSelector)
     {
@@ -306,7 +306,7 @@ public Task OnRenderCompletedAsync(long incomingBatchId, string? errorMessageOrN
         }
     }
 
-    protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode)
+    protected internal override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode)
         => renderMode switch
         {
             InteractiveServerRenderMode or InteractiveAutoRenderMode => componentActivator.CreateInstance(componentType),
@@ -369,7 +369,7 @@ private async Task CaptureAsyncExceptions(Task task)
         }
     }
 
-    private static partial class Log
+    private static new partial class Log
     {
         [LoggerMessage(100, LogLevel.Warning, "Unhandled exception rendering component: {Message}", EventName = "ExceptionRenderingComponent")]
         private static partial void UnhandledExceptionRenderingComponent(ILogger logger, string message, Exception exception);
diff --git a/src/Components/Server/src/ComponentHub.cs b/src/Components/Server/src/ComponentHub.cs
index b3308f17bfd7..84561349ee48 100644
--- a/src/Components/Server/src/ComponentHub.cs
+++ b/src/Components/Server/src/ComponentHub.cs
@@ -2,6 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Buffers;
+using System.Diagnostics;
 using System.Runtime.CompilerServices;
 using Microsoft.AspNetCore.Components.Server.Circuits;
 using Microsoft.AspNetCore.DataProtection;
@@ -43,6 +44,7 @@ internal sealed partial class ComponentHub : Hub
     private readonly CircuitRegistry _circuitRegistry;
     private readonly ICircuitHandleRegistry _circuitHandleRegistry;
     private readonly ILogger _logger;
+    private readonly ActivityContext _httpContext;
 
     public ComponentHub(
         IServerComponentDeserializer serializer,
@@ -60,6 +62,7 @@ public ComponentHub(
         _circuitRegistry = circuitRegistry;
         _circuitHandleRegistry = circuitHandleRegistry;
         _logger = logger;
+        _httpContext = ComponentsActivitySource.CaptureHttpContext();
     }
 
     /// <summary>
@@ -137,7 +140,7 @@ public async ValueTask<string> StartCircuit(string baseUri, string uri, string s
             // SignalR message loop (we'd get a deadlock if any of the initialization
             // logic relied on receiving a subsequent message from SignalR), and it will
             // take care of its own errors anyway.
-            _ = circuitHost.InitializeAsync(store, Context.ConnectionAborted);
+            _ = circuitHost.InitializeAsync(store, _httpContext, Context.ConnectionAborted);
 
             // It's safe to *publish* the circuit now because nothing will be able
             // to run inside it until after InitializeAsync completes.
diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs
index 08195b0218c7..4c6eb34d27f6 100644
--- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs
+++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs
@@ -2,7 +2,6 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Diagnostics.CodeAnalysis;
-using System.Diagnostics.Metrics;
 using Microsoft.AspNetCore.Components;
 using Microsoft.AspNetCore.Components.Authorization;
 using Microsoft.AspNetCore.Components.Forms;
@@ -62,11 +61,7 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
         // user's configuration. So even if the user has multiple independent server-side
         // Components entrypoints, this lot is the same and repeated registrations are a no-op.
 
-        services.TryAddSingleton(s =>
-        {
-            var meterFactory = s.GetService<IMeterFactory>();
-            return meterFactory != null ? new CircuitMetrics(meterFactory) : null;
-        });
+        services.TryAddSingleton<CircuitMetrics>();
         services.TryAddSingleton<ICircuitFactory, CircuitFactory>();
         services.TryAddSingleton<ICircuitHandleRegistry, CircuitHandleRegistry>();
         services.TryAddSingleton<RootTypeCache>();
diff --git a/src/Components/Server/test/Circuits/CircuitHostTest.cs b/src/Components/Server/test/Circuits/CircuitHostTest.cs
index 47d7e7cf5a76..b68bdf8286fa 100644
--- a/src/Components/Server/test/Circuits/CircuitHostTest.cs
+++ b/src/Components/Server/test/Circuits/CircuitHostTest.cs
@@ -193,7 +193,7 @@ public async Task InitializeAsync_InvokesHandlers()
         var circuitHost = TestCircuitHost.Create(handlers: new[] { handler1.Object, handler2.Object });
 
         // Act
-        await circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of<IDataProtectionProvider>()), cancellationToken);
+        await circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of<IDataProtectionProvider>()), default, cancellationToken);
 
         // Assert
         handler1.VerifyAll();
@@ -236,7 +236,7 @@ public async Task InitializeAsync_RendersRootComponentsInParallel()
         // Act
         object initializeException = null;
         circuitHost.UnhandledException += (sender, eventArgs) => initializeException = eventArgs.ExceptionObject;
-        var initializeTask = circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of<IDataProtectionProvider>()), cancellationToken);
+        var initializeTask = circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of<IDataProtectionProvider>()), default, cancellationToken);
         await initializeTask.WaitAsync(initializeTimeout);
 
         // Assert: This was not reached only because an exception was thrown in InitializeAsync()
@@ -266,7 +266,7 @@ public async Task InitializeAsync_ReportsOwnAsyncExceptions()
         };
 
         // Act
-        var initializeAsyncTask = circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of<IDataProtectionProvider>()), new CancellationToken());
+        var initializeAsyncTask = circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of<IDataProtectionProvider>()), default, new CancellationToken());
 
         // Assert: No synchronous exceptions
         handler.VerifyAll();
diff --git a/src/Components/Server/test/Circuits/CircuitIdFactoryTest.cs b/src/Components/Server/test/Circuits/CircuitIdFactoryTest.cs
index fd8c950dc6fd..4c89b7ec182a 100644
--- a/src/Components/Server/test/Circuits/CircuitIdFactoryTest.cs
+++ b/src/Components/Server/test/Circuits/CircuitIdFactoryTest.cs
@@ -5,7 +5,7 @@
 
 namespace Microsoft.AspNetCore.Components.Server.Circuits;
 
-public class circuitIdFactoryTest
+public class CircuitIdFactoryTest
 {
     [Fact]
     public void CreateCircuitId_Generates_NewRandomId()
diff --git a/src/Components/Server/test/Circuits/CircuitMetricsTest.cs b/src/Components/Server/test/Circuits/CircuitMetricsTest.cs
index 770125996634..8394b01036dc 100644
--- a/src/Components/Server/test/Circuits/CircuitMetricsTest.cs
+++ b/src/Components/Server/test/Circuits/CircuitMetricsTest.cs
@@ -39,21 +39,15 @@ public void OnCircuitOpened_IncreasesCounters()
     {
         // Arrange
         var circuitMetrics = new CircuitMetrics(_meterFactory);
-        using var activeTotalCounter = new MetricCollector<long>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.count");
         using var activeCircuitCounter = new MetricCollector<long>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.active_circuits");
+            CircuitMetrics.MeterName, "aspnetcore.components.circuit.active");
 
         // Act
         circuitMetrics.OnCircuitOpened();
 
         // Assert
-        var totalMeasurements = activeTotalCounter.GetMeasurementSnapshot();
         var activeMeasurements = activeCircuitCounter.GetMeasurementSnapshot();
 
-        Assert.Single(totalMeasurements);
-        Assert.Equal(1, totalMeasurements[0].Value);
-
         Assert.Single(activeMeasurements);
         Assert.Equal(1, activeMeasurements[0].Value);
     }
@@ -64,7 +58,7 @@ public void OnConnectionUp_IncreasesConnectedCounter()
         // Arrange
         var circuitMetrics = new CircuitMetrics(_meterFactory);
         using var connectedCircuitCounter = new MetricCollector<long>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.connected_circuits");
+            CircuitMetrics.MeterName, "aspnetcore.components.circuit.connected");
 
         // Act
         circuitMetrics.OnConnectionUp();
@@ -82,7 +76,7 @@ public void OnConnectionDown_DecreasesConnectedCounter()
         // Arrange
         var circuitMetrics = new CircuitMetrics(_meterFactory);
         using var connectedCircuitCounter = new MetricCollector<long>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.connected_circuits");
+            CircuitMetrics.MeterName, "aspnetcore.components.circuit.connected");
 
         // Act
         circuitMetrics.OnConnectionDown();
@@ -95,20 +89,20 @@ public void OnConnectionDown_DecreasesConnectedCounter()
     }
 
     [Fact]
-    public void OnCircuitDown_UpdatesCountersAndRecordsDuration()
+    public async Task OnCircuitDown_UpdatesCountersAndRecordsDuration()
     {
         // Arrange
         var circuitMetrics = new CircuitMetrics(_meterFactory);
         using var activeCircuitCounter = new MetricCollector<long>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.active_circuits");
+            CircuitMetrics.MeterName, "aspnetcore.components.circuit.active");
         using var connectedCircuitCounter = new MetricCollector<long>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.connected_circuits");
+            CircuitMetrics.MeterName, "aspnetcore.components.circuit.connected");
         using var circuitDurationCollector = new MetricCollector<double>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.duration");
+            CircuitMetrics.MeterName, "aspnetcore.components.circuit.duration");
 
         // Act
         var startTime = Stopwatch.GetTimestamp();
-        Thread.Sleep(10); // Add a small delay to ensure a measurable duration
+        await Task.Delay(10); // Add a small delay to ensure a measurable duration
         var endTime = Stopwatch.GetTimestamp();
         circuitMetrics.OnCircuitDown(startTime, endTime);
 
@@ -135,7 +129,7 @@ public void IsDurationEnabled_ReturnsMeterEnabledState()
 
         // Create a collector to ensure the meter is enabled
         using var circuitDurationCollector = new MetricCollector<double>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.duration");
+            CircuitMetrics.MeterName, "aspnetcore.components.circuit.duration");
 
         // Act & Assert
         Assert.True(circuitMetrics.IsDurationEnabled());
@@ -146,14 +140,12 @@ public void FullCircuitLifecycle_RecordsAllMetricsCorrectly()
     {
         // Arrange
         var circuitMetrics = new CircuitMetrics(_meterFactory);
-        using var totalCounter = new MetricCollector<long>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.count");
         using var activeCircuitCounter = new MetricCollector<long>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.active_circuits");
+            CircuitMetrics.MeterName, "aspnetcore.components.circuit.active");
         using var connectedCircuitCounter = new MetricCollector<long>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.connected_circuits");
+            CircuitMetrics.MeterName, "aspnetcore.components.circuit.connected");
         using var circuitDurationCollector = new MetricCollector<double>(_meterFactory,
-            CircuitMetrics.MeterName, "aspnetcore.components.circuits.duration");
+            CircuitMetrics.MeterName, "aspnetcore.components.circuit.duration");
 
         // Act - Simulating a full circuit lifecycle
         var startTime = Stopwatch.GetTimestamp();
@@ -176,15 +168,10 @@ public void FullCircuitLifecycle_RecordsAllMetricsCorrectly()
         circuitMetrics.OnCircuitDown(startTime, endTime);
 
         // Assert
-        var totalMeasurements = totalCounter.GetMeasurementSnapshot();
         var activeMeasurements = activeCircuitCounter.GetMeasurementSnapshot();
         var connectedMeasurements = connectedCircuitCounter.GetMeasurementSnapshot();
         var durationMeasurements = circuitDurationCollector.GetMeasurementSnapshot();
 
-        // Total circuit count should have 1 measurement with value 1
-        Assert.Single(totalMeasurements);
-        Assert.Equal(1, totalMeasurements[0].Value);
-
         // Active circuit count should have 2 measurements (1 for open, -1 for close)
         Assert.Equal(2, activeMeasurements.Count);
         Assert.Equal(1, activeMeasurements[0].Value);
diff --git a/src/Components/Server/test/Circuits/TestCircuitHost.cs b/src/Components/Server/test/Circuits/TestCircuitHost.cs
index eeb86ad3a639..11e9f7ed4d65 100644
--- a/src/Components/Server/test/Circuits/TestCircuitHost.cs
+++ b/src/Components/Server/test/Circuits/TestCircuitHost.cs
@@ -1,6 +1,7 @@
 // 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.InternalTesting;
 using Microsoft.AspNetCore.SignalR;
@@ -15,8 +16,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits;
 
 internal class TestCircuitHost : CircuitHost
 {
-    private TestCircuitHost(CircuitId circuitId, AsyncServiceScope scope, CircuitOptions options, CircuitClientProxy client, RemoteRenderer renderer, IReadOnlyList<ComponentDescriptor> descriptors, RemoteJSRuntime jsRuntime, RemoteNavigationManager navigationManager, CircuitHandler[] circuitHandlers, CircuitMetrics circuitMetrics, ILogger logger)
-        : base(circuitId, scope, options, client, renderer, descriptors, jsRuntime, navigationManager, circuitHandlers, circuitMetrics, logger)
+    private TestCircuitHost(CircuitId circuitId, AsyncServiceScope scope, CircuitOptions options, CircuitClientProxy client, RemoteRenderer renderer, IReadOnlyList<ComponentDescriptor> descriptors, RemoteJSRuntime jsRuntime, RemoteNavigationManager navigationManager, CircuitHandler[] circuitHandlers, CircuitMetrics circuitMetrics, ComponentsActivitySource componentsActivitySource, ILogger logger)
+        : base(circuitId, scope, options, client, renderer, descriptors, jsRuntime, navigationManager, circuitHandlers, circuitMetrics, componentsActivitySource, logger)
     {
     }
 
@@ -38,6 +39,7 @@ public static CircuitHost Create(
             .Returns(jsRuntime);
         var serverComponentDeserializer = Mock.Of<IServerComponentDeserializer>();
         var circuitMetrics = new CircuitMetrics(new TestMeterFactory());
+        var componentsActivitySource = new ComponentsActivitySource();
 
         if (remoteRenderer == null)
         {
@@ -64,6 +66,7 @@ public static CircuitHost Create(
             navigationManager,
             handlers,
             circuitMetrics,
+            componentsActivitySource,
             NullLogger<CircuitHost>.Instance);
     }
 }
diff --git a/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs
index 104dcb930d66..a77b3e98c4ec 100644
--- a/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs
+++ b/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs
@@ -21,7 +21,6 @@ public partial class StaticHtmlRenderer : Renderer
 {
     private static readonly RendererInfo _componentPlatform = new RendererInfo("Static", isInteractive: false);
 
-    private static readonly Task CanceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true));
     private readonly NavigationManager? _navigationManager;
 
     /// <summary>
diff --git a/src/Shared/Metrics/MetricsConstants.cs b/src/Shared/Metrics/MetricsConstants.cs
index ff64c6fefcad..cebbaba0e6a9 100644
--- a/src/Shared/Metrics/MetricsConstants.cs
+++ b/src/Shared/Metrics/MetricsConstants.cs
@@ -11,6 +11,9 @@ internal static class MetricsConstants
     // Not based on a standard. Larger bucket sizes for longer lasting operations, e.g. HTTP connection duration. See https://github.com/open-telemetry/semantic-conventions/issues/336
     public static readonly IReadOnlyList<double> LongSecondsBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300];
 
-    // For Blazor/signalR sessions, which can last a long time.
-    public static readonly IReadOnlyList<double> VeryLongSecondsBucketBoundaries = [0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600, 1500, 60*60, 2 * 60 * 60, 4 * 60 * 60];
+    // For blazor rendering, which should be very fast.
+    public static readonly IReadOnlyList<double> BlazorRenderingSecondsBucketBoundaries = [0.000001, 0.00001, 0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10];
+
+    // For blazor circuit sessions, which can last a long time.
+    public static readonly IReadOnlyList<double> BlazorCircuitSecondsBucketBoundaries = [1, 3, 10, 30, 1 * 60, 3 * 60, 10 * 60, 30 * 60, 1 * 60 * 60, 3 * 60 * 60, 10 * 60 * 60, 24 * 60 * 60];
 }


Reviews:
user: login: samsp-msft
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-28T18:45:19Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T06:56:07Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T07:01:04Z
user: login: noahfalk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T20:58:25Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T21:13:54Z
user: login: noahfalk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T21:28:02Z
user: login: samsp-msft
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-29T21:53:13Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T10:16:39Z
user: login: maraf
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T12:05:25Z
user: login: samsp-msft
body: Some naming suggestions.

For sessions, I wonder if when log messages are fired, is the activity context going to be the session activity, and if not is there a way to force it to be? @noahfalk  this is an issue with essentially zero length spans - you might want to force log messages to be parented to it but something else is the activity at that point?
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-30T23:46:58Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T08:42:22Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T08:52:47Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T08:54:01Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T08:56:24Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T09:30:12Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T09:40:04Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T09:41:51Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T10:25:47Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T10:28:49Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T10:32:58Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T10:36:31Z
user: login: noahfalk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T19:01:09Z
user: login: noahfalk
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T19:17:31Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T10:30:08Z
user: login: javiercn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T11:01:25Z
user: login: javiercn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T11:02:43Z
user: login: javiercn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T11:08:06Z
user: login: javiercn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T11:12:50Z
user: login: javiercn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T12:02:32Z
user: login: javiercn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T12:03:34Z
user: login: javiercn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T12:05:06Z
user: login: javiercn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T12:12:39Z
user: login: javiercn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T12:16:20Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T12:37:22Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T12:41:42Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T14:07:01Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T14:09:00Z
user: login: JamesNK
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T14:25:50Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T17:03:10Z
user: login: JamesNK
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T23:52:36Z
user: login: javiercn
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T11:28:45Z
user: login: javiercn
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T08:49:44Z
user: login: pavelsavara
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T10:04:22Z

Review comments:
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: samsp-msft
body: As a neophyte Blazor developer, I don't quite understand why you'd want metrics broken down to the level of parameters. I am probably misunderstanding what this represents?
created_at: 2025-04-28T18:45:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: Blazor "parameters" are properties of a component that can receive values from its parent component, marked with the [Parameter] attribute. They enable data to flow down from parent to child components. When Blazor parameters change, the component goes through a re-rendering cycle. I think it they are well defined term.

The duration measured here is the act of parameter propagation and the user business logic that is triggered by it.

See also https://learn.microsoft.com/en-us/aspnet/core/blazor/components/cascading-values-and-parameters?view=aspnetcore-9.0
created_at: 2025-04-29T06:56:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: I think your feedback is that meaning of individual diagnostic instruments needs to be documented after we are done here. 

cc @guardrex
created_at: 2025-04-29T07:01:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: Ideally an experienced Blazor developer should be able to make a good guess at the meaning of the metric given just its name. I'm fine to assume those devs understand the meaning of 'parameters'. As described above it sounded like 'parameters' is a noun that doesn't inherently have a notion of time duration associated with it? Perhaps we could name this something like `aspnetcore.components.update_parameters.duration`? I'm not sure if there is a better term than update that Blazor uses.
created_at: 2025-04-29T20:58:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: According to OTel naming guidelines `count` only gets used for UpDownCounters (https://opentelemetry.io/docs/specs/semconv/general/naming/#use-count-instead-of-pluralization-for-updowncounters). I don't know the details on how Blazor routing compares to ASP.NET routing but the prior art from ASP.NET is https://opentelemetry.io/docs/specs/semconv/dotnet/dotnet-aspnetcore-metrics/#metric-aspnetcoreroutingmatch_attempts. Does the action being counted have a similar concept and naming to ASP.NET 'match attempts' or does Blazor do it differently?
created_at: 2025-04-29T21:06:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: Copy/paste error?
created_at: 2025-04-29T21:06:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: ```suggestion
            "aspnetcore.components.event.exceptions",
```
(https://opentelemetry.io/docs/specs/semconv/general/naming/#pluralization)
created_at: 2025-04-29T21:08:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: ```suggestion
            unit: "{exception}",
```
I think OTel had guidance that unit names should be singular. I can't find it now but that is the pattern all our other metric units follow.
created_at: 2025-04-29T21:09:02Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _meter.CreateCounter<long>(
+            "aspnetcore.components.parameters.exception",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: ```suggestion
            "aspnetcore.components.parameters.exceptions",
```
If the naming changes for duration it might flow here too. It feels a bit unclear what a 'parameter exception' is, but hopefully a 'parameter update exception' would be well understood.
created_at: 2025-04-29T21:09:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _meter.CreateCounter<long>(
+            "aspnetcore.components.parameters.exception",
+            unit: "{exceptions}",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: ```suggestion
            unit: "{exception}",
```
created_at: 2025-04-29T21:09:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: > `aspnetcore.components.update_parameters.duration`

Sounds good to me.

created_at: 2025-04-29T21:13:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _meter.CreateCounter<long>(
+            "aspnetcore.components.parameters.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _meter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _meter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exception",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: ```suggestion
            "aspnetcore.components.rendering.batch.exceptions",
```
created_at: 2025-04-29T21:13:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _meter.CreateCounter<long>(
+            "aspnetcore.components.parameters.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _meter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _meter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exception",
+            unit: "{exceptions}",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: ```suggestion
            unit: "{exception}",
```
created_at: 2025-04-29T21:14:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: samsp-msft
body: I think the granularity here is probably too fine. I don't think we need to be tracking this on a per component/parameter basis - there could potentially be hundreds of those on a page. I would suggest that we focus on what the end user will see which is that they take an action and that results in an update to the page. That will admittedly include a network round-trip, but understanding it from the server level is probably sufficient as it is what is in the developers control (unlike the network from the browser)
created_at: 2025-04-29T21:53:12Z
author_association: NONE
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: The user code that's running in the triggered events could make async HTTP call or database calls. If they do SELECT N+1 anti-pattern, it would be visible here. 

Those problems are currently not easy to diagnose, especially if the components are from different vendors or teams.

I think it's good to know which component was rendered when state changed. How many times and how long it took.

The action they could take based on this data, is to cache/redesign data acquisition in their components or reduce number of components or tree depth. Maybe also reduce percentage of cases that the sub-tree is re-rendered on data propagation.

Maybe we could have separate meter called `Microsoft.AspNetCore.Components.Lifecycle` which have this finer granularity and `Microsoft.AspNetCore.Components` could be for the big events.

@danroth27 thoughts ?
created_at: 2025-04-30T10:16:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,235 @@
+// 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";
+    private readonly Meter _meter;
+
+    private readonly Counter<long> _navigationCount;
+
+    private readonly Histogram<double> _eventDuration;
+    private readonly Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.Enabled;
+
+    public ComponentsMetrics(IMeterFactory meterFactory)
+    {
+        Debug.Assert(meterFactory != null);
+
+        _meter = meterFactory.Create(MeterName);
+
+        _navigationCount = _meter.CreateCounter<long>(
+            "aspnetcore.components.navigation.count",
+            unit: "{exceptions}",
+            description: "Total number of route changes.");
+
+        _eventDuration = _meter.CreateHistogram(
+            "aspnetcore.components.event.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exception",
+            unit: "{exceptions}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _meter.CreateHistogram(
+            "aspnetcore.components.parameters.duration",
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: maraf
body: > I think it's good to know which component was rendered when state changed. How many times and how long it took.

I agree
created_at: 2025-04-30T12:05:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.OnCircuit";
+    internal const string OnRouteName = $"{Name}.OnRoute";
+    internal const string OnEventName = $"{Name}.OnEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Server, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"CIRCUIT {circuitId ?? ""}";
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: samsp-msft
body: ```suggestion
            activity.DisplayName = $"Circuit {circuitId ?? ""}";
```
Its in all-caps for HTTP as its the method name, this is our own term.
created_at: 2025-04-30T22:56:19Z
author_association: NONE
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.OnCircuit";
+    internal const string OnRouteName = $"{Name}.OnRoute";
+    internal const string OnEventName = $"{Name}.OnEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Server, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"CIRCUIT {circuitId ?? ""}";
+            activity.Start();
+            _circuitContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public void FailCircuitActivity(Activity? activity, Exception ex)
+    {
+        _circuitContext = default;
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public Activity? StartRouteActivity(string componentType, string route)
+    {
+        if (_httpContext == default)
+        {
+            _httpContext = CaptureHttpContext();
+        }
+
+        var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, null, null);
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: samsp-msft
body: In talking with Dan Roth,  the ActivityKind should probably be Internal when running in WASM. Its neither really client nor server in that case.
created_at: 2025-04-30T22:59:13Z
author_association: NONE
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.OnCircuit";
+    internal const string OnRouteName = $"{Name}.OnRoute";
+    internal const string OnEventName = $"{Name}.OnEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Server, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"CIRCUIT {circuitId ?? ""}";
+            activity.Start();
+            _circuitContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public void FailCircuitActivity(Activity? activity, Exception ex)
+    {
+        _circuitContext = default;
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public Activity? StartRouteActivity(string componentType, string route)
+    {
+        if (_httpContext == default)
+        {
+            _httpContext = CaptureHttpContext();
+        }
+
+        var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("component.type", componentType);
+                }
+                if (route != null)
+                {
+                    activity.SetTag("route", route);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+            }
+
+            activity.DisplayName = $"ROUTE {route ?? "unknown"} -> {componentType ?? "unknown"}";
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: samsp-msft
body: ```suggestion
            activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}";
```
created_at: 2025-04-30T23:00:50Z
author_association: NONE
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.OnCircuit";
+    internal const string OnRouteName = $"{Name}.OnRoute";
+    internal const string OnEventName = $"{Name}.OnEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Server, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"CIRCUIT {circuitId ?? ""}";
+            activity.Start();
+            _circuitContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public void FailCircuitActivity(Activity? activity, Exception ex)
+    {
+        _circuitContext = default;
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public Activity? StartRouteActivity(string componentType, string route)
+    {
+        if (_httpContext == default)
+        {
+            _httpContext = CaptureHttpContext();
+        }
+
+        var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("component.type", componentType);
+                }
+                if (route != null)
+                {
+                    activity.SetTag("route", route);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+            }
+
+            activity.DisplayName = $"ROUTE {route ?? "unknown"} -> {componentType ?? "unknown"}";
+            activity.Start();
+            _routeContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName)
+    {
+        var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("component.type", componentType);
+                }
+                if (methodName != null)
+                {
+                    activity.SetTag("component.method", methodName);
+                }
+                if (attributeName != null)
+                {
+                    activity.SetTag("attribute.name", attributeName);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+                if (_routeContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_routeContext));
+                }
+            }
+
+            activity.DisplayName = $"EVENT {attributeName ?? "unknown"} -> {componentType ?? "unknown"}.{methodName ?? "unknown"}";
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: samsp-msft
body: ```suggestion
            activity.DisplayName = $"Event {attributeName ?? "[unknown]"} -> {componentType ?? "[unknown]"}.{methodName ?? "[unknown]"}";
```
created_at: 2025-04-30T23:02:09Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: samsp-msft
body: `ShortSecondsBucketBoundaries` is designed for network level latencies such as an http request. I suspect that components may need buckets more focused on the sub ms level timings? 
created_at: 2025-04-30T23:05:59Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: samsp-msft
body: same as above for the durations
created_at: 2025-04-30T23:06:30Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: samsp-msft
body: should `route` be `http.route` to match the http metrics / semconv? https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/built-in?view=aspnetcore-9.0#metric-httpserverrequestduration
created_at: 2025-04-30T23:09:06Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: samsp-msft
body: if we don't know the route, is it better to skip the tag all together? I can see it going both ways - it makes it more explicit this way.
created_at: 2025-04-30T23:11:05Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
+        };
+
+        _navigationCount.Add(1, tags);
+    }
+
+    public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
+                { "component.method", methodName ?? "unknown" },
+                { "attribute.name", attributeName ?? "unknown" }
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: samsp-msft
body: I understand why this is based on the attribute, but its really also the event name. would that make more sense as "event"?
created_at: 2025-04-30T23:17:02Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
+        };
+
+        _navigationCount.Add(1, tags);
+    }
+
+    public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: samsp-msft
body: `code.class.name`? to match https://github.com/open-telemetry/semantic-conventions/blob/main/docs/attributes-registry/code.md
created_at: 2025-04-30T23:19:14Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
+        };
+
+        _navigationCount.Add(1, tags);
+    }
+
+    public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
+                { "component.method", methodName ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: samsp-msft
body: `code.function.name` is the semconv for this - it may mean we don't need a separate one for the component?
created_at: 2025-04-30T23:20:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
+        };
+
+        _navigationCount.Add(1, tags);
+    }
+
+    public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
+                { "component.method", methodName ?? "unknown" },
+                { "attribute.name", attributeName ?? "unknown" }
+            };
+
+            var duration = Stopwatch.GetElapsedTime(startTimestamp);
+            _eventDuration.Record(duration.TotalSeconds, tags);
+        }
+        catch
+        {
+            // none
+        }
+    }
+
+    public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, string? componentType)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
+            };
+
+            var duration = Stopwatch.GetElapsedTime(startTimestamp);
+            _parametersDuration.Record(duration.TotalSeconds, tags);
+        }
+        catch
+        {
+            // none
+        }
+    }
+
+    public void BatchDuration(long startTimestamp, int diffLength)
+    {
+        var tags = new TagList
+        {
+            { "diff.length.bucket", BucketEditLength(diffLength) }
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: samsp-msft
body: `diff.approx_length`? I was a bit confused by the bucket in the name. Totally the right thing to do to bucketize the sizes as that eliminates dimension explosion, but the name confused me as to whether the feature has buckets or something)

@lmolkova - do you know of a precedent for where this is done elsewhere?
created_at: 2025-04-30T23:31:33Z
author_association: NONE
diff_hunk: @@ -11,6 +11,6 @@ internal static class MetricsConstants
     // Not based on a standard. Larger bucket sizes for longer lasting operations, e.g. HTTP connection duration. See https://github.com/open-telemetry/semantic-conventions/issues/336
     public static readonly IReadOnlyList<double> LongSecondsBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300];
 
-    // For Blazor/signalR sessions, which can last a long time.
-    public static readonly IReadOnlyList<double> VeryLongSecondsBucketBoundaries = [0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600, 1500, 60*60, 2 * 60 * 60, 4 * 60 * 60];
+    // For blazor circuit sessions, which can last a long time.
+    public static readonly IReadOnlyList<double> VeryLongSecondsBucketBoundaries = [1, 10, 30, 1 * 60, 2 * 60, 3 * 60, 4 * 60, 5 * 60, 6 * 60, 7 * 60, 8 * 60, 9 * 60, 10 * 60, 1 * 60 * 60, 2 * 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60];
path: src/Shared/Metrics/MetricsConstants.cs
user: login: samsp-msft
body: These should ideally be exponential eg
```
1, 3, 10, 30,
1*60, 3*60, 10*60, 30*60,
1*60*60, 3*60*60, 10*60*60, 24*60*60  
```
This uses an approx multiplier of 3, while meeting user sensible boundaries

created_at: 2025-04-30T23:43:43Z
author_association: NONE
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: This is route in terms of browser address bar URL segment. But it is not always HTTP on the wire. In WASM case, there is not network request at all.
I don't have strong opinion either way.

@noahfalk thoughts ?
created_at: 2025-05-02T08:42:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: I'm thinking about running a query on this. Leaving the tag out could mean you need "LEFT JOIN" to not lose the whole activity row. I don't know if this is realistic situation.
created_at: 2025-05-02T08:52:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
+        };
+
+        _navigationCount.Add(1, tags);
+    }
+
+    public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: We don't really have code reference, as in file name and line number. 
created_at: 2025-05-02T08:54:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
+        };
+
+        _navigationCount.Add(1, tags);
+    }
+
+    public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
+                { "component.method", methodName ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: having separate tag for component type could make it easier to filter when running queries on it. "contains" operation makes bad use of DB index on join.
created_at: 2025-05-02T08:56:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
+        };
+
+        _navigationCount.Add(1, tags);
+    }
+
+    public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
+                { "component.method", methodName ?? "unknown" },
+                { "attribute.name", attributeName ?? "unknown" }
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: This refers to `onclick` in the example below. Attribute makes it easier for me personally but I don't have strong preference. It's `attributeName` thru Blazor internals.

@danroth27 do you prefer "event" or something else ?

```
<button class="btn btn-primary" @onclick="Buy" @focus="OnFocus">Buy</button>
```
created_at: 2025-05-02T09:30:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: the 3rd option is tag is present but value is null.  `{ "route", null },` just for completeness
created_at: 2025-05-02T09:40:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
+        };
+
+        _navigationCount.Add(1, tags);
+    }
+
+    public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
+                { "component.method", methodName ?? "unknown" },
+                { "attribute.name", attributeName ?? "unknown" }
+            };
+
+            var duration = Stopwatch.GetElapsedTime(startTimestamp);
+            _eventDuration.Record(duration.TotalSeconds, tags);
+        }
+        catch
+        {
+            // none
+        }
+    }
+
+    public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, string? componentType)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
+            };
+
+            var duration = Stopwatch.GetElapsedTime(startTimestamp);
+            _parametersDuration.Record(duration.TotalSeconds, tags);
+        }
+        catch
+        {
+            // none
+        }
+    }
+
+    public void BatchDuration(long startTimestamp, int diffLength)
+    {
+        var tags = new TagList
+        {
+            { "diff.length.bucket", BucketEditLength(diffLength) }
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: made it `diff.approximate.length`, happy to change it further 
created_at: 2025-05-02T09:41:51Z
author_association: MEMBER
diff_hunk: @@ -11,6 +11,6 @@ internal static class MetricsConstants
     // Not based on a standard. Larger bucket sizes for longer lasting operations, e.g. HTTP connection duration. See https://github.com/open-telemetry/semantic-conventions/issues/336
     public static readonly IReadOnlyList<double> LongSecondsBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300];
 
-    // For Blazor/signalR sessions, which can last a long time.
-    public static readonly IReadOnlyList<double> VeryLongSecondsBucketBoundaries = [0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600, 1500, 60*60, 2 * 60 * 60, 4 * 60 * 60];
+    // For blazor circuit sessions, which can last a long time.
+    public static readonly IReadOnlyList<double> VeryLongSecondsBucketBoundaries = [1, 10, 30, 1 * 60, 2 * 60, 3 * 60, 4 * 60, 5 * 60, 6 * 60, 7 * 60, 8 * 60, 9 * 60, 10 * 60, 1 * 60 * 60, 2 * 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60];
path: src/Shared/Metrics/MetricsConstants.cs
user: login: pavelsavara
body: done
created_at: 2025-05-02T10:25:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: Added `BlazorBatchDiffSecondsBucketBoundaries = [0.000001, 0.00001, 0.0001, 0.001, 0.01, 0.1, 1]`
created_at: 2025-05-02T10:28:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: pavelsavara
body: This could involve network requests or not. 
Added `BlazorParametersUpdateSecondsBucketBoundaries = [0.000001, 0.00001, 0.0001, 0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]`
created_at: 2025-05-02T10:32:58Z
author_association: MEMBER
diff_hunk: @@ -11,6 +11,6 @@ internal static class MetricsConstants
     // Not based on a standard. Larger bucket sizes for longer lasting operations, e.g. HTTP connection duration. See https://github.com/open-telemetry/semantic-conventions/issues/336
     public static readonly IReadOnlyList<double> LongSecondsBucketBoundaries = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, 30, 60, 120, 300];
 
-    // For Blazor/signalR sessions, which can last a long time.
-    public static readonly IReadOnlyList<double> VeryLongSecondsBucketBoundaries = [0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600, 1500, 60*60, 2 * 60 * 60, 4 * 60 * 60];
+    // For blazor circuit sessions, which can last a long time.
+    public static readonly IReadOnlyList<double> VeryLongSecondsBucketBoundaries = [1, 10, 30, 1 * 60, 2 * 60, 3 * 60, 4 * 60, 5 * 60, 6 * 60, 7 * 60, 8 * 60, 9 * 60, 10 * 60, 1 * 60 * 60, 2 * 60 * 60, 3 * 60 * 60, 6 * 60 * 60, 12 * 60 * 60, 24 * 60 * 60];
path: src/Shared/Metrics/MetricsConstants.cs
user: login: pavelsavara
body: Also renamed to be Blazor-specific to `BlazorCircuitSecondsBucketBoundaries`
created_at: 2025-05-02T10:36:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: I don't have a good sense of it offhand - this is the kind of thing where I expect we'd get better feedback by creating the OTel semantic convention PR and getting feedback from that community.
created_at: 2025-05-02T19:01:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,239 @@
+// 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 Counter<long> _eventException;
+
+    private readonly Histogram<double> _parametersDuration;
+    private readonly Counter<long> _parametersException;
+
+    private readonly Histogram<double> _batchDuration;
+    private readonly Counter<long> _batchException;
+
+    public bool IsNavigationEnabled => _navigationCount.Enabled;
+
+    public bool IsEventDurationEnabled => _eventDuration.Enabled;
+    public bool IsEventExceptionEnabled => _eventException.Enabled;
+
+    public bool IsParametersDurationEnabled => _parametersDuration.Enabled;
+    public bool IsParametersExceptionEnabled => _parametersException.Enabled;
+
+    public bool IsBatchDurationEnabled => _batchDuration.Enabled;
+    public bool IsBatchExceptionEnabled => _batchException.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.duration",
+            unit: "s",
+            description: "Duration of processing browser event.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _eventException = _meter.CreateCounter<long>(
+            "aspnetcore.components.event.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during browser event processing.");
+
+        _parametersDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.update_parameters.duration",
+            unit: "s",
+            description: "Duration of processing component parameters.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _parametersException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.update_parameters.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during processing component parameters.");
+
+        _batchDuration = _lifeCycleMeter.CreateHistogram(
+            "aspnetcore.components.rendering.batch.duration",
+            unit: "s",
+            description: "Duration of rendering batch.",
+            advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+        _batchException = _lifeCycleMeter.CreateCounter<long>(
+            "aspnetcore.components.rendering.batch.exceptions",
+            unit: "{exception}",
+            description: "Total number of exceptions during batch rendering.");
+    }
+
+    public void Navigation(string componentType, string route)
+    {
+        var tags = new TagList
+        {
+            { "component.type", componentType ?? "unknown" },
+            { "route", route ?? "unknown" },
+        };
+
+        _navigationCount.Add(1, tags);
+    }
+
+    public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName)
+    {
+        try
+        {
+            await task;
+
+            var tags = new TagList
+            {
+                { "component.type", componentType ?? "unknown" },
path: src/Components/Components/src/ComponentsMetrics.cs
user: login: noahfalk
body: My initial thought is that the thing being refered to was the Blazor component abstraction in which case component.type and component.method made sense to me. The fact that code.class.name isn't currently defined also nudges me in that direction. If we did want to use code.* attributes instead this feels like another spot where OTel semconv feedback could be helpful.
created_at: 2025-05-02T19:17:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.OnCircuit";
+    internal const string OnRouteName = $"{Name}.OnRoute";
+    internal const string OnEventName = $"{Name}.OnEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Server, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"CIRCUIT {circuitId ?? ""}";
+            activity.Start();
+            _circuitContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public void FailCircuitActivity(Activity? activity, Exception ex)
+    {
+        _circuitContext = default;
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public Activity? StartRouteActivity(string componentType, string route)
+    {
+        if (_httpContext == default)
+        {
+            _httpContext = CaptureHttpContext();
+        }
+
+        var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, null, null);
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: pavelsavara
body: done
created_at: 2025-05-06T10:30:08Z
author_association: MEMBER
diff_hunk: @@ -369,7 +369,7 @@ private async Task CaptureAsyncExceptions(Task task)
         }
     }
 
-    private static partial class Log
+    private static new partial class Log
path: src/Components/Server/src/Circuits/RemoteRenderer.cs
user: login: javiercn
body: Is this `new` really needed here? 
created_at: 2025-05-15T11:01:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,57 @@
+// 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.Metrics;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Microsoft.AspNetCore.Components.Infrastructure;
+
+/// <summary>
+/// Infrastructure APIs for registering diagnostic metrics.
+/// </summary>
+public static class ComponentsMetricsServiceCollectionExtensions
+{
+    /// <summary>
+    /// Registers component rendering metrics
+    /// </summary>
+    /// <param name="services">The <see cref="IServiceCollection"/>.</param>
+    /// <returns>The <see cref="IServiceCollection"/>.</returns>
+    public static IServiceCollection AddComponentsMetrics(
+        IServiceCollection services)
+    {
+        // do not register IConfigureOptions<StartupValidatorOptions> multiple times
+        if (!IsMeterFactoryRegistered(services))
+        {
+            services.AddMetrics();
+        }
path: src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs
user: login: javiercn
body: Is this check to avoid calling `services.AddMetrics()` more than once? Is there a failure in that situation?
created_at: 2025-05-15T11:02:43Z
author_association: MEMBER
diff_hunk: @@ -105,7 +108,7 @@ public CircuitHost(
 
     // InitializeAsync is used in a fire-and-forget context, so it's responsible for its own
     // error handling.
-    public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, CancellationToken cancellationToken)
+    public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, ActivityContext httpContext, CancellationToken cancellationToken)
path: src/Components/Server/src/Circuits/CircuitHost.cs
user: login: javiercn
body: Can we name this activityContext? `httpContext` has a very well-defined meaning across the codebase and it set the expectation that it's an `HttpContext` instance, which this is not.
created_at: 2025-05-15T11:08:05Z
author_association: MEMBER
diff_hunk: @@ -51,6 +52,7 @@ public CircuitHost(
         RemoteNavigationManager navigationManager,
         CircuitHandler[] circuitHandlers,
         CircuitMetrics? circuitMetrics,
+        ComponentsActivitySource? componentsActivitySource,
path: src/Components/Server/src/Circuits/CircuitHost.cs
user: login: javiercn
body: The activity source and the metrics shouldn't be nullable, they should be the same as RemoteJsRuntime and other parameters.

We unconditionally register them https://github.com/dotnet/aspnetcore/pull/61609/files#diff-e82512017622b574554e1a1519f2bb3bbb1d8957e5346b3a4cbeb0f10e368c79R64 and

https://github.com/dotnet/aspnetcore/pull/61609/files#diff-9e461d4480403d04ed6d5e6c31c44261fe8d5926969ab338dd910564ea211396R79-R80

We should throw an exception if they aren't being provided similarly to how we do for other services.
created_at: 2025-05-15T11:12:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: javiercn
body: Layering wise this shouldn't go here.

`Microsoft.AspNetCore.Components` shoudn't have a dependency on `Microsoft.AspNetCore.Hosting`
created_at: 2025-05-15T12:02:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"Circuit {circuitId ?? ""}";
+            activity.Start();
+            _circuitContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public void FailCircuitActivity(Activity? activity, Exception ex)
+    {
+        _circuitContext = default;
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public Activity? StartRouteActivity(string componentType, string route)
+    {
+        if (_httpContext == default)
+        {
+            _httpContext = CaptureHttpContext();
+        }
+
+        var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("aspnetcore.components.type", componentType);
+                }
+                if (route != null)
+                {
+                    activity.SetTag("aspnetcore.components.route", route);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+            }
+
+            activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}";
+            activity.Start();
+            _routeContext = activity.Context;
+        }
+        return activity;
+    }
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: javiercn
body: This should go on the Server assembly.
created_at: 2025-05-15T12:03:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"Circuit {circuitId ?? ""}";
+            activity.Start();
+            _circuitContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public void FailCircuitActivity(Activity? activity, Exception ex)
+    {
+        _circuitContext = default;
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public Activity? StartRouteActivity(string componentType, string route)
+    {
+        if (_httpContext == default)
+        {
+            _httpContext = CaptureHttpContext();
+        }
+
+        var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("aspnetcore.components.type", componentType);
+                }
+                if (route != null)
+                {
+                    activity.SetTag("aspnetcore.components.route", route);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+            }
+
+            activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}";
+            activity.Start();
+            _routeContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName)
+    {
+        var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("aspnetcore.components.type", componentType);
+                }
+                if (methodName != null)
+                {
+                    activity.SetTag("aspnetcore.components.method", methodName);
+                }
+                if (attributeName != null)
+                {
+                    activity.SetTag("aspnetcore.components.attribute.name", attributeName);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+                if (_routeContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_routeContext));
+                }
+            }
+
+            activity.DisplayName = $"Event {attributeName ?? "[unknown attribute]"} -> {componentType ?? "[unknown component]"}.{methodName ?? "[unknown method]"}";
+            activity.Start();
+        }
+        return activity;
+    }
+
+    public static void FailEventActivity(Activity? activity, Exception ex)
+    {
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public static async Task CaptureEventStopAsync(Task task, Activity? activity)
+    {
+        try
+        {
+            await task;
+            activity?.Stop();
+        }
+        catch (Exception ex)
+        {
+            FailEventActivity(activity, ex);
+        }
+    }
+}
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: javiercn
body: This probably needs to go into a different class `ComponentEventActivitySource` (or any other name, RendererEventMetrics or something like that)
created_at: 2025-05-15T12:05:06Z
author_association: MEMBER
diff_hunk: @@ -770,6 +813,7 @@ private void ProcessRenderQueue()
 
         _isBatchInProgress = true;
         var updateDisplayTask = Task.CompletedTask;
+        var batchStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsBatchEnabled ? Stopwatch.GetTimestamp() : 0;
path: src/Components/Components/src/RenderTree/Renderer.cs
user: login: javiercn
body: Don't remember the exact syntax, but you can write it like below

```suggestion
        var batchStartTimestamp = ComponentMetrics  is { IsBatchEnabled: true } ? Stopwatch.GetTimestamp() : 0;
```
created_at: 2025-05-15T12:12:39Z
author_association: MEMBER
diff_hunk: @@ -51,6 +52,11 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
             _hasCascadingParameters = true;
             _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions();
         }
+
+        if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersEnabled)
+        {
+            _componentTypeName = component.GetType().FullName;
path: src/Components/Components/src/Rendering/ComponentState.cs
user: login: javiercn
body: Is this perf related or "convenience" related.

Adding a field to ComponentState is not great, since this increments the cost of each component that gets created (which your app can have thousands of).
created_at: 2025-05-15T12:16:20Z
author_association: MEMBER
diff_hunk: @@ -369,7 +369,7 @@ private async Task CaptureAsyncExceptions(Task task)
         }
     }
 
-    private static partial class Log
+    private static new partial class Log
path: src/Components/Server/src/Circuits/RemoteRenderer.cs
user: login: pavelsavara
body: It's a warning now after I added the project reference.
It is hiding the parent logger. 

A) If we want to use the parent logger, I can delete this one.
B) or I can #pragma the warning
created_at: 2025-05-15T12:37:22Z
author_association: MEMBER
diff_hunk: @@ -51,6 +52,11 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
             _hasCascadingParameters = true;
             _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions();
         }
+
+        if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersEnabled)
+        {
+            _componentTypeName = component.GetType().FullName;
path: src/Components/Components/src/Rendering/ComponentState.cs
user: login: pavelsavara
body: This is for perf. Because we need that type _componentTypeName on all parameter updates on each component instance. (when this metric is enabled)
created_at: 2025-05-15T12:41:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: pavelsavara
body: Please be more specific, what do you want to see instead ?
created_at: 2025-05-15T14:07:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"Circuit {circuitId ?? ""}";
+            activity.Start();
+            _circuitContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public void FailCircuitActivity(Activity? activity, Exception ex)
+    {
+        _circuitContext = default;
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public Activity? StartRouteActivity(string componentType, string route)
+    {
+        if (_httpContext == default)
+        {
+            _httpContext = CaptureHttpContext();
+        }
+
+        var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("aspnetcore.components.type", componentType);
+                }
+                if (route != null)
+                {
+                    activity.SetTag("aspnetcore.components.route", route);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+            }
+
+            activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}";
+            activity.Start();
+            _routeContext = activity.Context;
+        }
+        return activity;
+    }
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: pavelsavara
body: If we move it to different class/instance, we would not be able to use `_circuitId` and `_circuitContext`. And we would have to create lot of semi-public interfaces to pass that info between those classes. Are you sure it's worth it ?
created_at: 2025-05-15T14:09:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: JamesNK
body: Are you trying to get the activity of the current request here? There is a feature in the features collection for doing that: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.features.ihttpactivityfeature.activity?view=aspnetcore-9.0
created_at: 2025-05-15T14:25:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: pavelsavara
body: Can I get that from DI without passing HttpContext thru all SignalR layers ? Or from some TLS ?
created_at: 2025-05-15T17:03:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: JamesNK
body: It comes from HttpContext. It's in the features collection.
created_at: 2025-05-15T23:52:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"Circuit {circuitId ?? ""}";
+            activity.Start();
+            _circuitContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public void FailCircuitActivity(Activity? activity, Exception ex)
+    {
+        _circuitContext = default;
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public Activity? StartRouteActivity(string componentType, string route)
+    {
+        if (_httpContext == default)
+        {
+            _httpContext = CaptureHttpContext();
+        }
+
+        var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("aspnetcore.components.type", componentType);
+                }
+                if (route != null)
+                {
+                    activity.SetTag("aspnetcore.components.route", route);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+            }
+
+            activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}";
+            activity.Start();
+            _routeContext = activity.Context;
+        }
+        return activity;
+    }
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: javiercn
body: Isn't the CircuitId on a parent activity? (like start circuit or something like that)

All the `_httpContext`, `_circuitContext` on that event is at the wrong level of abstraction. I don't know if it can be set on the parent activity and flowed into this one, but the route activity should not know about _circuitContext or _httpContext.

To put it in a different way, this method/class should not be responsible for resolving the parent activity to link to. That should be provided somehow. We might need public API for that.

We don't want to keep adding to this list when/if we add new render modes. For example, I imagine WebView migth also want to track its own parent activity and it would require us to add it here.
created_at: 2025-05-16T11:28:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Components;
+
+/// <summary>
+/// This is instance scoped per renderer
+/// </summary>
+internal class ComponentsActivitySource
+{
+    internal const string Name = "Microsoft.AspNetCore.Components";
+    internal const string OnCircuitName = $"{Name}.CircuitStart";
+    internal const string OnRouteName = $"{Name}.RouteChange";
+    internal const string OnEventName = $"{Name}.HandleEvent";
+
+    private ActivityContext _httpContext;
+    private ActivityContext _circuitContext;
+    private string? _circuitId;
+    private ActivityContext _routeContext;
+
+    private ActivitySource ActivitySource { get; } = new ActivitySource(Name);
+
+    public static ActivityContext CaptureHttpContext()
+    {
+        var parentActivity = Activity.Current;
+        if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
+        {
+            return parentActivity.Context;
+        }
+        return default;
+    }
+
+    public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
+    {
+        _circuitId = circuitId;
+
+        var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(httpContext));
+                }
+            }
+            activity.DisplayName = $"Circuit {circuitId ?? ""}";
+            activity.Start();
+            _circuitContext = activity.Context;
+        }
+        return activity;
+    }
+
+    public void FailCircuitActivity(Activity? activity, Exception ex)
+    {
+        _circuitContext = default;
+        if (activity != null && !activity.IsStopped)
+        {
+            activity.SetTag("error.type", ex.GetType().FullName);
+            activity.SetStatus(ActivityStatusCode.Error);
+            activity.Stop();
+        }
+    }
+
+    public Activity? StartRouteActivity(string componentType, string route)
+    {
+        if (_httpContext == default)
+        {
+            _httpContext = CaptureHttpContext();
+        }
+
+        var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null);
+        if (activity is not null)
+        {
+            if (activity.IsAllDataRequested)
+            {
+                if (_circuitId != null)
+                {
+                    activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
+                }
+                if (componentType != null)
+                {
+                    activity.SetTag("aspnetcore.components.type", componentType);
+                }
+                if (route != null)
+                {
+                    activity.SetTag("aspnetcore.components.route", route);
+                }
+                if (_httpContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_httpContext));
+                }
+                if (_circuitContext != default)
+                {
+                    activity.AddLink(new ActivityLink(_circuitContext));
+                }
+            }
+
+            activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}";
+            activity.Start();
+            _routeContext = activity.Context;
+        }
+        return activity;
+    }
path: src/Components/Components/src/ComponentsActivitySource.cs
user: login: pavelsavara
body: Let's address this in next PR.

I agree that we should not break existing expectations about layers. On the other hand, I would like to keep this as simple as possible. This is internal class and I don't prefer lofty layered architectures for simple and perf sensitive classes like this one.
created_at: 2025-05-19T10:04:22Z
author_association: MEMBER

PR comments:
user: login: JamesNK
created_at: 2025-04-24T23:30:15Z
body: You're adding a lot of metrics here. I think you should do some performance testing. There is performance overhead of metrics - they require some synronization when incrementing counters and recording values.

Having many low level metrics could cause performance issues.
author_association: MEMBER
user: login: pavelsavara
created_at: 2025-04-25T18:46:19Z
body: > Having many low level metrics could cause performance issues.

I removed few and kept only the most useful ones. 
The only `aspnetcore.components.parameters.duration` is per Blazor component. 
It's async and  executes customer's business logic. 
If they have thousands of them they are in trouble anyway. And this will help them to figure it out.
The rest of them are per request, which should be OK.

I have 2 remaining issues
- on metrics/duration histogram I'm unable to see any time bigger than `5` in dashboard. I assume it is the 0.005s bucket. Even if I have added 1s delay and validated stopwatch said 1.1s TotalSeconds. I think there is some problem with aggregation or display ? Or maybe all the other fast examples are drowning this one ?
- ~~on tracing/activities I would like to link HTTP activity from the place where SignalR is creating the Blazor circuit. So I capture it's `Activity.Current.Context` and use it later to `AddLink()` on my activity. In some cases it leads to the HTTP activity, but in may cases the HTTP activity is not in the dashboard at all. I'm thinking it could be sampling. I would like to skip the link if I know that the HTTP activity was not selected for sampling. But HTTP activity always (on dev machine without pressure) have `.Recorded` true and sometimes is missing on the dashboard anyway.~~

I would appreciate hints, many thanks! @noahfalk @JamesNK
author_association: MEMBER
user: login: BrennanConroy
created_at: 2025-04-25T19:24:16Z
body: > * I would like to link HTTP activity from the place where SignalR is creating the Blazor circuit. So I capture it's `Activity.Current.Context` and use it later to `AddLink()` on my activity.

I don't know how Blazor circuits are created, but if it's from a Hub method then Activity.Current won't be the HTTP activity. We hop off the HTTP activity on purpose in SignalR: https://github.com/dotnet/aspnetcore/blob/9f2b0880ff3c09db4a8915d62788a7d1ba62df64/src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs#L398-L403

> * in may cases the HTTP activity is not in the dashboard at all

Is that because the HTTP request is still running? I don't think activites show up in the dashboard until they're stopped, and if you're using SignalR you're likely using a websocket request which is long running.
author_association: MEMBER
user: login: pavelsavara
created_at: 2025-04-28T07:30:30Z
body: > I don't know how Blazor circuits are created, but if it's from a Hub method then Activity.Current won't be the HTTP activity. We hop off the HTTP activity on purpose in SignalR:

I'm capturing `Activity.Current.Context` in `ComponentHub` constructor, which happens before any `Activity` changes in the SignalR layer, and so I'm able to capture HTTP Activity. Capture works, I test the `Name` and I get the `TraceId` just fine.

> Is that because the HTTP request is still running? I don't think activites show up in the dashboard until they're stopped, and if you're using SignalR you're likely using a websocket request which is long running.

This is it, thank you @BrennanConroy !

author_association: MEMBER
user: login: pavelsavara
created_at: 2025-04-28T07:38:45Z
body: It's also topic to discuss for long running activities on Blazor. 
- current circuit - this is object in memory, with SignalR connection. Could be hours or even days long.
- current route - this is more logical than physical. But it makes sense to link click event activities to the current route

We have 2 way how to deal with them I think
- keep them open for the whole long duration
    - should we install them back to `Activity.Current` on next HTTP/SignalR request for child Activity sake ?
    - Should we use parent relationship instead of link ? I understood the feedback in [29846](https://github.com/dotnet/aspnetcore/issues/29846) to mean that long running parent makes bad UX in the dashboard.
- close them soon - at the end of the current event/click/navigation
    - this makes them immediately visible in dashboard/OTEL and linkable
    - but we don't capture the true duration other sub-spans: disconnect, reconnect, close

Right now I have short+links implementation. 

I guess developers use OTEL mostly in production and so even the long running traces would be recorded already.

But maybe developers also use it in inner dev loop ? In which case it would be great to have "trace preview" for thing that started but not stopped yet. To not get confused the same way as I did. 
author_association: MEMBER
user: login: samsp-msft
created_at: 2025-04-28T18:41:46Z
body: Adding a general naming one here - `Microsoft.AspNetCore.Components` I would have no idea that this was for Blazor. I can understand why you may not want to directly put Blazor in the name, but maybe `Microsoft.AspNetCore.UI` or `Microsoft.AspNetCore.UIComponents` would be more obvious?
author_association: NONE
user: login: pavelsavara
created_at: 2025-04-29T06:59:08Z
body: > Adding a general naming one here - `Microsoft.AspNetCore.Components` I would have no idea that this was for Blazor.

`Microsoft.AspNetCore.Components` is a C# namespace in which whole Blazor lives since forever.
author_association: MEMBER
user: login: noahfalk
created_at: 2025-04-29T22:00:41Z
body: > Microsoft.AspNetCore.Components.OnCircuit: CIRCUIT {circuitId}
tags: circuit.id
links: HTTP activity
Microsoft.AspNetCore.Components.OnRoute: ROUTE {route} -> {componentType}
tags: circuit.id, component.type, route
links: HTTP trace, circuit trace
Microsoft.AspNetCore.Components.OnEvent: EVENT {attributeName} -> {componentType}.{methodName}
tags: circuit.id, component.type, component.method, attribute.name
links: HTTP trace, circuit trace, router trace

I'm a bit confused by the distributed tracing part of this. Partly that might be my limited experience with Blazor. I'm not sure what period of time is being measured by the different OnCircuit, OnRoute, OnEvent spans. For example I know what a Blazor circuit is but I don't know what 'OnCircuit' is measuring. Is this a span that measures the entire duration of 1 blazor circuit?

I'll probably have more questions once I understand what each of the spans represents.
author_association: MEMBER
user: login: danroth27
created_at: 2025-04-29T22:25:19Z
body: > aspnetcore.components.rendering.batch.duration - Duration of rendering batch.

I don't think we currently define in our public Blazor docs what a render batch is. The only public mention of render batches that I could find is the [CircuitOptions.MaxBufferedUnacknowledgedRenderBatches](https://learn.microsoft.com//dotnet/api/microsoft.aspnetcore.components.server.circuitoptions.maxbufferedunacknowledgedrenderbatches) property. So, it's not clear to me what this value represents or whether it is useful. Should we be measuring something else that is more directly correlated the publicly documented [component lifecycle](https://learn.microsoft.com/aspnet/core/blazor/components/lifecycle)? Or do we want to introduce the concept of a render batch in our docs?

> aspnetcore.components.rendering.batch.exception - Total number of exceptions during batch rendering.

Again, since render batch isn't currently a publicly defined concept, should we be counting the number of exceptions per some other period?

> aspnetcore.components.event.duration - Duration of processing browser event asynchronously.

I assume this is an average duration of all browser event handlers across the entire app regardless of render mode. That seems reasonable as a high-level view of the responsiveness of the app. But what does the "asynchronously" imply? Are synchronous event handlers not included in this metric?

> aspnetcore.components.parameters.duration - Duration of processing component parameters asynchronously.

I'm not sure what's included in the "processing" of component parameters. Is this the duration of the `OnParametersSet` component lifecycle event?

> aspnetcore.components.navigation.count - Total number of route changes.

Is this a total count of all page navigations across the entire app regardless of render mode? What would that be used for?
author_association: MEMBER
user: login: noahfalk
created_at: 2025-04-29T23:48:54Z
body: > What would that be used for?

I was under the (perhaps mistaken?) impression that the scenarios using the metrics had already been looked at and appropriate metrics identified. If not, perhaps a good starting point is to identify what diagnostic questions we'd like users to be able to solve here. Usually I'd recommend:

- P0 - Is my service healthy? (Most responses are successful/correct, latency is reasonable)
- P1 - What is the load on my service? (How many requests/sec?)
- P1 - How many resources is my service using? (Often CPU, memory, disk are already measured elsewhere, but maybe a service would have some specialized resource it consumes that needs to be measured?)
- P2 - Why is my service unhealthy? Metrics can be used to narrow down or even root cause problems but beware the slippery slope of adding too many metrics that are only useful in ever more niche cases. Its not unreasonable to require logs or distributed tracing for problem diagnosis.
author_association: MEMBER
user: login: pavelsavara
created_at: 2025-04-30T10:55:08Z
body: P0-P2 - This is useful angle, thanks!

> P0 - Is my service healthy? (Most responses are successful/correct, latency is reasonable)
- `aspnetcore.components.event.exceptions` - click to which component causes what type of exception. Stats, not trace, this doesn't tell you which sub component failed.
- `aspnetcore.components.event.duration` - click to which component is slow ?

> P1 - What is the load on my service? (How many requests/sec?)
- `aspnetcore.components.circuits.count` - how many sessions I processed today ?
- `aspnetcore.components.event.duration` - this is also counter, how many clicks I processed today ?
- `aspnetcore.components.circuits.duration` - this is session duration, interesting for many other KPIs
- `aspnetcore.components.navigation.count` - how many different Blazor pages people visited ? Route as tag.

> P1 - How many resources is my service using?
- `aspnetcore.components.circuits.active_circuits` - proxy to how much memory the sessions state holds ?
- `aspnetcore.components.circuits.connected_circuits` - how many signalR connections are open ? 
- Delta between the two above is about WebSocket disconnect/re-connect, network quality, browser tab going to sleep etc.

> P2 - Why is my service unhealthy?
- `aspnetcore.components.parameters.exceptions` - which component failed ? Exception type tag/stats, no trace.
- `aspnetcore.components.rendering.batch.exceptions` - which component failed ?
- `aspnetcore.components.parameters.duration` - which component makes my app rendering slow ?
- `aspnetcore.components.rendering.batch.duration` - which component makes events slow ? Also how many UI elements were in the diff ?

Note, I also mention `aspnetcore.components.circuits` metrics which already landed before, but we can improve them too if you want.
author_association: MEMBER
user: login: pavelsavara
created_at: 2025-04-30T11:03:06Z
body: > For example I know what a Blazor circuit is but I don't know what 'OnCircuit' is measuring. Is this a span that measures the entire duration of 1 blazor circuit?

This goes back to my [questions about long running activities](https://github.com/dotnet/aspnetcore/pull/61609#issuecomment-2834256951). 
That activity is relatively short lived at the moment, compared to whole life-time of the circuit. We can change that with consequences for inner dev loop.

We can definitely improve naming. 

`OnCircuit` - is representing logical circuit (duration), but at the moment, we stop it earlier.
`OnRoute` - is representing logical route/page in the app. Logicaly it should be active until you navigate elsewhere, but right now we stop it early. It links to circuit (when Blazor interactive).
`OnEvent` - in Blazor interactive, something was clicked. It typically happens inside of specific route and circuit which are linked.

Right now, the short circuit and route activities mostly serve as something that click event activities could link to. For the context.
author_association: MEMBER
user: login: pavelsavara
created_at: 2025-04-30T11:18:48Z
body: > > aspnetcore.components.rendering.batch.duration - Duration of rendering batch.
> 
> I don't think we currently define in our public Blazor docs what a render batch is. 

Maybe we just need to rename it? Anyway, this is more on the troubleshooting side of misbehaving component. Producing long diffs/batches leads to network traffic, latency and slow rendering. 

As I suggested above, we could have separate namespace for it with separate opt-in.
Or we could drop it and circle back in the future.

> > aspnetcore.components.rendering.batch.exception - Total number of exceptions during batch rendering.
> should we be counting the number of exceptions per some other period?

We also count exceptions per click/event. But I need to see if the exceptions from batch related problems would appear there.

> > aspnetcore.components.event.duration - Duration of processing browser event asynchronously.
> 
> I assume this is an average duration of all browser event handlers across the entire app regardless of render mode. 

At the moment this works only for SignalR interactive. I think we could also make it work for form-submit.
Making it work for WASM means that we need to fix OTEL for WASM and implement some publishing for it. 
It's out of scope for Net10.

> what does the "asynchronously" imply?

I already renamed this and dropped "async". It means including your DB request or whatever async business logic.

> Is this the duration of the `OnParametersSet` component lifecycle event?

Yes, or `OnInitialized`.

> > aspnetcore.components.navigation.count - Total number of route changes.
> 
> Is this a total count of all page navigations across the entire app regardless of render mode? 

Except WASM. 

> What would that be used for?

It has the route pattern as tag/dimension that you can use as filter. It's more business oriented KPI. Which of my pages are hot ?

author_association: MEMBER
user: login: pavelsavara
created_at: 2025-04-30T12:48:12Z
body: Making circuit/route activity/trace long lived has troubles with re-installing them into `Activity.Current`. 
And then troubles display of hours long one with sub-spans in the UI.
So, I think it's little benefit.

If we keep them short, maybe they should be literally 0ms long. Just an context anchor, grouping other traces.

Re Activity names: they are not very visible in the Aspire UI, and `DisplayName` prevails.

Circuit Activity/trace is created in internal `ComponentHub.StartCircuit` and so maybe `StartCircuit` is good name. There is also public API `CircuitHandler.OnCircuitOpenedAsync`, but I dislike `Async` postfix and also `OnCircuitOpenedAsync` happens short while later, being triggered by another message from JS. But `OnCircuitOpened` would be my second choice.

Route Activity/trace is created in `Router.SetParametersAsync` -> `Router.Refresh`. Not great names for the activity.
Maybe we can change it to `OnRouteChanged`

Regarding click/event. We already have concept of [event](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling). The activity should be active thru whole duration of `DispatchEventAsync` and would be parent for any business logic and distributed HTTP client traces. 

I would like event Activity also trigger for form submit, interop call from JS, and enhanced navigation.

Maybe we can change it to `Event` or `BrowserEvent` ?


author_association: MEMBER
user: login: pavelsavara
created_at: 2025-04-30T16:01:24Z
body: Sam: Is circuitId useful for DisplayName of Circuit activity? What else we could display there instead. Could we have IP address ?
author_association: MEMBER
user: login: samsp-msft
created_at: 2025-04-30T16:04:13Z
body: I met with @pavelsavara and I now understand what everything is for - it looks great. I think customers will be really happy with this. My concerns about granularity in terms of sending too much data have been mitigated.
author_association: NONE
user: login: samsp-msft
created_at: 2025-04-30T16:10:29Z
body: > Sam: Is circuitId useful for DisplayName of Circuit activity? What else we could display there instead. Could we have IP address ?

My mind set - if a customer is having an issue with your site, and calls IT to complain - how do they match the traces to the user? Is there somewhere that ID gets displayed to them?

If we don't stick in that data (which is a good from a being secure by default position) is there a hook-point we can document before the activity is finished that the customer can access the activity and add their extra tags to it? As the circuit is created as an instantaneous activity, it may not be on the stack for many calls to user code where they can access it.
author_association: NONE
user: login: pavelsavara
created_at: 2025-04-30T16:35:51Z
body: I think this problem of mapping traces to users on hotline is not specific to Blazor.

I'm linking the HTTP Activity/trace that created the circuit, so if there is more tags on the HTTP trace, they could use that (after the session and the long running HTTP/WS connection is finished).

Sometimes there is authenticated user in the HTTP context, but I think it's probably not good for use to expose any PII.

If the app developer wanted to add more tags, I believe that they could capture `Activity.Current` and add something.

Maybe the app developer could also add "show my circuitID" into application settings menu and the IT call center could ask them to click it. The circuitID is random number and it's not a secret from security perspective.
author_association: MEMBER
user: login: samsp-msft
created_at: 2025-04-30T17:32:34Z
body: > If the app developer wanted to add more tags, I believe that they could capture `Activity.Current` and add something.

The activity is created in CircuitFactory at line 127 and is stopped at line 173. Is there any user code executed between these points where the activity would be active, so they can retrieve it and add tags. Once its stopped, AFAIK its too late to add anything to it.
author_association: NONE
user: login: noahfalk
created_at: 2025-05-01T23:00:02Z
body: > This goes back to my https://github.com/dotnet/aspnetcore/pull/61609#issuecomment-2834256951.
That activity is relatively short lived at the moment, compared to whole life-time of the circuit. We can change that with consequences for inner dev loop.


> OnCircuit - is representing logical circuit (duration), but at the moment, we stop it earlier.
OnRoute - is representing logical route/page in the app. Logicaly it should be active until you navigate elsewhere, but right now we stop it early. It links to circuit (when Blazor interactive).
OnEvent - in Blazor interactive, something was clicked. It typically happens inside of specific route and circuit which are linked.

> Right now, the short circuit and route activities mostly serve as something that click event activities could link to. For the context.

Thanks, that helps a bunch on understanding where things are at now. A couple thoughts:

- In the question how to handle long-lived activities I think there is a 3rd option which is not to produce the activity at all. Instead of an event linking to the activity the only reference would be the circuit.id tag or route.id tag.
- Using links and ~0-length activities as a grouping/linking mechanism feels like a novel use of distributed tracing telemetry that many tools may not visualize in a helpful way. Have you tried looking at any of the telemetry in Azure Monitor's portal? As I recall Azure Monitor's viewer is quite aggressive at following links transitively in both the backwards and forwards directions. I'm suspicious that if you try to visualize any event AzureMonitor is going to wind up showing you every route and event in the entire circuit superimposed on a single very cluttered timeline.
- If we keep Circuit and Route I agree a name like CircuitStart or RouteStart/RouteChanged might be more readily understood. For Event perhaps we drop the 'On' and just name it 'Event' or 'HandleEvent'?

> For sessions, I wonder if when log messages are fired, is the activity context going to be the session activity, and if not is there a way to force it to be? @noahfalk this is an issue with essentially zero length spans - you might want to force log messages to be parented to it but something else is the activity at that point?

Activity.Current can be set at any time, but it can't be set to an Activity that is already stopped. If you did want to produce the set of all log messages that occurred within a given circuit I think the best options would be one of:
- In the telemetry viewer, query for all activities where circuit.id tag = X, then merge all logs linked to those activities. (This requires tools to support a table JOIN across logs and distributed traces which probably isn't universally supported)
- Add circuit.id as another field in the log messages and query for it directly. circuit.id could either be an explicit field in select log messages, or it could be added via enrichment. Once the field is there querying for all logs where circuit.id=X would return the desired logs.

author_association: MEMBER
user: login: pavelsavara
created_at: 2025-05-06T13:32:31Z
body: All, I created https://github.com/open-telemetry/semantic-conventions/pull/2235 in order to validate and document the new metrics in OTEL community.

As a result I updated all tags in this PR with prefix `aspnetcore.components`
author_association: MEMBER
user: login: pavelsavara
created_at: 2025-05-07T10:07:34Z
body: @lmolkova's feedback on the other PR is

- [x] merge duration and exception metrics into single histogram metric. Our HTTP _requestDuration already does that too. I will do it.

"event" means something specific in otel. But also something in the browser and in Blazor. [Suggestion](https://github.com/open-telemetry/semantic-conventions/pull/2235#discussion_r2076414163) to rename `aspnetcore.components.event` to `aspnetcore.components.operation` or `aspnetcore.components.call`. 

- [x] Perhaps we should think about it as event handler ? `aspnetcore.components.event_handler`

- [x] `circuits.active_circuits` -> `circuit.active`
- [x] `rendering.batch` -> `render_batch`

- `update_parameters` -> `parameter_set` or `parameter_binding`
- rename `aspnetcore.components` metrics prefix to -> `aspnetcore.component` because of otel pluralization rules. 
This would create mismatch with C# existing namespace. I prefer C# namespace because that is a existing namespace already. I will keep it as it is, unless somebody here convinces me otherwise.


author_association: MEMBER
user: login: pavelsavara
created_at: 2025-05-07T15:12:57Z
body: @lmolkova I pushed changes based on your feedback. 

The remaining issues are 
- `update_parameters` instrument name - I don't have strong opinion
- `aspnetcore.components` namespace vs `aspnetcore.component` namespace. 
Your proposal has one benefit, which is that `aspnetcore.component.type` tag sounds much better than `aspnetcore.components.type` for a specific class name. The downside is that we would not match C# namespace.

I will discuss it with @javiercn and come to some conclusion.
author_association: MEMBER
user: login: pavelsavara
created_at: 2025-05-19T10:05:40Z
body: I will merge this so that we could demo the feature soon. There are few pending issues, which I will address on next PR.
author_association: MEMBER