diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs
new file mode 100644
index 000000000000..592d3bee9164
--- /dev/null
+++ b/src/Components/Components/src/ComponentsActivitySource.cs
@@ -0,0 +1,176 @@
+// 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;
+
+///
+/// This is instance scoped per renderer
+///
+internal class ComponentsActivitySource
+{
+ internal const string Name = "Microsoft.AspNetCore.Components";
+ internal const string OnEventName = $"{Name}.OnEvent";
+ internal const string OnRouteName = $"{Name}.OnRoute";
+
+ 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(OnRouteName, 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.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"}";
+ activity.Start();
+ }
+ return activity;
+ }
+
+ public static void FailEventActivity(Activity activity, Exception ex)
+ {
+ if (!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..609707e7f1ed
--- /dev/null
+++ b/src/Components/Components/src/ComponentsMetrics.cs
@@ -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 _navigationCount;
+
+ private readonly Histogram _eventDuration;
+ private readonly Counter _eventException;
+
+ private readonly Histogram _parametersDuration;
+ private readonly Counter _parametersException;
+
+ private readonly Histogram _batchDuration;
+ private readonly Counter _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(
+ "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 { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+ _eventException = _meter.CreateCounter(
+ "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 { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+ _parametersException = _meter.CreateCounter(
+ "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 { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries });
+
+ _batchException = _meter.CreateCounter(
+ "aspnetcore.components.rendering.batch.exception",
+ unit: "{exceptions}",
+ 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) }
+ };
+
+ var duration = Stopwatch.GetElapsedTime(startTimestamp);
+ _batchDuration.Record(duration.TotalSeconds, tags);
+ }
+
+ public void EventFailed(string? exceptionType, EventCallback callback, string? attributeName)
+ {
+ var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate?.Target?.GetType())?.FullName;
+ var tags = new TagList
+ {
+ { "component.type", receiverName ?? "unknown" },
+ { "attribute.name", attributeName ?? "unknown"},
+ { "error.type", exceptionType ?? "unknown"}
+ };
+ _eventException.Add(1, tags);
+ }
+
+ public async Task CaptureEventFailedAsync(Task task, EventCallback callback, string? attributeName)
+ {
+ try
+ {
+ await task;
+ }
+ catch (Exception ex)
+ {
+ EventFailed(ex.GetType().Name, callback, attributeName);
+ }
+ }
+
+ public void PropertiesFailed(string? exceptionType, string? componentType)
+ {
+ var tags = new TagList
+ {
+ { "component.type", componentType ?? "unknown" },
+ { "error.type", exceptionType ?? "unknown"}
+ };
+ _parametersException.Add(1, tags);
+ }
+
+ public async Task CapturePropertiesFailedAsync(Task task, string? componentType)
+ {
+ try
+ {
+ await task;
+ }
+ catch (Exception ex)
+ {
+ PropertiesFailed(ex.GetType().Name, componentType);
+ }
+ }
+
+ public void BatchFailed(string? exceptionType)
+ {
+ var tags = new TagList
+ {
+ { "error.type", exceptionType ?? "unknown"}
+ };
+ _batchException.Add(1, tags);
+ }
+
+ public async Task CaptureBatchFailedAsync(Task task)
+ {
+ try
+ {
+ await task;
+ }
+ catch (Exception ex)
+ {
+ BatchFailed(ex.GetType().Name);
+ }
+ }
+
+ private static int BucketEditLength(int batchLength)
+ {
+ return batchLength switch
+ {
+ <= 1 => 1,
+ <= 2 => 2,
+ <= 5 => 5,
+ <= 10 => 10,
+ <= 50 => 50,
+ <= 100 => 100,
+ <= 500 => 500,
+ <= 1000 => 1000,
+ <= 10000 => 10000,
+ _ => 10001,
+ };
+ }
+
+ public void Dispose()
+ {
+ _meter.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 @@
+
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.NavigationManager.NotFound() -> void
Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, System.Func! 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(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;
+
+///
+/// Infrastructure APIs for registering diagnostic metrics.
+///
+public static class ComponentsMetricsServiceCollectionExtensions
+{
+ ///
+ /// Registers component rendering metrics
+ ///
+ /// The .
+ /// The .
+ public static IServiceCollection AddComponentsMetrics(
+ IServiceCollection services)
+ {
+ // do not register IConfigureOptions multiple times
+ if (!IsMeterFactoryRegistered(services))
+ {
+ services.AddMetrics();
+ }
+ services.TryAddSingleton();
+
+ return services;
+ }
+
+ ///
+ /// Registers component rendering traces
+ ///
+ /// The .
+ /// The .
+ public static IServiceCollection AddComponentsTracing(
+ IServiceCollection services)
+ {
+ services.TryAddScoped();
+
+ 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;
+
///
/// Gets the associated with the component.
///
diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs
index 5ba977930a46..d68640ea8b7b 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 _componentStateById = new Dictionary();
private readonly Dictionary _componentStateByComponent = new Dictionary();
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
- private readonly Dictionary _eventBindings = new();
+ private readonly Dictionary _eventBindings = new();
private readonly Dictionary _eventHandlerIdReplacements = new Dictionary();
private readonly ILogger _logger;
private readonly ComponentFactory _componentFactory;
- private readonly RenderingMetrics? _renderingMetrics;
+ private readonly ComponentsMetrics? _componentsMetrics;
+ private readonly ComponentsActivitySource? _componentsActivitySource;
+
private Dictionary? _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();
- _renderingMetrics = meterFactory != null ? new RenderingMetrics(meterFactory) : null;
+ _componentsMetrics = serviceProvider.GetService();
+ _componentsActivitySource = serviceProvider.GetService();
ServiceProviderCascadingValueSuppliers = serviceProvider.GetService() is null
? Array.Empty()
: serviceProvider.GetServices().ToArray();
}
+ internal ComponentsMetrics? ComponentMetrics => _componentsMetrics;
+ internal ComponentsActivitySource? ComponentActivitySource => _componentsActivitySource;
+
internal ICascadingValueSupplier[] ServiceProviderCascadingValueSuppliers { get; }
internal HotReloadManager HotReloadManager { get; set; } = HotReloadManager.Default;
@@ -442,7 +446,18 @@ 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;
+ if (ComponentActivitySource != null)
+ {
+ var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName;
+ var methodName = callback.Delegate.Method?.Name;
+ activity = ComponentActivitySource.StartEventActivity(receiverName, methodName, attributeName);
+ }
+
+ var eventStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsEventDurationEnabled ? 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 +499,37 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
_isBatchInProgress = true;
task = callback.InvokeAsync(eventArgs);
+
+ // collect metrics
+ if (ComponentMetrics != null && ComponentMetrics.IsEventDurationEnabled)
+ {
+ var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName;
+ var methodName = callback.Delegate.Method?.Name;
+ _ = ComponentMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, methodName, attributeName);
+ }
+ if (ComponentMetrics != null && ComponentMetrics.IsEventExceptionEnabled)
+ {
+ _ = ComponentMetrics.CaptureEventFailedAsync(task, callback, attributeName);
+ }
+
+ // stop activity/trace
+ if (ComponentActivitySource != null && activity != null)
+ {
+ _ = ComponentsActivitySource.CaptureEventStopAsync(task, activity);
+ }
}
catch (Exception e)
{
+ if (ComponentMetrics != null && ComponentMetrics.IsEventExceptionEnabled)
+ {
+ ComponentMetrics.EventFailed(e.GetType().FullName, callback, 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 here. EventCallback 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.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0;
try
{
@@ -801,9 +845,23 @@ 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.IsBatchDurationEnabled)
+ {
+ ComponentMetrics.BatchDuration(batchStartTimestamp, batch.UpdatedComponents.Count);
+ }
+ if (ComponentMetrics != null && ComponentMetrics.IsBatchExceptionEnabled)
+ {
+ _ = ComponentMetrics.CaptureBatchFailedAsync(updateDisplayTask);
+ }
}
catch (Exception e)
{
+ if (ComponentMetrics != null && ComponentMetrics.IsBatchExceptionEnabled)
+ {
+ ComponentMetrics.BatchFailed(e.GetType().Name);
+ }
+
// Ensure we catch errors while running the render functions of the components.
HandleException(e);
return;
@@ -947,15 +1005,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..a4a91ae9e0d2 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? _latestDirectParametersSnapshot; // Lazily instantiated
private bool _componentWasDisposed;
+ private readonly string? _componentTypeName;
///
/// Constructs an instance of .
@@ -51,6 +52,11 @@ public ComponentState(Renderer renderer, int componentId, IComponent component,
_hasCascadingParameters = true;
_hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions();
}
+
+ if (_renderer.ComponentMetrics != null && (_renderer.ComponentMetrics.IsParametersDurationEnabled || _renderer.ComponentMetrics.IsParametersExceptionEnabled))
+ {
+ _componentTypeName = component.GetType().FullName;
+ }
}
private static ComponentState? GetSectionOutletLogicalParent(Renderer renderer, SectionOutlet sectionOutlet)
@@ -231,14 +237,31 @@ 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
+ // Normalize sync and async exceptions into a Task
Task setParametersAsyncTask;
try
{
+ var stateStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersDurationEnabled ? Stopwatch.GetTimestamp() : 0;
+
setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters);
+
+ // collect metrics
+ if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersDurationEnabled)
+ {
+ _ = _renderer.ComponentMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName);
+ }
+ if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersExceptionEnabled)
+ {
+ _ = _renderer.ComponentMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName);
+ }
}
catch (Exception ex)
{
+ if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersExceptionEnabled)
+ {
+ _renderer.ComponentMetrics.PropertiesFailed(ex.GetType().FullName, _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 _renderTotalCounter;
- private readonly UpDownCounter _renderActiveCounter;
- private readonly Histogram _renderDuration;
-
- public RenderingMetrics(IMeterFactory meterFactory)
- {
- Debug.Assert(meterFactory != null);
-
- _meter = meterFactory.Create(MeterName);
-
- _renderTotalCounter = _meter.CreateCounter(
- "aspnetcore.components.rendering.count",
- unit: "{renders}",
- description: "Number of component renders performed.");
-
- _renderActiveCounter = _meter.CreateUpDownCounter(
- "aspnetcore.components.rendering.active_renders",
- unit: "{renders}",
- description: "Number of component renders performed.");
-
- _renderDuration = _meter.CreateHistogram(
- "aspnetcore.components.rendering.duration",
- unit: "ms",
- description: "Duration of component rendering operations per component.",
- advice: new InstrumentAdvice { 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(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/Rendering/ComponentsMetricsTest.cs b/src/Components/Components/test/Rendering/ComponentsMetricsTest.cs
new file mode 100644
index 000000000000..15a468cc819d
--- /dev/null
+++ b/src/Components/Components/test/Rendering/ComponentsMetricsTest.cs
@@ -0,0 +1,271 @@
+// 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 ComponentsMetricsTest
+{
+ private readonly TestMeterFactory _meterFactory;
+
+ public ComponentsMetricsTest()
+ {
+ _meterFactory = new TestMeterFactory();
+ }
+
+ [Fact]
+ public void Constructor_CreatesMetersCorrectly()
+ {
+ // Arrange & Act
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+
+ // Assert
+ Assert.Single(_meterFactory.Meters);
+ Assert.Equal(ComponentsMetrics.MeterName, _meterFactory.Meters[0].Name);
+ }
+
+ [Fact]
+ public async Task CaptureEventDurationAsync_RecordsDuration()
+ {
+ // Arrange
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+ using var eventAsyncDurationCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.event.duration");
+
+ // Act
+ var startTime = Stopwatch.GetTimestamp();
+ var task = Task.Delay(10); // Create a delay task
+ await componentsMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "MyMethod", "OnClickAsync");
+
+ // Assert
+ var measurements = eventAsyncDurationCollector.GetMeasurementSnapshot();
+
+ Assert.Single(measurements);
+ Assert.True(measurements[0].Value > 0);
+ Assert.Equal("TestComponent", measurements[0].Tags["component.type"]);
+ Assert.Equal("OnClickAsync", measurements[0].Tags["attribute.name"]);
+ Assert.Equal("MyMethod", measurements[0].Tags["component.method"]);
+ }
+
+ [Fact]
+ public async Task CaptureParametersDurationAsync_RecordsDuration()
+ {
+ // Arrange
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+ using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.parameters.duration");
+
+ // Act
+ var startTime = Stopwatch.GetTimestamp();
+ var task = Task.Delay(10); // Create a delay task
+ await componentsMetrics.CaptureParametersDurationAsync(task, startTime, "TestComponent");
+
+ // Assert
+ var measurements = parametersAsyncDurationCollector.GetMeasurementSnapshot();
+
+ Assert.Single(measurements);
+ Assert.True(measurements[0].Value > 0);
+ Assert.Equal("TestComponent", measurements[0].Tags["component.type"]);
+ }
+
+ [Fact]
+ public void BatchDuration_RecordsDuration()
+ {
+ // Arrange
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+ using var batchDurationCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.duration");
+
+ // Act
+ var startTime = Stopwatch.GetTimestamp();
+ Thread.Sleep(10); // Add a small delay to ensure a measurable duration
+ componentsMetrics.BatchDuration(startTime, 50);
+
+ // Assert
+ var measurements = batchDurationCollector.GetMeasurementSnapshot();
+
+ Assert.Single(measurements);
+ Assert.True(measurements[0].Value > 0);
+ Assert.Equal(50, measurements[0].Tags["diff.length.bucket"]);
+ }
+
+ [Fact]
+ public void EventFailed_RecordsException()
+ {
+ // Arrange
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+ using var eventExceptionCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.event.exception");
+
+ // Create a mock EventCallback
+ var callback = new EventCallback(new TestComponent(), (Action)(() => { }));
+
+ // Act
+ componentsMetrics.EventFailed("ArgumentException", callback, "OnClick");
+
+ // Assert
+ var measurements = eventExceptionCollector.GetMeasurementSnapshot();
+
+ Assert.Single(measurements);
+ Assert.Equal(1, measurements[0].Value);
+ Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]);
+ Assert.Equal("OnClick", measurements[0].Tags["attribute.name"]);
+ Assert.Contains("Microsoft.AspNetCore.Components.Rendering.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]);
+ }
+
+ [Fact]
+ public async Task CaptureEventFailedAsync_RecordsException()
+ {
+ // Arrange
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+ using var eventExceptionCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.event.exception");
+
+ // Create a mock EventCallback
+ var callback = new EventCallback(new TestComponent(), (Action)(() => { }));
+
+ // Create a task that throws an exception
+ var task = Task.FromException(new InvalidOperationException());
+
+ // Act
+ await componentsMetrics.CaptureEventFailedAsync(task, callback, "OnClickAsync");
+
+ // Assert
+ var measurements = eventExceptionCollector.GetMeasurementSnapshot();
+
+ Assert.Single(measurements);
+ Assert.Equal(1, measurements[0].Value);
+ Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]);
+ Assert.Equal("OnClickAsync", measurements[0].Tags["attribute.name"]);
+ Assert.Contains("Microsoft.AspNetCore.Components.Rendering.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]);
+ }
+
+ [Fact]
+ public void PropertiesFailed_RecordsException()
+ {
+ // Arrange
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+ using var parametersExceptionCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception");
+
+ // Act
+ componentsMetrics.PropertiesFailed("ArgumentException", "TestComponent");
+
+ // Assert
+ var measurements = parametersExceptionCollector.GetMeasurementSnapshot();
+
+ Assert.Single(measurements);
+ Assert.Equal(1, measurements[0].Value);
+ Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]);
+ Assert.Equal("TestComponent", measurements[0].Tags["component.type"]);
+ }
+
+ [Fact]
+ public async Task CapturePropertiesFailedAsync_RecordsException()
+ {
+ // Arrange
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+ using var parametersExceptionCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception");
+
+ // Create a task that throws an exception
+ var task = Task.FromException(new InvalidOperationException());
+
+ // Act
+ await componentsMetrics.CapturePropertiesFailedAsync(task, "TestComponent");
+
+ // Assert
+ var measurements = parametersExceptionCollector.GetMeasurementSnapshot();
+
+ Assert.Single(measurements);
+ Assert.Equal(1, measurements[0].Value);
+ Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]);
+ Assert.Equal("TestComponent", measurements[0].Tags["component.type"]);
+ }
+
+ [Fact]
+ public void BatchFailed_RecordsException()
+ {
+ // Arrange
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+ using var batchExceptionCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception");
+
+ // Act
+ componentsMetrics.BatchFailed("ArgumentException");
+
+ // Assert
+ var measurements = batchExceptionCollector.GetMeasurementSnapshot();
+
+ Assert.Single(measurements);
+ Assert.Equal(1, measurements[0].Value);
+ Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]);
+ }
+
+ [Fact]
+ public async Task CaptureBatchFailedAsync_RecordsException()
+ {
+ // Arrange
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+ using var batchExceptionCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception");
+
+ // Create a task that throws an exception
+ var task = Task.FromException(new InvalidOperationException());
+
+ // Act
+ await componentsMetrics.CaptureBatchFailedAsync(task);
+
+ // Assert
+ var measurements = batchExceptionCollector.GetMeasurementSnapshot();
+
+ Assert.Single(measurements);
+ Assert.Equal(1, measurements[0].Value);
+ Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]);
+ }
+
+ [Fact]
+ public void EnabledProperties_ReflectMeterState()
+ {
+ // Arrange
+ var componentsMetrics = new ComponentsMetrics(_meterFactory);
+
+ // Create collectors to ensure the meters are enabled
+ using var eventAsyncDurationCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.event.duration");
+ using var eventExceptionCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.event.exception");
+ using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.parameters.duration");
+ using var parametersExceptionCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception");
+ using var batchDurationCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.duration");
+ using var batchExceptionCollector = new MetricCollector(_meterFactory,
+ ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception");
+
+ // Assert
+ Assert.True(componentsMetrics.IsEventDurationEnabled);
+ Assert.True(componentsMetrics.IsEventExceptionEnabled);
+ Assert.True(componentsMetrics.IsParametersDurationEnabled);
+ Assert.True(componentsMetrics.IsParametersExceptionEnabled);
+ Assert.True(componentsMetrics.IsBatchDurationEnabled);
+ Assert.True(componentsMetrics.IsBatchExceptionEnabled);
+ }
+
+ // Helper class for mock components
+ public class TestComponent : IComponent, IHandleEvent
+ {
+ public void Attach(RenderHandle renderHandle) { }
+ public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => Task.CompletedTask;
+ public Task SetParametersAsync(ParameterView parameters) => Task.CompletedTask;
+ }
+}
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(_meterFactory,
- RenderingMetrics.MeterName, "aspnetcore.components.rendering.count");
- using var activeCounter = new MetricCollector(_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(_meterFactory,
- RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
- using var durationCollector = new MetricCollector(_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(_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(_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(_meterFactory,
- RenderingMetrics.MeterName, "aspnetcore.components.rendering.count");
- using var activeCounter = new MetricCollector(_meterFactory,
- RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
- using var durationCollector = new MetricCollector(_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(_meterFactory,
- RenderingMetrics.MeterName, "aspnetcore.components.rendering.count");
- using var activeCounter = new MetricCollector(_meterFactory,
- RenderingMetrics.MeterName, "aspnetcore.components.rendering.active_renders");
- using var durationCollector = new MetricCollector(_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();
+ ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(services);
+ ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(services);
+
// Form handling
services.AddSupplyValueFromFormProvider();
services.TryAddScoped();
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 CreateCircuitHostAsync(
{
navigationManager.Initialize(baseUri, uri);
}
+ var componentsActivitySource = scope.ServiceProvider.GetService();
if (components.Count > 0)
{
@@ -109,6 +110,7 @@ public async ValueTask CreateCircuitHostAsync(
navigationManager,
circuitHandlers,
_circuitMetrics,
+ componentsActivitySource,
_loggerFactory.CreateLogger());
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, 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..da7b8e9d297b 100644
--- a/src/Components/Server/src/Circuits/CircuitMetrics.cs
+++ b/src/Components/Server/src/Circuits/CircuitMetrics.cs
@@ -26,7 +26,7 @@ public CircuitMetrics(IMeterFactory meterFactory)
_circuitTotalCounter = _meter.CreateCounter(
"aspnetcore.components.circuits.count",
unit: "{circuits}",
- description: "Number of active circuits.");
+ description: "Total number of circuits.");
_circuitActiveCounter = _meter.CreateUpDownCounter(
"aspnetcore.components.circuits.active_circuits",
@@ -47,57 +47,48 @@ public CircuitMetrics(IMeterFactory meterFactory)
public void OnCircuitOpened()
{
- var tags = new TagList();
-
if (_circuitActiveCounter.Enabled)
{
- _circuitActiveCounter.Add(1, tags);
+ _circuitActiveCounter.Add(1);
}
if (_circuitTotalCounter.Enabled)
{
- _circuitTotalCounter.Add(1, tags);
+ _circuitTotalCounter.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();
}
///
@@ -137,7 +140,7 @@ public async ValueTask 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();
- return meterFactory != null ? new CircuitMetrics(meterFactory) : null;
- });
+ services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddSingleton();
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()), cancellationToken);
+ await circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of()), 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()), cancellationToken);
+ var initializeTask = circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of()), 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()), new CancellationToken());
+ var initializeAsyncTask = circuitHost.InitializeAsync(new ProtectedPrerenderComponentApplicationStore(Mock.Of()), default, new CancellationToken());
// Assert: No synchronous exceptions
handler.VerifyAll();
diff --git a/src/Components/Server/test/Circuits/CircuitMetricsTest.cs b/src/Components/Server/test/Circuits/CircuitMetricsTest.cs
index 770125996634..9124d5d64294 100644
--- a/src/Components/Server/test/Circuits/CircuitMetricsTest.cs
+++ b/src/Components/Server/test/Circuits/CircuitMetricsTest.cs
@@ -95,7 +95,7 @@ public void OnConnectionDown_DecreasesConnectedCounter()
}
[Fact]
- public void OnCircuitDown_UpdatesCountersAndRecordsDuration()
+ public async Task OnCircuitDown_UpdatesCountersAndRecordsDuration()
{
// Arrange
var circuitMetrics = new CircuitMetrics(_meterFactory);
@@ -108,7 +108,7 @@ public void OnCircuitDown_UpdatesCountersAndRecordsDuration()
// 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);
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 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 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();
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.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;
///
diff --git a/src/Shared/Metrics/MetricsConstants.cs b/src/Shared/Metrics/MetricsConstants.cs
index ff64c6fefcad..cdb338f1d7a0 100644
--- a/src/Shared/Metrics/MetricsConstants.cs
+++ b/src/Shared/Metrics/MetricsConstants.cs
@@ -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 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 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 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];
}