From cebb68edcfcbdf880050053e66f5627b048110aa Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 23 Apr 2025 20:01:34 +0200 Subject: [PATCH 01/26] rebase --- .../Components/src/PublicAPI.Unshipped.txt | 2 + ...eringMetricsServiceCollectionExtensions.cs | 31 ++ .../Components/src/RenderTree/Renderer.cs | 64 ++- .../src/Rendering/ComponentState.cs | 34 +- .../src/Rendering/RenderingMetrics.cs | 277 +++++++++--- .../test/Rendering/RenderingMetricsTest.cs | 415 ++++++++++++------ ...orComponentsServiceCollectionExtensions.cs | 2 + .../Server/src/Circuits/CircuitHost.cs | 3 +- .../Server/src/Circuits/CircuitMetrics.cs | 25 +- .../ComponentServiceCollectionExtensions.cs | 7 +- .../test/Circuits/CircuitMetricsTest.cs | 4 +- .../src/HtmlRendering/StaticHtmlRenderer.cs | 1 - src/Shared/Metrics/MetricsConstants.cs | 4 +- 13 files changed, 649 insertions(+), 220 deletions(-) create mode 100644 src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 21c0226e2ef5..46b13fa70546 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ #nullable enable +Microsoft.AspNetCore.Components.Infrastructure.RenderingMetricsServiceCollectionExtensions 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 @@ -11,5 +12,6 @@ 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.RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(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..da1a539278e9 --- /dev/null +++ b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.AspNetCore.Components.Infrastructure; + +/// +/// Infrastructure APIs for registering diagnostic metrics. +/// +public static class RenderingMetricsServiceCollectionExtensions +{ + /// + /// Registers component rendering metrics + /// + /// The . + /// The . + public static IServiceCollection AddRenderingMetrics( + IServiceCollection services) + { + if (RenderingMetrics.IsMetricsSupported) + { + services.AddMetrics(); + services.TryAddSingleton(); + } + + return services; + } +} diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 5ba977930a46..f616da46b94f 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,12 +24,14 @@ 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; @@ -92,16 +93,18 @@ 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; + if (RenderingMetrics.IsMetricsSupported) + { + _renderingMetrics = serviceProvider.GetService(); + } ServiceProviderCascadingValueSuppliers = serviceProvider.GetService() is null ? Array.Empty() : serviceProvider.GetServices().ToArray(); } + internal RenderingMetrics? RenderingMetrics => RenderingMetrics.IsMetricsSupported ? _renderingMetrics : null; + internal ICascadingValueSupplier[] ServiceProviderCascadingValueSuppliers { get; } internal HotReloadManager HotReloadManager { get; set; } = HotReloadManager.Default; @@ -437,12 +440,14 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie { Dispatcher.AssertAccess(); + var eventStartTimestamp = RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled ? Stopwatch.GetTimestamp() : 0; + if (waitForQuiescence) { _pendingTasks ??= new(); } - var (renderedByComponentId, callback) = GetRequiredEventBindingEntry(eventHandlerId); + var (renderedByComponentId, callback, attributeName) = GetRequiredEventBindingEntry(eventHandlerId); // 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 +489,25 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie _isBatchInProgress = true; task = callback.InvokeAsync(eventArgs); + + // collect metrics + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled) + { + var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; + RenderingMetrics.EventDurationSync(eventStartTimestamp, receiverName, attributeName); + _ = RenderingMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, attributeName); + } + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + { + _ = RenderingMetrics.CaptureEventFailedAsync(task, callback, attributeName); + } } catch (Exception e) { + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + { + RenderingMetrics.EventFailed(e.GetType().FullName, callback, attributeName); + } HandleExceptionViaErrorBoundary(e, receiverComponentState); return Task.CompletedTask; } @@ -497,6 +518,10 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie // Since the task has yielded - process any queued rendering work before we return control // to the caller. ProcessPendingRender(); + + //callback.Receiver + //callback.Delegate.Method. + } // Task completed synchronously or is still running. We already processed all of the rendering @@ -638,7 +663,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 +671,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 +721,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 +795,7 @@ private void ProcessRenderQueue() _isBatchInProgress = true; var updateDisplayTask = Task.CompletedTask; + var batchStartTimestamp = RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { @@ -801,9 +827,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 (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled) + { + _renderingMetrics.BatchDuration(batchStartTimestamp, batch.UpdatedComponents.Count); + } + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + { + _ = _renderingMetrics.CaptureBatchFailedAsync(updateDisplayTask); + } } catch (Exception e) { + if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + { + _renderingMetrics.BatchFailed(e.GetType().Name); + } + // Ensure we catch errors while running the render functions of the components. HandleException(e); return; @@ -947,15 +987,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..7b80404fb4d3 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 (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && (_renderer.RenderingMetrics.IsDiffDurationEnabled || _renderer.RenderingMetrics.IsStateDurationEnabled || _renderer.RenderingMetrics.IsStateExceptionEnabled)) + { + _componentTypeName = component.GetType().FullName; + } } private static ComponentState? GetSectionOutletLogicalParent(Renderer renderer, SectionOutlet sectionOutlet) @@ -102,6 +108,7 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re _nextRenderTree.Clear(); + var diffStartTimestamp = RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { renderFragment(_nextRenderTree); @@ -118,6 +125,8 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re // We don't want to make errors from this be recoverable, because there's no legitimate reason for them to happen _nextRenderTree.AssertTreeIsValid(Component); + var startCount = batchBuilder.EditsBuffer.Count; + // Swap the old and new tree builders (CurrentRenderTree, _nextRenderTree) = (_nextRenderTree, CurrentRenderTree); @@ -129,6 +138,11 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re CurrentRenderTree.GetFrames()); batchBuilder.UpdatedComponentDiffs.Append(diff); batchBuilder.InvalidateParameterViews(); + + if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled) + { + _renderer.RenderingMetrics.DiffDuration(diffStartTimestamp, _componentTypeName, batchBuilder.EditsBuffer.Count - startCount); + } } // Callers expect this method to always return a faulted task. @@ -231,14 +245,32 @@ 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 = RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; + setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters); + + // collect metrics + if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled) + { + _renderer.RenderingMetrics.ParametersDurationSync(stateStartTimestamp, _componentTypeName); + _ = _renderer.RenderingMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName); + } + if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + { + _ = _renderer.RenderingMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); + } } catch (Exception ex) { + if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + { + _renderer.RenderingMetrics.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 index 54b32a793cc7..6c0f9f918053 100644 --- a/src/Components/Components/src/Rendering/RenderingMetrics.cs +++ b/src/Components/Components/src/Rendering/RenderingMetrics.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; @@ -10,97 +11,273 @@ 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; + + private readonly Histogram _eventSyncDuration; + private readonly Histogram _eventAsyncDuration; + private readonly Counter _eventException; + + private readonly Histogram _parametersSyncDuration; + private readonly Histogram _parametersAsyncDuration; + private readonly Counter _parametersException; + + private readonly Histogram _diffDuration; + + private readonly Histogram _batchDuration; + private readonly Counter _batchException; + + [FeatureSwitchDefinition("System.Diagnostics.Metrics.Meter.IsSupported")] + public static bool IsMetricsSupported { get; } = InitializeIsMetricsSupported(); + private static bool InitializeIsMetricsSupported() => AppContext.TryGetSwitch("System.Diagnostics.Metrics.Meter.IsSupported", out bool isSupported) ? isSupported : true; + + public bool IsEventDurationEnabled => IsMetricsSupported && (_eventSyncDuration.Enabled || _eventAsyncDuration.Enabled); + public bool IsEventExceptionEnabled => IsMetricsSupported && _eventException.Enabled; + + public bool IsStateDurationEnabled => IsMetricsSupported && (_parametersSyncDuration.Enabled || _parametersAsyncDuration.Enabled); + public bool IsStateExceptionEnabled => IsMetricsSupported && _parametersException.Enabled; + + public bool IsDiffDurationEnabled => IsMetricsSupported && _diffDuration.Enabled; + + public bool IsBatchDurationEnabled => IsMetricsSupported && _batchDuration.Enabled; + public bool IsBatchExceptionEnabled => IsMetricsSupported && _batchException.Enabled; public RenderingMetrics(IMeterFactory meterFactory) { + if (!IsMetricsSupported) + { + // TryAddSingleton prevents trimming constructors, so we trim constructor this way + throw new NotSupportedException("Metrics are not supported in this environment."); + } + Debug.Assert(meterFactory != null); _meter = meterFactory.Create(MeterName); - _renderTotalCounter = _meter.CreateCounter( - "aspnetcore.components.rendering.count", - unit: "{renders}", - description: "Number of component renders performed."); + _eventSyncDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.event.synchronous.duration", + unit: "s", + description: "Duration of processing browser event synchronously.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + + _eventAsyncDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.event.asynchronous.duration", + unit: "s", + description: "Duration of processing browser event asynchronously.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + + _eventException = _meter.CreateCounter( + "aspnetcore.components.rendering.event.exception", + unit: "{exceptions}", + description: "Total number of exceptions during browser event processing."); + + _parametersSyncDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.parameters.synchronous.duration", + unit: "s", + description: "Duration of processing component parameters synchronously.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + + _parametersAsyncDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.parameters.asynchronous.duration", + unit: "s", + description: "Duration of processing component parameters asynchronously.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + + _parametersException = _meter.CreateCounter( + "aspnetcore.components.rendering.parameters.exception", + unit: "{exceptions}", + description: "Total number of exceptions during processing component parameters."); - _renderActiveCounter = _meter.CreateUpDownCounter( - "aspnetcore.components.rendering.active_renders", - unit: "{renders}", - description: "Number of component renders performed."); + _diffDuration = _meter.CreateHistogram( + "aspnetcore.components.rendering.diff.duration", + unit: "s", + description: "Duration of rendering component HTML diff.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); - _renderDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.duration", - unit: "ms", - description: "Duration of component rendering operations per component.", + _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 RenderStart(string componentType) + public void EventDurationSync(long startTimestamp, string? componentType, string? attributeName) { - var tags = new TagList(); - tags = InitializeRequestTags(componentType, tags); + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + { "attribute.name", attributeName ?? "unknown"} + }; - if (_renderActiveCounter.Enabled) + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _eventSyncDuration.Record(duration.TotalSeconds, tags); + } + + public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? attributeName) + { + try { - _renderActiveCounter.Add(1, tags); + await task; + + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + { "attribute.name", attributeName ?? "unknown" } + }; + + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _eventAsyncDuration.Record(duration.TotalSeconds, tags); } - if (_renderTotalCounter.Enabled) + catch { - _renderTotalCounter.Add(1, tags); + // none } } - public void RenderEnd(string componentType, Exception? exception, long startTimestamp, long currentTimestamp) + public void ParametersDurationSync(long startTimestamp, string? componentType) + { + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + }; + + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _parametersSyncDuration.Record(duration.TotalSeconds, tags); + } + + public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, string? componentType) { - // Tags must match request start. - var tags = new TagList(); - tags = InitializeRequestTags(componentType, tags); + try + { + await task; - if (_renderActiveCounter.Enabled) + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + }; + + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _parametersAsyncDuration.Record(duration.TotalSeconds, tags); + } + catch { - _renderActiveCounter.Add(-1, tags); + // none } + } - if (_renderDuration.Enabled) + public void DiffDuration(long startTimestamp, string? componentType, int diffLength) + { + var tags = new TagList { - if (exception != null) - { - TryAddTag(ref tags, "error.type", exception.GetType().FullName); - } + { "component.type", componentType ?? "unknown" }, + { "diff.length.bucket", BucketEditLength(diffLength) } + }; + + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _diffDuration.Record(duration.TotalSeconds, tags); + } + + 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); + } - var duration = Stopwatch.GetElapsedTime(startTimestamp, currentTimestamp); - _renderDuration.Record(duration.TotalMilliseconds, 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); } } - private static TagList InitializeRequestTags(string componentType, TagList tags) + public void PropertiesFailed(string? exceptionType, string? componentType) { - tags.Add("component.type", componentType); - return tags; + var tags = new TagList + { + { "component.type", componentType ?? "unknown" }, + { "error.type", exceptionType ?? "unknown"} + }; + _parametersException.Add(1, tags); } - public bool IsDurationEnabled() => _renderDuration.Enabled; + public async Task CapturePropertiesFailedAsync(Task task, string? componentType) + { + try + { + await task; + } + catch (Exception ex) + { + PropertiesFailed(ex.GetType().Name, componentType); + } + } - public void Dispose() + public void BatchFailed(string? exceptionType) { - _meter.Dispose(); + var tags = new TagList + { + { "error.type", exceptionType ?? "unknown"} + }; + _batchException.Add(1, tags); } - private static bool TryAddTag(ref TagList tags, string name, object? value) + public async Task CaptureBatchFailedAsync(Task task) { - for (var i = 0; i < tags.Count; i++) + try { - if (tags[i].Key == name) - { - return false; - } + await task; } + catch (Exception ex) + { + BatchFailed(ex.GetType().Name); + } + } - tags.Add(new KeyValuePair(name, value)); - return true; + 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/test/Rendering/RenderingMetricsTest.cs b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs index 7339ebbf5dec..be4133ac9a9f 100644 --- a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs +++ b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs @@ -33,206 +33,367 @@ public void Constructor_CreatesMetersCorrectly() } [Fact] - public void RenderStart_IncreasesCounters() + public void EventDurationSync_RecordsDuration() { // 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 eventSyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); - var componentType = "TestComponent"; + // Act + var startTime = Stopwatch.GetTimestamp(); + Thread.Sleep(10); // Add a small delay to ensure a measurable duration + renderingMetrics.EventDurationSync(startTime, "TestComponent", "OnClick"); + + // Assert + var measurements = eventSyncDurationCollector.GetMeasurementSnapshot(); + + Assert.Single(measurements); + Assert.True(measurements[0].Value > 0); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); + Assert.Equal("OnClick", measurements[0].Tags["attribute.name"]); + } + + [Fact] + public async Task CaptureEventDurationAsync_RecordsDuration() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); // Act - renderingMetrics.RenderStart(componentType); + var startTime = Stopwatch.GetTimestamp(); + var task = Task.Delay(10); // Create a delay task + await renderingMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "OnClickAsync"); // Assert - var totalMeasurements = totalCounter.GetMeasurementSnapshot(); - var activeMeasurements = activeCounter.GetMeasurementSnapshot(); + var measurements = eventAsyncDurationCollector.GetMeasurementSnapshot(); - Assert.Single(totalMeasurements); - Assert.Equal(1, totalMeasurements[0].Value); - Assert.Equal(componentType, totalMeasurements[0].Tags["component.type"]); + 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.Single(activeMeasurements); - Assert.Equal(1, activeMeasurements[0].Value); - Assert.Equal(componentType, activeMeasurements[0].Tags["component.type"]); + [Fact] + public void ParametersDurationSync_RecordsDuration() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); + + // Act + var startTime = Stopwatch.GetTimestamp(); + Thread.Sleep(10); // Add a small delay to ensure a measurable duration + renderingMetrics.ParametersDurationSync(startTime, "TestComponent"); + + // Assert + var measurements = parametersSyncDurationCollector.GetMeasurementSnapshot(); + + Assert.Single(measurements); + Assert.True(measurements[0].Value > 0); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); } [Fact] - public void RenderEnd_DecreasesActiveCounterAndRecordsDuration() + public async Task CaptureParametersDurationAsync_RecordsDuration() { // 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"); + using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + + // Act + var startTime = Stopwatch.GetTimestamp(); + var task = Task.Delay(10); // Create a delay task + await renderingMetrics.CaptureParametersDurationAsync(task, startTime, "TestComponent"); + + // Assert + var measurements = parametersAsyncDurationCollector.GetMeasurementSnapshot(); - var componentType = "TestComponent"; + Assert.Single(measurements); + Assert.True(measurements[0].Value > 0); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); + } + + [Fact] + public void DiffDuration_RecordsDuration() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var diffDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); // 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); + renderingMetrics.DiffDuration(startTime, "TestComponent", 5); // Assert - var activeMeasurements = activeCounter.GetMeasurementSnapshot(); - var durationMeasurements = durationCollector.GetMeasurementSnapshot(); + var measurements = diffDurationCollector.GetMeasurementSnapshot(); - Assert.Single(activeMeasurements); - Assert.Equal(-1, activeMeasurements[0].Value); - Assert.Equal(componentType, activeMeasurements[0].Tags["component.type"]); + Assert.Single(measurements); + Assert.True(measurements[0].Value > 0); + Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); + Assert.Equal(5, measurements[0].Tags["diff.length.bucket"]); + } - Assert.Single(durationMeasurements); - Assert.True(durationMeasurements[0].Value > 0); - Assert.Equal(componentType, durationMeasurements[0].Tags["component.type"]); + [Fact] + public void BatchDuration_RecordsDuration() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var batchDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); + + // Act + var startTime = Stopwatch.GetTimestamp(); + Thread.Sleep(10); // Add a small delay to ensure a measurable duration + renderingMetrics.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 RenderEnd_AddsErrorTypeTag_WhenExceptionIsProvided() + public void EventFailed_RecordsException() { // Arrange var renderingMetrics = new RenderingMetrics(_meterFactory); - using var durationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration"); + using var eventExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); - var componentType = "TestComponent"; - var exception = new InvalidOperationException("Test exception"); + // Create a mock EventCallback + var callback = new EventCallback(new TestComponent(), (Action)(() => { })); // Act - var startTime = Stopwatch.GetTimestamp(); - Thread.Sleep(10); - var endTime = Stopwatch.GetTimestamp(); - renderingMetrics.RenderEnd(componentType, exception, startTime, endTime); + renderingMetrics.EventFailed("ArgumentException", callback, "OnClick"); // Assert - var durationMeasurements = durationCollector.GetMeasurementSnapshot(); + var measurements = eventExceptionCollector.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"]); + 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.RenderingMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); } [Fact] - public void IsDurationEnabled_ReturnsMeterEnabledState() + public async Task CaptureEventFailedAsync_RecordsException() { // Arrange var renderingMetrics = new RenderingMetrics(_meterFactory); + using var eventExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.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()); - // Create a collector to ensure the meter is enabled - using var durationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.duration"); + // Act + await renderingMetrics.CaptureEventFailedAsync(task, callback, "OnClickAsync"); + + // Assert + var measurements = eventExceptionCollector.GetMeasurementSnapshot(); - // Act & Assert - Assert.True(renderingMetrics.IsDurationEnabled()); + 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.RenderingMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); } [Fact] - public void FullRenderingLifecycle_RecordsAllMetricsCorrectly() + public void PropertiesFailed_RecordsException() { // 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"); + using var parametersExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); - var componentType = "TestComponent"; + // Act + renderingMetrics.PropertiesFailed("ArgumentException", "TestComponent"); - // Act - Simulating a full rendering lifecycle - var startTime = Stopwatch.GetTimestamp(); + // Assert + var measurements = parametersExceptionCollector.GetMeasurementSnapshot(); - // 1. Component render starts - renderingMetrics.RenderStart(componentType); + 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"]); + } - // 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); + [Fact] + public async Task CapturePropertiesFailedAsync_RecordsException() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var parametersExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + + // Create a task that throws an exception + var task = Task.FromException(new InvalidOperationException()); + + // Act + await renderingMetrics.CapturePropertiesFailedAsync(task, "TestComponent"); // Assert - var totalMeasurements = totalCounter.GetMeasurementSnapshot(); - var activeMeasurements = activeCounter.GetMeasurementSnapshot(); - var durationMeasurements = durationCollector.GetMeasurementSnapshot(); + var measurements = parametersExceptionCollector.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"]); + 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"]); + } - // 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"]); + [Fact] + public void BatchFailed_RecordsException() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var batchExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + + // Act + renderingMetrics.BatchFailed("ArgumentException"); + + // Assert + var measurements = batchExceptionCollector.GetMeasurementSnapshot(); - // 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"]); + Assert.Single(measurements); + Assert.Equal(1, measurements[0].Value); + Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]); } [Fact] - public void MultipleRenders_TracksMetricsIndependently() + public async Task CaptureBatchFailedAsync_RecordsException() { // 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"); + using var batchExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); - var componentType1 = "TestComponent1"; - var componentType2 = "TestComponent2"; + // Create a task that throws an exception + var task = Task.FromException(new InvalidOperationException()); // Act - // First component render - var startTime1 = Stopwatch.GetTimestamp(); - renderingMetrics.RenderStart(componentType1); + await renderingMetrics.CaptureBatchFailedAsync(task); - // Second component render starts while first is still rendering - var startTime2 = Stopwatch.GetTimestamp(); - renderingMetrics.RenderStart(componentType2); + // Assert + var measurements = batchExceptionCollector.GetMeasurementSnapshot(); - // First component render ends - Thread.Sleep(5); - var endTime1 = Stopwatch.GetTimestamp(); - renderingMetrics.RenderEnd(componentType1, null, startTime1, endTime1); + Assert.Single(measurements); + Assert.Equal(1, measurements[0].Value); + Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]); + } - // Second component render ends - Thread.Sleep(5); - var endTime2 = Stopwatch.GetTimestamp(); - renderingMetrics.RenderEnd(componentType2, null, startTime2, endTime2); + [Fact] + public void EnabledProperties_ReflectMeterState() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + + // Create collectors to ensure the meters are enabled + using var eventSyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); + using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); + using var eventExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); + using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + using var parametersExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + using var diffDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + using var batchDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); + using var batchExceptionCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); // 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); + Assert.True(renderingMetrics.IsEventDurationEnabled); + Assert.True(renderingMetrics.IsEventExceptionEnabled); + Assert.True(renderingMetrics.IsStateDurationEnabled); + Assert.True(renderingMetrics.IsStateExceptionEnabled); + Assert.True(renderingMetrics.IsDiffDurationEnabled); + Assert.True(renderingMetrics.IsBatchDurationEnabled); + Assert.True(renderingMetrics.IsBatchExceptionEnabled); + } + + [Fact] + public void BucketEditLength_ReturnsCorrectBucket() + { + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + using var diffDurationCollector = new MetricCollector(_meterFactory, + RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + + // Act & Assert - Test different diff lengths + var startTime = Stopwatch.GetTimestamp(); + + // Test each bucket boundary + renderingMetrics.DiffDuration(startTime, "Component", 1); + renderingMetrics.DiffDuration(startTime, "Component", 2); + renderingMetrics.DiffDuration(startTime, "Component", 5); + renderingMetrics.DiffDuration(startTime, "Component", 10); + renderingMetrics.DiffDuration(startTime, "Component", 50); + renderingMetrics.DiffDuration(startTime, "Component", 100); + renderingMetrics.DiffDuration(startTime, "Component", 500); + renderingMetrics.DiffDuration(startTime, "Component", 1000); + renderingMetrics.DiffDuration(startTime, "Component", 10000); + renderingMetrics.DiffDuration(startTime, "Component", 20000); // Should be 10001 + + // Assert + var measurements = diffDurationCollector.GetMeasurementSnapshot(); + + Assert.Equal(10, measurements.Count); + Assert.Equal(1, measurements[0].Tags["diff.length.bucket"]); + Assert.Equal(2, measurements[1].Tags["diff.length.bucket"]); + Assert.Equal(5, measurements[2].Tags["diff.length.bucket"]); + Assert.Equal(10, measurements[3].Tags["diff.length.bucket"]); + Assert.Equal(50, measurements[4].Tags["diff.length.bucket"]); + Assert.Equal(100, measurements[5].Tags["diff.length.bucket"]); + Assert.Equal(500, measurements[6].Tags["diff.length.bucket"]); + Assert.Equal(1000, measurements[7].Tags["diff.length.bucket"]); + Assert.Equal(10000, measurements[8].Tags["diff.length.bucket"]); + Assert.Equal(10001, measurements[9].Tags["diff.length.bucket"]); + } + + [Fact] + public void Dispose_DisposesUnderlyingMeter() + { + // This test verifies that the meter is disposed when the metrics instance is disposed + // This is a bit tricky to test directly, so we'll use an indirect approach + + // Arrange + var renderingMetrics = new RenderingMetrics(_meterFactory); + + // Act + renderingMetrics.Dispose(); + + // Try to use the disposed meter - this should not throw since TestMeterFactory + // doesn't actually dispose the meter in test contexts + var startTime = Stopwatch.GetTimestamp(); + renderingMetrics.EventDurationSync(startTime, "TestComponent", "OnClick"); + } + + // 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/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 302dec7dcb16..249ee69fccd3 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -76,6 +76,8 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddScoped(); + RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(services); + // Form handling services.AddSupplyValueFromFormProvider(); services.TryAddScoped(); diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index b8bc2b05e158..ab461d8b62a2 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -120,6 +120,8 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, C { _initialized = true; // We're ready to accept incoming JSInterop calls from here on + _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. // On Blazor Web scenarios we delay running these handlers until the first UpdateRootComponents call @@ -235,7 +237,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/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/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/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]; } From 0bcd459a9a86b1bd19520cfa4b1f08d6390a4520 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 09:07:38 +0200 Subject: [PATCH 02/26] more --- ...eringMetricsServiceCollectionExtensions.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs index da1a539278e9..875ba6998a65 100644 --- a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs +++ b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.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.Metrics; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -22,10 +23,26 @@ public static IServiceCollection AddRenderingMetrics( { if (RenderingMetrics.IsMetricsSupported) { - services.AddMetrics(); + // do not register IConfigureOptions multiple times + if (!IsMeterFactoryRegistered(services)) + { + services.AddMetrics(); + } services.TryAddSingleton(); } return services; } + + private static bool IsMeterFactoryRegistered(IServiceCollection services) + { + foreach (var service in services) + { + if (service.ServiceType == typeof(IMeterFactory)) + { + return true; + } + } + return false; + } } From 2557f08a5f2dbfa216173d02e1ca75b03a1342ae Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 12:56:56 +0200 Subject: [PATCH 03/26] - remove FeatureSwitchDefinition for now - add tracing --- .../Components/src/PublicAPI.Unshipped.txt | 1 + ...eringMetricsServiceCollectionExtensions.cs | 24 +++++--- .../Components/src/RenderTree/Renderer.cs | 54 ++++++++++++------ .../src/Rendering/ComponentState.cs | 14 ++--- .../src/Rendering/RenderingActivitySource.cs | 55 +++++++++++++++++++ .../src/Rendering/RenderingMetrics.cs | 31 ++++------- .../test/Rendering/RenderingMetricsTest.cs | 7 ++- ...orComponentsServiceCollectionExtensions.cs | 1 + 8 files changed, 134 insertions(+), 53 deletions(-) create mode 100644 src/Components/Components/src/Rendering/RenderingActivitySource.cs diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 477cb88e7d85..c372bbf750c2 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -15,5 +15,6 @@ Microsoft.AspNetCore.Components.SupplyParameterFromPersistentComponentStateAttri 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.RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.AspNetCore.Components.Infrastructure.RenderingMetricsServiceCollectionExtensions.AddRenderingTracing(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 index 875ba6998a65..b40b3adf4de3 100644 --- a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs +++ b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs @@ -21,15 +21,25 @@ public static class RenderingMetricsServiceCollectionExtensions public static IServiceCollection AddRenderingMetrics( IServiceCollection services) { - if (RenderingMetrics.IsMetricsSupported) + // do not register IConfigureOptions multiple times + if (!IsMeterFactoryRegistered(services)) { - // do not register IConfigureOptions multiple times - if (!IsMeterFactoryRegistered(services)) - { - services.AddMetrics(); - } - services.TryAddSingleton(); + services.AddMetrics(); } + services.TryAddSingleton(); + + return services; + } + + /// + /// Registers component rendering traces + /// + /// The . + /// The . + public static IServiceCollection AddRenderingTracing( + IServiceCollection services) + { + services.TryAddSingleton(); return services; } diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index f616da46b94f..1e4036042417 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -36,6 +36,7 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable private readonly ILogger _logger; private readonly ComponentFactory _componentFactory; private readonly RenderingMetrics? _renderingMetrics; + private readonly RenderingActivitySource? _renderingActivitySource; private Dictionary? _rootComponentsLatestParameters; private Task? _ongoingQuiescenceTask; @@ -93,17 +94,16 @@ 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); - if (RenderingMetrics.IsMetricsSupported) - { - _renderingMetrics = serviceProvider.GetService(); - } + _renderingMetrics = serviceProvider.GetService(); + _renderingActivitySource = serviceProvider.GetService(); ServiceProviderCascadingValueSuppliers = serviceProvider.GetService() is null ? Array.Empty() : serviceProvider.GetServices().ToArray(); } - internal RenderingMetrics? RenderingMetrics => RenderingMetrics.IsMetricsSupported ? _renderingMetrics : null; + internal RenderingMetrics? RenderingMetrics => _renderingMetrics; + internal RenderingActivitySource? RenderingActivitySource => _renderingActivitySource; internal ICascadingValueSupplier[] ServiceProviderCascadingValueSuppliers { get; } @@ -440,8 +440,6 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie { Dispatcher.AssertAccess(); - var eventStartTimestamp = RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled ? Stopwatch.GetTimestamp() : 0; - if (waitForQuiescence) { _pendingTasks ??= new(); @@ -449,6 +447,17 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie var (renderedByComponentId, callback, attributeName) = GetRequiredEventBindingEntry(eventHandlerId); + // collect trace + Activity? activity = null; + if (RenderingActivitySource != null) + { + var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; + var methodName = callback.Delegate.Method?.Name; + activity = RenderingActivitySource.StartEventActivity(receiverName, methodName, attributeName, null); + } + + var eventStartTimestamp = RenderingMetrics != null && RenderingMetrics.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. // The reason the following check is based on "which component rendered this frame" and not on "which component @@ -491,23 +500,36 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie task = callback.InvokeAsync(eventArgs); // collect metrics - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled) { var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; - RenderingMetrics.EventDurationSync(eventStartTimestamp, receiverName, attributeName); - _ = RenderingMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, attributeName); + var methodName = callback.Delegate.Method?.Name; + RenderingMetrics.EventDurationSync(eventStartTimestamp, receiverName, methodName, attributeName); + _ = RenderingMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, methodName, attributeName); } - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) { _ = RenderingMetrics.CaptureEventFailedAsync(task, callback, attributeName); } + + // stop activity/trace + if (RenderingActivitySource != null && activity != null) + { + _ = RenderingActivitySource.CaptureEventStopAsync(task, activity); + } } catch (Exception e) { - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) { RenderingMetrics.EventFailed(e.GetType().FullName, callback, attributeName); } + + if (RenderingActivitySource != null && activity != null) + { + RenderingActivitySource.FailEventActivity(activity, e); + } + HandleExceptionViaErrorBoundary(e, receiverComponentState); return Task.CompletedTask; } @@ -795,7 +817,7 @@ private void ProcessRenderQueue() _isBatchInProgress = true; var updateDisplayTask = Task.CompletedTask; - var batchStartTimestamp = RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var batchStartTimestamp = RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { @@ -828,18 +850,18 @@ private void ProcessRenderQueue() // if there is async work to be done. _ = InvokeRenderCompletedCalls(batch.UpdatedComponents, updateDisplayTask); - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled) { _renderingMetrics.BatchDuration(batchStartTimestamp, batch.UpdatedComponents.Count); } - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) { _ = _renderingMetrics.CaptureBatchFailedAsync(updateDisplayTask); } } catch (Exception e) { - if (RenderingMetrics.IsMetricsSupported && RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + if (RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) { _renderingMetrics.BatchFailed(e.GetType().Name); } diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 7b80404fb4d3..40ad68733248 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -53,7 +53,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions(); } - if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && (_renderer.RenderingMetrics.IsDiffDurationEnabled || _renderer.RenderingMetrics.IsStateDurationEnabled || _renderer.RenderingMetrics.IsStateExceptionEnabled)) + if (_renderer.RenderingMetrics != null && (_renderer.RenderingMetrics.IsDiffDurationEnabled || _renderer.RenderingMetrics.IsStateDurationEnabled || _renderer.RenderingMetrics.IsStateExceptionEnabled)) { _componentTypeName = component.GetType().FullName; } @@ -108,7 +108,7 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re _nextRenderTree.Clear(); - var diffStartTimestamp = RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var diffStartTimestamp = _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { renderFragment(_nextRenderTree); @@ -139,7 +139,7 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re batchBuilder.UpdatedComponentDiffs.Append(diff); batchBuilder.InvalidateParameterViews(); - if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled) + if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled) { _renderer.RenderingMetrics.DiffDuration(diffStartTimestamp, _componentTypeName, batchBuilder.EditsBuffer.Count - startCount); } @@ -249,24 +249,24 @@ private void SupplyCombinedParameters(ParameterView directAndCascadingParameters Task setParametersAsyncTask; try { - var stateStartTimestamp = RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var stateStartTimestamp = _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters); // collect metrics - if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled) + if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled) { _renderer.RenderingMetrics.ParametersDurationSync(stateStartTimestamp, _componentTypeName); _ = _renderer.RenderingMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName); } - if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) { _ = _renderer.RenderingMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); } } catch (Exception ex) { - if (RenderingMetrics.IsMetricsSupported && _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) { _renderer.RenderingMetrics.PropertiesFailed(ex.GetType().FullName, _componentTypeName); } diff --git a/src/Components/Components/src/Rendering/RenderingActivitySource.cs b/src/Components/Components/src/Rendering/RenderingActivitySource.cs new file mode 100644 index 000000000000..1471d3b7d722 --- /dev/null +++ b/src/Components/Components/src/Rendering/RenderingActivitySource.cs @@ -0,0 +1,55 @@ +// 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.Rendering; +internal class RenderingActivitySource +{ + internal const string Name = "Microsoft.AspNetCore.Components.Rendering"; + internal const string OnEventName = $"{Name}.OnEvent"; + + public ActivitySource ActivitySource { get; } = new ActivitySource(Name); + + + public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName, Activity? linkedActivity) + { + IEnumerable> tags = + [ + new("component.type", componentType ?? "unknown"), + new("component.method", methodName ?? "unknown"), + new("attribute.name", attributeName ?? "unknown"), + ]; + IEnumerable? links = (linkedActivity is not null) ? [new ActivityLink(linkedActivity.Context)] : null; + + var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); + if (activity is not null) + { + activity.DisplayName = $"{componentType ?? "unknown"}/{methodName ?? "unknown"}/{attributeName ?? "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/Rendering/RenderingMetrics.cs b/src/Components/Components/src/Rendering/RenderingMetrics.cs index 6c0f9f918053..9e95487c502e 100644 --- a/src/Components/Components/src/Rendering/RenderingMetrics.cs +++ b/src/Components/Components/src/Rendering/RenderingMetrics.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using Microsoft.AspNetCore.Http; @@ -26,29 +25,19 @@ internal sealed class RenderingMetrics : IDisposable private readonly Histogram _batchDuration; private readonly Counter _batchException; - [FeatureSwitchDefinition("System.Diagnostics.Metrics.Meter.IsSupported")] - public static bool IsMetricsSupported { get; } = InitializeIsMetricsSupported(); - private static bool InitializeIsMetricsSupported() => AppContext.TryGetSwitch("System.Diagnostics.Metrics.Meter.IsSupported", out bool isSupported) ? isSupported : true; + public bool IsEventDurationEnabled => _eventSyncDuration.Enabled || _eventAsyncDuration.Enabled; + public bool IsEventExceptionEnabled => _eventException.Enabled; - public bool IsEventDurationEnabled => IsMetricsSupported && (_eventSyncDuration.Enabled || _eventAsyncDuration.Enabled); - public bool IsEventExceptionEnabled => IsMetricsSupported && _eventException.Enabled; + public bool IsStateDurationEnabled => _parametersSyncDuration.Enabled || _parametersAsyncDuration.Enabled; + public bool IsStateExceptionEnabled => _parametersException.Enabled; - public bool IsStateDurationEnabled => IsMetricsSupported && (_parametersSyncDuration.Enabled || _parametersAsyncDuration.Enabled); - public bool IsStateExceptionEnabled => IsMetricsSupported && _parametersException.Enabled; + public bool IsDiffDurationEnabled => _diffDuration.Enabled; - public bool IsDiffDurationEnabled => IsMetricsSupported && _diffDuration.Enabled; - - public bool IsBatchDurationEnabled => IsMetricsSupported && _batchDuration.Enabled; - public bool IsBatchExceptionEnabled => IsMetricsSupported && _batchException.Enabled; + public bool IsBatchDurationEnabled => _batchDuration.Enabled; + public bool IsBatchExceptionEnabled => _batchException.Enabled; public RenderingMetrics(IMeterFactory meterFactory) { - if (!IsMetricsSupported) - { - // TryAddSingleton prevents trimming constructors, so we trim constructor this way - throw new NotSupportedException("Metrics are not supported in this environment."); - } - Debug.Assert(meterFactory != null); _meter = meterFactory.Create(MeterName); @@ -105,11 +94,12 @@ public RenderingMetrics(IMeterFactory meterFactory) description: "Total number of exceptions during batch rendering."); } - public void EventDurationSync(long startTimestamp, string? componentType, string? attributeName) + public void EventDurationSync(long startTimestamp, string? componentType, string? methodName, string? attributeName) { var tags = new TagList { { "component.type", componentType ?? "unknown" }, + { "component.method", methodName ?? "unknown" }, { "attribute.name", attributeName ?? "unknown"} }; @@ -117,7 +107,7 @@ public void EventDurationSync(long startTimestamp, string? componentType, string _eventSyncDuration.Record(duration.TotalSeconds, tags); } - public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? attributeName) + public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName) { try { @@ -126,6 +116,7 @@ public async Task CaptureEventDurationAsync(Task task, long startTimestamp, stri var tags = new TagList { { "component.type", componentType ?? "unknown" }, + { "component.method", methodName ?? "unknown" }, { "attribute.name", attributeName ?? "unknown" } }; diff --git a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs index be4133ac9a9f..f1d7762d11ff 100644 --- a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs +++ b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs @@ -43,7 +43,7 @@ public void EventDurationSync_RecordsDuration() // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - renderingMetrics.EventDurationSync(startTime, "TestComponent", "OnClick"); + renderingMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); // Assert var measurements = eventSyncDurationCollector.GetMeasurementSnapshot(); @@ -65,7 +65,7 @@ public async Task CaptureEventDurationAsync_RecordsDuration() // Act var startTime = Stopwatch.GetTimestamp(); var task = Task.Delay(10); // Create a delay task - await renderingMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "OnClickAsync"); + await renderingMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "MyMethod", "OnClickAsync"); // Assert var measurements = eventAsyncDurationCollector.GetMeasurementSnapshot(); @@ -74,6 +74,7 @@ public async Task CaptureEventDurationAsync_RecordsDuration() 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] @@ -386,7 +387,7 @@ public void Dispose_DisposesUnderlyingMeter() // Try to use the disposed meter - this should not throw since TestMeterFactory // doesn't actually dispose the meter in test contexts var startTime = Stopwatch.GetTimestamp(); - renderingMetrics.EventDurationSync(startTime, "TestComponent", "OnClick"); + renderingMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); } // Helper class for mock components diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 249ee69fccd3..4618ab6dc7b5 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -77,6 +77,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddScoped(); RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(services); + RenderingMetricsServiceCollectionExtensions.AddRenderingTracing(services); // Form handling services.AddSupplyValueFromFormProvider(); From 4c7549527014d691693ef12275ad34e8e7dcdfb7 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 13:09:19 +0200 Subject: [PATCH 04/26] whitespace --- .../Components/src/Rendering/RenderingActivitySource.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Components/src/Rendering/RenderingActivitySource.cs b/src/Components/Components/src/Rendering/RenderingActivitySource.cs index 1471d3b7d722..af83cb7e4381 100644 --- a/src/Components/Components/src/Rendering/RenderingActivitySource.cs +++ b/src/Components/Components/src/Rendering/RenderingActivitySource.cs @@ -11,7 +11,6 @@ internal class RenderingActivitySource public ActivitySource ActivitySource { get; } = new ActivitySource(Name); - public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName, Activity? linkedActivity) { IEnumerable> tags = From 0f3d48a4d6c30cf3da7d7d00b90edf94ab4365a6 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 18:35:42 +0200 Subject: [PATCH 05/26] navigation draft --- .../src/ComponentsActivitySource.cs | 102 ++++++++++++ ...nderingMetrics.cs => ComponentsMetrics.cs} | 41 +++-- .../Components/src/PublicAPI.Unshipped.txt | 6 +- ...eringMetricsServiceCollectionExtensions.cs | 11 +- src/Components/Components/src/RenderHandle.cs | 3 + .../Components/src/RenderTree/Renderer.cs | 55 +++---- .../src/Rendering/ComponentState.cs | 24 +-- .../src/Rendering/RenderingActivitySource.cs | 54 ------- .../Components/src/Routing/Router.cs | 17 ++ .../test/Rendering/RenderingMetricsTest.cs | 148 +++++++++--------- ...orComponentsServiceCollectionExtensions.cs | 4 +- .../src/RazorComponentEndpointInvoker.cs | 2 + 12 files changed, 279 insertions(+), 188 deletions(-) create mode 100644 src/Components/Components/src/ComponentsActivitySource.cs rename src/Components/Components/src/{Rendering/RenderingMetrics.cs => ComponentsMetrics.cs} (88%) delete mode 100644 src/Components/Components/src/Rendering/RenderingActivitySource.cs diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs new file mode 100644 index 000000000000..2a314760cf86 --- /dev/null +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -0,0 +1,102 @@ +// 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 OnNavigationName = $"{Name}.OnNavigation"; + + public static ActivitySource ActivitySource { get; } = new ActivitySource(Name); + + private Activity? _routeActivity; + + public void StartRouteActivity(string componentType, string route) + { + StopRouteActivity(); + + IEnumerable> tags = + [ + new("component.type", componentType ?? "unknown"), + new("route", route ?? "unknown"), + ]; + var parentActivity = Activity.Current; + IEnumerable? links = parentActivity is not null ? [new ActivityLink(parentActivity.Context)] : null; + + var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); + if (activity is not null) + { + activity.DisplayName = $"NAVIGATE {route ?? "unknown"} -> {componentType ?? "unknown"}"; + activity.Start(); + _routeActivity = activity; + } + } + + public void StopRouteActivity() + { + if (_routeActivity != null) + { + _routeActivity.Stop(); + _routeActivity = null; + return; + } + } + + public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName) + { + IEnumerable> tags = + [ + new("component.type", componentType ?? "unknown"), + new("component.method", methodName ?? "unknown"), + new("attribute.name", attributeName ?? "unknown"), + ]; + List? links = new List(); + var parentActivity = Activity.Current; + if (parentActivity is not null) + { + links.Add(new ActivityLink(parentActivity.Context)); + } + if (_routeActivity is not null) + { + links.Add(new ActivityLink(_routeActivity.Context)); + } + + var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); + if (activity is not null) + { + 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/Rendering/RenderingMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs similarity index 88% rename from src/Components/Components/src/Rendering/RenderingMetrics.cs rename to src/Components/Components/src/ComponentsMetrics.cs index 9e95487c502e..c9f5bafe1462 100644 --- a/src/Components/Components/src/Rendering/RenderingMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -3,15 +3,18 @@ using System.Diagnostics; using System.Diagnostics.Metrics; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Components.Rendering; +namespace Microsoft.AspNetCore.Components; -internal sealed class RenderingMetrics : IDisposable +internal sealed class ComponentsMetrics : IDisposable { - public const string MeterName = "Microsoft.AspNetCore.Components.Rendering"; + public const string MeterName = "Microsoft.AspNetCore.Components"; private readonly Meter _meter; + private readonly Counter _navigationCount; + private readonly Histogram _eventSyncDuration; private readonly Histogram _eventAsyncDuration; private readonly Counter _eventException; @@ -25,6 +28,8 @@ internal sealed class RenderingMetrics : IDisposable private readonly Histogram _batchDuration; private readonly Counter _batchException; + public bool IsNavigationEnabled => _navigationCount.Enabled; + public bool IsEventDurationEnabled => _eventSyncDuration.Enabled || _eventAsyncDuration.Enabled; public bool IsEventExceptionEnabled => _eventException.Enabled; @@ -36,43 +41,48 @@ internal sealed class RenderingMetrics : IDisposable public bool IsBatchDurationEnabled => _batchDuration.Enabled; public bool IsBatchExceptionEnabled => _batchException.Enabled; - public RenderingMetrics(IMeterFactory meterFactory) + 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."); + _eventSyncDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.event.synchronous.duration", + "aspnetcore.components.event.synchronous.duration", unit: "s", description: "Duration of processing browser event synchronously.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _eventAsyncDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.event.asynchronous.duration", + "aspnetcore.components.event.asynchronous.duration", unit: "s", description: "Duration of processing browser event asynchronously.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _eventException = _meter.CreateCounter( - "aspnetcore.components.rendering.event.exception", + "aspnetcore.components.event.exception", unit: "{exceptions}", description: "Total number of exceptions during browser event processing."); _parametersSyncDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.parameters.synchronous.duration", + "aspnetcore.components.parameters.synchronous.duration", unit: "s", description: "Duration of processing component parameters synchronously.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _parametersAsyncDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.parameters.asynchronous.duration", + "aspnetcore.components.parameters.asynchronous.duration", unit: "s", description: "Duration of processing component parameters asynchronously.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _parametersException = _meter.CreateCounter( - "aspnetcore.components.rendering.parameters.exception", + "aspnetcore.components.parameters.exception", unit: "{exceptions}", description: "Total number of exceptions during processing component parameters."); @@ -94,6 +104,17 @@ public RenderingMetrics(IMeterFactory meterFactory) 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 void EventDurationSync(long startTimestamp, string? componentType, string? methodName, string? attributeName) { var tags = new TagList diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index c372bbf750c2..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.RenderingMetricsServiceCollectionExtensions +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,7 +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.RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.AspNetCore.Components.Infrastructure.RenderingMetricsServiceCollectionExtensions.AddRenderingTracing(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> 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 index b40b3adf4de3..e1e224e5f71b 100644 --- a/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs +++ b/src/Components/Components/src/RegisterRenderingMetricsServiceCollectionExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.Metrics; -using Microsoft.AspNetCore.Components.Rendering; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -11,14 +10,14 @@ namespace Microsoft.AspNetCore.Components.Infrastructure; /// /// Infrastructure APIs for registering diagnostic metrics. /// -public static class RenderingMetricsServiceCollectionExtensions +public static class ComponentsMetricsServiceCollectionExtensions { /// /// Registers component rendering metrics /// /// The . /// The . - public static IServiceCollection AddRenderingMetrics( + public static IServiceCollection AddComponentsMetrics( IServiceCollection services) { // do not register IConfigureOptions multiple times @@ -26,7 +25,7 @@ public static IServiceCollection AddRenderingMetrics( { services.AddMetrics(); } - services.TryAddSingleton(); + services.TryAddSingleton(); return services; } @@ -36,10 +35,10 @@ public static IServiceCollection AddRenderingMetrics( /// /// The . /// The . - public static IServiceCollection AddRenderingTracing( + public static IServiceCollection AddComponentsTracing( IServiceCollection services) { - services.TryAddSingleton(); + services.TryAddScoped(); return services; } 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 1e4036042417..baf363e620ae 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -35,8 +35,9 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable private readonly Dictionary _eventHandlerIdReplacements = new Dictionary(); private readonly ILogger _logger; private readonly ComponentFactory _componentFactory; - private readonly RenderingMetrics? _renderingMetrics; - private readonly RenderingActivitySource? _renderingActivitySource; + private readonly ComponentsMetrics? _componentsMetrics; + private readonly ComponentsActivitySource? _componentsActivitySource; + private Dictionary? _rootComponentsLatestParameters; private Task? _ongoingQuiescenceTask; @@ -94,16 +95,16 @@ 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); - _renderingMetrics = serviceProvider.GetService(); - _renderingActivitySource = serviceProvider.GetService(); + _componentsMetrics = serviceProvider.GetService(); + _componentsActivitySource = serviceProvider.GetService(); ServiceProviderCascadingValueSuppliers = serviceProvider.GetService() is null ? Array.Empty() : serviceProvider.GetServices().ToArray(); } - internal RenderingMetrics? RenderingMetrics => _renderingMetrics; - internal RenderingActivitySource? RenderingActivitySource => _renderingActivitySource; + internal ComponentsMetrics? ComponentMetrics => _componentsMetrics; + internal ComponentsActivitySource? ComponentActivitySource => _componentsActivitySource; internal ICascadingValueSupplier[] ServiceProviderCascadingValueSuppliers { get; } @@ -449,14 +450,14 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie // collect trace Activity? activity = null; - if (RenderingActivitySource != null) + if (ComponentActivitySource != null) { var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; var methodName = callback.Delegate.Method?.Name; - activity = RenderingActivitySource.StartEventActivity(receiverName, methodName, attributeName, null); + activity = ComponentActivitySource.StartEventActivity(receiverName, methodName, attributeName); } - var eventStartTimestamp = RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled ? Stopwatch.GetTimestamp() : 0; + 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. @@ -500,34 +501,34 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie task = callback.InvokeAsync(eventArgs); // collect metrics - if (RenderingMetrics != null && RenderingMetrics.IsEventDurationEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsEventDurationEnabled) { var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; var methodName = callback.Delegate.Method?.Name; - RenderingMetrics.EventDurationSync(eventStartTimestamp, receiverName, methodName, attributeName); - _ = RenderingMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, methodName, attributeName); + ComponentMetrics.EventDurationSync(eventStartTimestamp, receiverName, methodName, attributeName); + _ = ComponentMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, methodName, attributeName); } - if (RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsEventExceptionEnabled) { - _ = RenderingMetrics.CaptureEventFailedAsync(task, callback, attributeName); + _ = ComponentMetrics.CaptureEventFailedAsync(task, callback, attributeName); } // stop activity/trace - if (RenderingActivitySource != null && activity != null) + if (ComponentActivitySource != null && activity != null) { - _ = RenderingActivitySource.CaptureEventStopAsync(task, activity); + _ = ComponentsActivitySource.CaptureEventStopAsync(task, activity); } } catch (Exception e) { - if (RenderingMetrics != null && RenderingMetrics.IsEventExceptionEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsEventExceptionEnabled) { - RenderingMetrics.EventFailed(e.GetType().FullName, callback, attributeName); + ComponentMetrics.EventFailed(e.GetType().FullName, callback, attributeName); } - if (RenderingActivitySource != null && activity != null) + if (ComponentActivitySource != null && activity != null) { - RenderingActivitySource.FailEventActivity(activity, e); + ComponentsActivitySource.FailEventActivity(activity, e); } HandleExceptionViaErrorBoundary(e, receiverComponentState); @@ -817,7 +818,7 @@ private void ProcessRenderQueue() _isBatchInProgress = true; var updateDisplayTask = Task.CompletedTask; - var batchStartTimestamp = RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var batchStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { @@ -850,20 +851,20 @@ private void ProcessRenderQueue() // if there is async work to be done. _ = InvokeRenderCompletedCalls(batch.UpdatedComponents, updateDisplayTask); - if (RenderingMetrics != null && RenderingMetrics.IsBatchDurationEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsBatchDurationEnabled) { - _renderingMetrics.BatchDuration(batchStartTimestamp, batch.UpdatedComponents.Count); + ComponentMetrics.BatchDuration(batchStartTimestamp, batch.UpdatedComponents.Count); } - if (RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsBatchExceptionEnabled) { - _ = _renderingMetrics.CaptureBatchFailedAsync(updateDisplayTask); + _ = ComponentMetrics.CaptureBatchFailedAsync(updateDisplayTask); } } catch (Exception e) { - if (RenderingMetrics != null && RenderingMetrics.IsBatchExceptionEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsBatchExceptionEnabled) { - _renderingMetrics.BatchFailed(e.GetType().Name); + ComponentMetrics.BatchFailed(e.GetType().Name); } // Ensure we catch errors while running the render functions of the components. diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 40ad68733248..201c74e10019 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -53,7 +53,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions(); } - if (_renderer.RenderingMetrics != null && (_renderer.RenderingMetrics.IsDiffDurationEnabled || _renderer.RenderingMetrics.IsStateDurationEnabled || _renderer.RenderingMetrics.IsStateExceptionEnabled)) + if (_renderer.ComponentMetrics != null && (_renderer.ComponentMetrics.IsDiffDurationEnabled || _renderer.ComponentMetrics.IsStateDurationEnabled || _renderer.ComponentMetrics.IsStateExceptionEnabled)) { _componentTypeName = component.GetType().FullName; } @@ -108,7 +108,7 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re _nextRenderTree.Clear(); - var diffStartTimestamp = _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var diffStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { renderFragment(_nextRenderTree); @@ -139,9 +139,9 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re batchBuilder.UpdatedComponentDiffs.Append(diff); batchBuilder.InvalidateParameterViews(); - if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsDiffDurationEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsDiffDurationEnabled) { - _renderer.RenderingMetrics.DiffDuration(diffStartTimestamp, _componentTypeName, batchBuilder.EditsBuffer.Count - startCount); + _renderer.ComponentMetrics.DiffDuration(diffStartTimestamp, _componentTypeName, batchBuilder.EditsBuffer.Count - startCount); } } @@ -249,26 +249,26 @@ private void SupplyCombinedParameters(ParameterView directAndCascadingParameters Task setParametersAsyncTask; try { - var stateStartTimestamp = _renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var stateStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters); // collect metrics - if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateDurationEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateDurationEnabled) { - _renderer.RenderingMetrics.ParametersDurationSync(stateStartTimestamp, _componentTypeName); - _ = _renderer.RenderingMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName); + _renderer.ComponentMetrics.ParametersDurationSync(stateStartTimestamp, _componentTypeName); + _ = _renderer.ComponentMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName); } - if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateExceptionEnabled) { - _ = _renderer.RenderingMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); + _ = _renderer.ComponentMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); } } catch (Exception ex) { - if (_renderer.RenderingMetrics != null && _renderer.RenderingMetrics.IsStateExceptionEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateExceptionEnabled) { - _renderer.RenderingMetrics.PropertiesFailed(ex.GetType().FullName, _componentTypeName); + _renderer.ComponentMetrics.PropertiesFailed(ex.GetType().FullName, _componentTypeName); } setParametersAsyncTask = Task.FromException(ex); diff --git a/src/Components/Components/src/Rendering/RenderingActivitySource.cs b/src/Components/Components/src/Rendering/RenderingActivitySource.cs deleted file mode 100644 index af83cb7e4381..000000000000 --- a/src/Components/Components/src/Rendering/RenderingActivitySource.cs +++ /dev/null @@ -1,54 +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; - -namespace Microsoft.AspNetCore.Components.Rendering; -internal class RenderingActivitySource -{ - internal const string Name = "Microsoft.AspNetCore.Components.Rendering"; - internal const string OnEventName = $"{Name}.OnEvent"; - - public ActivitySource ActivitySource { get; } = new ActivitySource(Name); - - public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName, Activity? linkedActivity) - { - IEnumerable> tags = - [ - new("component.type", componentType ?? "unknown"), - new("component.method", methodName ?? "unknown"), - new("attribute.name", attributeName ?? "unknown"), - ]; - IEnumerable? links = (linkedActivity is not null) ? [new ActivityLink(linkedActivity.Context)] : null; - - var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); - if (activity is not null) - { - activity.DisplayName = $"{componentType ?? "unknown"}/{methodName ?? "unknown"}/{attributeName ?? "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/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index d562b94bb639..8aef8f4fcba7 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics; namespace Microsoft.AspNetCore.Components.Routing; @@ -226,6 +227,8 @@ internal virtual void Refresh(bool isNavigationIntercepted) // In order to avoid routing twice we check for RouteData if (RoutingStateProvider?.RouteData is { } endpointRouteData) { + 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 @@ -252,6 +255,8 @@ internal virtual void Refresh(bool isNavigationIntercepted) $"does not implement {typeof(IComponent).FullName}."); } + RecordDiagnostics(context.Handler.FullName, context.Entry.RoutePattern.RawText); + Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri); var routeData = new RouteData( @@ -286,6 +291,16 @@ internal virtual void Refresh(bool isNavigationIntercepted) } } + private void RecordDiagnostics(string componentType, string template) + { + _renderHandle.ComponentActivitySource?.StartRouteActivity(componentType, template); + + if (_renderHandle.ComponentMetrics != null && _renderHandle.ComponentMetrics.IsNavigationEnabled) + { + _renderHandle.ComponentMetrics.Navigation(componentType, template); + } + } + private static void DefaultNotFoundContent(RenderTreeBuilder builder) { // This output can't use any layout (none is specified), and it can't use any web-specific concepts @@ -340,6 +355,8 @@ internal async ValueTask RunOnNavigateAsync(string path, bool isNavigationInterc private void OnLocationChanged(object sender, LocationChangedEventArgs args) { + _renderHandle.ComponentActivitySource?.StopRouteActivity(); + _locationAbsolute = args.Location; if (_renderHandle.IsInitialized && Routes != null) { diff --git a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs index f1d7762d11ff..c750ebec5ce4 100644 --- a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs +++ b/src/Components/Components/test/Rendering/RenderingMetricsTest.cs @@ -12,11 +12,11 @@ namespace Microsoft.AspNetCore.Components.Rendering; -public class RenderingMetricsTest +public class ComponentsMetricsTest { private readonly TestMeterFactory _meterFactory; - public RenderingMetricsTest() + public ComponentsMetricsTest() { _meterFactory = new TestMeterFactory(); } @@ -25,25 +25,25 @@ public RenderingMetricsTest() public void Constructor_CreatesMetersCorrectly() { // Arrange & Act - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); // Assert Assert.Single(_meterFactory.Meters); - Assert.Equal(RenderingMetrics.MeterName, _meterFactory.Meters[0].Name); + Assert.Equal(ComponentsMetrics.MeterName, _meterFactory.Meters[0].Name); } [Fact] public void EventDurationSync_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventSyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - renderingMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); + componentsMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); // Assert var measurements = eventSyncDurationCollector.GetMeasurementSnapshot(); @@ -58,14 +58,14 @@ public void EventDurationSync_RecordsDuration() public async Task CaptureEventDurationAsync_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); // Act var startTime = Stopwatch.GetTimestamp(); var task = Task.Delay(10); // Create a delay task - await renderingMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "MyMethod", "OnClickAsync"); + await componentsMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "MyMethod", "OnClickAsync"); // Assert var measurements = eventAsyncDurationCollector.GetMeasurementSnapshot(); @@ -81,14 +81,14 @@ public async Task CaptureEventDurationAsync_RecordsDuration() public void ParametersDurationSync_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - renderingMetrics.ParametersDurationSync(startTime, "TestComponent"); + componentsMetrics.ParametersDurationSync(startTime, "TestComponent"); // Assert var measurements = parametersSyncDurationCollector.GetMeasurementSnapshot(); @@ -102,14 +102,14 @@ public void ParametersDurationSync_RecordsDuration() public async Task CaptureParametersDurationAsync_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); // Act var startTime = Stopwatch.GetTimestamp(); var task = Task.Delay(10); // Create a delay task - await renderingMetrics.CaptureParametersDurationAsync(task, startTime, "TestComponent"); + await componentsMetrics.CaptureParametersDurationAsync(task, startTime, "TestComponent"); // Assert var measurements = parametersAsyncDurationCollector.GetMeasurementSnapshot(); @@ -123,14 +123,14 @@ public async Task CaptureParametersDurationAsync_RecordsDuration() public void DiffDuration_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var diffDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - renderingMetrics.DiffDuration(startTime, "TestComponent", 5); + componentsMetrics.DiffDuration(startTime, "TestComponent", 5); // Assert var measurements = diffDurationCollector.GetMeasurementSnapshot(); @@ -145,14 +145,14 @@ public void DiffDuration_RecordsDuration() public void BatchDuration_RecordsDuration() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); // Act var startTime = Stopwatch.GetTimestamp(); Thread.Sleep(10); // Add a small delay to ensure a measurable duration - renderingMetrics.BatchDuration(startTime, 50); + componentsMetrics.BatchDuration(startTime, 50); // Assert var measurements = batchDurationCollector.GetMeasurementSnapshot(); @@ -166,15 +166,15 @@ public void BatchDuration_RecordsDuration() public void EventFailed_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); // Create a mock EventCallback var callback = new EventCallback(new TestComponent(), (Action)(() => { })); // Act - renderingMetrics.EventFailed("ArgumentException", callback, "OnClick"); + componentsMetrics.EventFailed("ArgumentException", callback, "OnClick"); // Assert var measurements = eventExceptionCollector.GetMeasurementSnapshot(); @@ -183,16 +183,16 @@ public void EventFailed_RecordsException() 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.RenderingMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); + Assert.Contains("Microsoft.AspNetCore.Components.Rendering.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); } [Fact] public async Task CaptureEventFailedAsync_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); // Create a mock EventCallback var callback = new EventCallback(new TestComponent(), (Action)(() => { })); @@ -201,7 +201,7 @@ public async Task CaptureEventFailedAsync_RecordsException() var task = Task.FromException(new InvalidOperationException()); // Act - await renderingMetrics.CaptureEventFailedAsync(task, callback, "OnClickAsync"); + await componentsMetrics.CaptureEventFailedAsync(task, callback, "OnClickAsync"); // Assert var measurements = eventExceptionCollector.GetMeasurementSnapshot(); @@ -210,19 +210,19 @@ public async Task CaptureEventFailedAsync_RecordsException() 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.RenderingMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); + Assert.Contains("Microsoft.AspNetCore.Components.Rendering.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); } [Fact] public void PropertiesFailed_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); // Act - renderingMetrics.PropertiesFailed("ArgumentException", "TestComponent"); + componentsMetrics.PropertiesFailed("ArgumentException", "TestComponent"); // Assert var measurements = parametersExceptionCollector.GetMeasurementSnapshot(); @@ -237,15 +237,15 @@ public void PropertiesFailed_RecordsException() public async Task CapturePropertiesFailedAsync_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); // Create a task that throws an exception var task = Task.FromException(new InvalidOperationException()); // Act - await renderingMetrics.CapturePropertiesFailedAsync(task, "TestComponent"); + await componentsMetrics.CapturePropertiesFailedAsync(task, "TestComponent"); // Assert var measurements = parametersExceptionCollector.GetMeasurementSnapshot(); @@ -260,12 +260,12 @@ public async Task CapturePropertiesFailedAsync_RecordsException() public void BatchFailed_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); // Act - renderingMetrics.BatchFailed("ArgumentException"); + componentsMetrics.BatchFailed("ArgumentException"); // Assert var measurements = batchExceptionCollector.GetMeasurementSnapshot(); @@ -279,15 +279,15 @@ public void BatchFailed_RecordsException() public async Task CaptureBatchFailedAsync_RecordsException() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); // Create a task that throws an exception var task = Task.FromException(new InvalidOperationException()); // Act - await renderingMetrics.CaptureBatchFailedAsync(task); + await componentsMetrics.CaptureBatchFailedAsync(task); // Assert var measurements = batchExceptionCollector.GetMeasurementSnapshot(); @@ -301,60 +301,60 @@ public async Task CaptureBatchFailedAsync_RecordsException() public void EnabledProperties_ReflectMeterState() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); // Create collectors to ensure the meters are enabled using var eventSyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); using var eventExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); using var diffDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); using var batchDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); using var batchExceptionCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); // Assert - Assert.True(renderingMetrics.IsEventDurationEnabled); - Assert.True(renderingMetrics.IsEventExceptionEnabled); - Assert.True(renderingMetrics.IsStateDurationEnabled); - Assert.True(renderingMetrics.IsStateExceptionEnabled); - Assert.True(renderingMetrics.IsDiffDurationEnabled); - Assert.True(renderingMetrics.IsBatchDurationEnabled); - Assert.True(renderingMetrics.IsBatchExceptionEnabled); + Assert.True(componentsMetrics.IsEventDurationEnabled); + Assert.True(componentsMetrics.IsEventExceptionEnabled); + Assert.True(componentsMetrics.IsStateDurationEnabled); + Assert.True(componentsMetrics.IsStateExceptionEnabled); + Assert.True(componentsMetrics.IsDiffDurationEnabled); + Assert.True(componentsMetrics.IsBatchDurationEnabled); + Assert.True(componentsMetrics.IsBatchExceptionEnabled); } [Fact] public void BucketEditLength_ReturnsCorrectBucket() { // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); using var diffDurationCollector = new MetricCollector(_meterFactory, - RenderingMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); // Act & Assert - Test different diff lengths var startTime = Stopwatch.GetTimestamp(); // Test each bucket boundary - renderingMetrics.DiffDuration(startTime, "Component", 1); - renderingMetrics.DiffDuration(startTime, "Component", 2); - renderingMetrics.DiffDuration(startTime, "Component", 5); - renderingMetrics.DiffDuration(startTime, "Component", 10); - renderingMetrics.DiffDuration(startTime, "Component", 50); - renderingMetrics.DiffDuration(startTime, "Component", 100); - renderingMetrics.DiffDuration(startTime, "Component", 500); - renderingMetrics.DiffDuration(startTime, "Component", 1000); - renderingMetrics.DiffDuration(startTime, "Component", 10000); - renderingMetrics.DiffDuration(startTime, "Component", 20000); // Should be 10001 + componentsMetrics.DiffDuration(startTime, "Component", 1); + componentsMetrics.DiffDuration(startTime, "Component", 2); + componentsMetrics.DiffDuration(startTime, "Component", 5); + componentsMetrics.DiffDuration(startTime, "Component", 10); + componentsMetrics.DiffDuration(startTime, "Component", 50); + componentsMetrics.DiffDuration(startTime, "Component", 100); + componentsMetrics.DiffDuration(startTime, "Component", 500); + componentsMetrics.DiffDuration(startTime, "Component", 1000); + componentsMetrics.DiffDuration(startTime, "Component", 10000); + componentsMetrics.DiffDuration(startTime, "Component", 20000); // Should be 10001 // Assert var measurements = diffDurationCollector.GetMeasurementSnapshot(); @@ -379,15 +379,15 @@ public void Dispose_DisposesUnderlyingMeter() // This is a bit tricky to test directly, so we'll use an indirect approach // Arrange - var renderingMetrics = new RenderingMetrics(_meterFactory); + var componentsMetrics = new ComponentsMetrics(_meterFactory); // Act - renderingMetrics.Dispose(); + componentsMetrics.Dispose(); // Try to use the disposed meter - this should not throw since TestMeterFactory // doesn't actually dispose the meter in test contexts var startTime = Stopwatch.GetTimestamp(); - renderingMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); + componentsMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); } // Helper class for mock components diff --git a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs index 4618ab6dc7b5..de2e1f4707e0 100644 --- a/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs +++ b/src/Components/Endpoints/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs @@ -76,8 +76,8 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection services.TryAddScoped(); - RenderingMetricsServiceCollectionExtensions.AddRenderingMetrics(services); - RenderingMetricsServiceCollectionExtensions.AddRenderingTracing(services); + ComponentsMetricsServiceCollectionExtensions.AddComponentsMetrics(services); + ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(services); // Form handling services.AddSupplyValueFromFormProvider(); diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 245f811d7f76..93c5579e6290 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -30,6 +30,8 @@ public RazorComponentEndpointInvoker(EndpointHtmlRenderer renderer, ILogger RenderComponentCore(context)); } From cf15c8d65a5b96c64564eb4106feee3f1df8d740 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 18:47:44 +0200 Subject: [PATCH 06/26] cleanup --- src/Components/Components/src/Routing/Router.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 8aef8f4fcba7..8c4d5069b60d 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; -using System.Diagnostics; namespace Microsoft.AspNetCore.Components.Routing; From 550f633d81c860b6ceff21d0f18a34ab920f7bc4 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 24 Apr 2025 19:05:50 +0200 Subject: [PATCH 07/26] cleanup --- src/Components/Components/src/RenderTree/Renderer.cs | 4 ---- src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs | 2 -- 2 files changed, 6 deletions(-) diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index baf363e620ae..8ffc1369f684 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -541,10 +541,6 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie // Since the task has yielded - process any queued rendering work before we return control // to the caller. ProcessPendingRender(); - - //callback.Receiver - //callback.Delegate.Method. - } // Task completed synchronously or is still running. We already processed all of the rendering diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 93c5579e6290..245f811d7f76 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -30,8 +30,6 @@ public RazorComponentEndpointInvoker(EndpointHtmlRenderer renderer, ILogger RenderComponentCore(context)); } From 9ab8a8481f0c16925d8d1db762cfb2c1e709507f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 25 Apr 2025 19:40:20 +0200 Subject: [PATCH 08/26] more --- .../src/ComponentsActivitySource.cs | 109 ++++++++++--- .../Components/src/ComponentsMetrics.cs | 86 ++-------- .../Microsoft.AspNetCore.Components.csproj | 1 + .../Components/src/RenderTree/Renderer.cs | 1 - .../src/Rendering/ComponentState.cs | 19 +-- .../Components/src/Routing/Router.cs | 28 +++- ...etricsTest.cs => ComponentsMetricsTest.cs} | 153 ++---------------- .../Server/src/Circuits/CircuitFactory.cs | 2 + .../Server/src/Circuits/CircuitHost.cs | 13 +- .../Server/src/Circuits/RemoteRenderer.cs | 10 +- src/Components/Server/src/ComponentHub.cs | 5 +- .../Server/test/Circuits/CircuitHostTest.cs | 6 +- .../Server/test/Circuits/TestCircuitHost.cs | 7 +- 13 files changed, 170 insertions(+), 270 deletions(-) rename src/Components/Components/test/Rendering/{RenderingMetricsTest.cs => ComponentsMetricsTest.cs} (62%) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 2a314760cf86..086de83edefc 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -12,40 +12,103 @@ internal class ComponentsActivitySource { internal const string Name = "Microsoft.AspNetCore.Components"; internal const string OnEventName = $"{Name}.OnEvent"; - internal const string OnNavigationName = $"{Name}.OnNavigation"; + internal const string OnRouteName = $"{Name}.OnRoute"; - public static ActivitySource ActivitySource { get; } = new ActivitySource(Name); + private ActivityContext _httpContext; + private ActivityContext _circuitContext; + private string? _circuitId; + private ActivityContext _routeContext; - private Activity? _routeActivity; + private ActivitySource ActivitySource { get; } = new ActivitySource(Name); - public void StartRouteActivity(string componentType, string route) + public static ActivityContext CaptureHttpContext() { - StopRouteActivity(); + var parentActivity = Activity.Current; + if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn") + { + return parentActivity.Context; + } + return default; + } + + public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext) + { + _circuitId = circuitId; + IEnumerable> tags = + [ + new("circuit.id", _circuitId ?? "unknown"), + ]; + + var links = new List(); + if (httpContext != default) + { + _httpContext = httpContext; + links.Add(new ActivityLink(httpContext)); + } + + var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId:null, tags, links); + if (activity is not null) + { + activity.DisplayName = $"CIRCUIT {circuitId ?? "unknown"}"; + activity.Start(); + _circuitContext = activity.Context; + + Console.WriteLine($"StartCircuitActivity: {circuitId}"); + Console.WriteLine($"circuitContext: {_circuitContext.TraceId} {_circuitContext.SpanId} {_circuitContext.TraceState}"); + Console.WriteLine($"httpContext: {httpContext.TraceId} {httpContext.SpanId} {httpContext.TraceState}"); + } + 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) + { IEnumerable> tags = [ + new("circuit.id", _circuitId ?? "unknown"), new("component.type", componentType ?? "unknown"), new("route", route ?? "unknown"), ]; - var parentActivity = Activity.Current; - IEnumerable? links = parentActivity is not null ? [new ActivityLink(parentActivity.Context)] : null; + var links = new List(); + if (_httpContext == default) + { + _httpContext = CaptureHttpContext(); + } + if (_httpContext != default) + { + links.Add(new ActivityLink(_httpContext)); + } + if (_circuitContext != default) + { + links.Add(new ActivityLink(_circuitContext)); + } - var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); + var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, tags, links); if (activity is not null) { - activity.DisplayName = $"NAVIGATE {route ?? "unknown"} -> {componentType ?? "unknown"}"; + _routeContext = activity.Context; + activity.DisplayName = $"ROUTE {route ?? "unknown"} -> {componentType ?? "unknown"}"; activity.Start(); - _routeActivity = activity; } + return activity; } - public void StopRouteActivity() + public void StopRouteActivity(Activity activity) { - if (_routeActivity != null) + _routeContext = default; + if (!activity.IsStopped) { - _routeActivity.Stop(); - _routeActivity = null; - return; + activity.Stop(); } } @@ -53,19 +116,23 @@ public void StopRouteActivity() { IEnumerable> tags = [ + new("circuit.id", _circuitId ?? "unknown"), new("component.type", componentType ?? "unknown"), new("component.method", methodName ?? "unknown"), new("attribute.name", attributeName ?? "unknown"), ]; - List? links = new List(); - var parentActivity = Activity.Current; - if (parentActivity is not null) + var links = new List(); + if (_httpContext != default) + { + links.Add(new ActivityLink(_httpContext)); + } + if (_circuitContext != default) { - links.Add(new ActivityLink(parentActivity.Context)); + links.Add(new ActivityLink(_circuitContext)); } - if (_routeActivity is not null) + if (_routeContext != default) { - links.Add(new ActivityLink(_routeActivity.Context)); + links.Add(new ActivityLink(_routeContext)); } var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index c9f5bafe1462..609707e7f1ed 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -15,28 +15,22 @@ internal sealed class ComponentsMetrics : IDisposable private readonly Counter _navigationCount; - private readonly Histogram _eventSyncDuration; - private readonly Histogram _eventAsyncDuration; + private readonly Histogram _eventDuration; private readonly Counter _eventException; - private readonly Histogram _parametersSyncDuration; - private readonly Histogram _parametersAsyncDuration; + private readonly Histogram _parametersDuration; private readonly Counter _parametersException; - private readonly Histogram _diffDuration; - private readonly Histogram _batchDuration; private readonly Counter _batchException; public bool IsNavigationEnabled => _navigationCount.Enabled; - public bool IsEventDurationEnabled => _eventSyncDuration.Enabled || _eventAsyncDuration.Enabled; + public bool IsEventDurationEnabled => _eventDuration.Enabled; public bool IsEventExceptionEnabled => _eventException.Enabled; - public bool IsStateDurationEnabled => _parametersSyncDuration.Enabled || _parametersAsyncDuration.Enabled; - public bool IsStateExceptionEnabled => _parametersException.Enabled; - - public bool IsDiffDurationEnabled => _diffDuration.Enabled; + public bool IsParametersDurationEnabled => _parametersDuration.Enabled; + public bool IsParametersExceptionEnabled => _parametersException.Enabled; public bool IsBatchDurationEnabled => _batchDuration.Enabled; public bool IsBatchExceptionEnabled => _batchException.Enabled; @@ -52,16 +46,10 @@ public ComponentsMetrics(IMeterFactory meterFactory) unit: "{exceptions}", description: "Total number of route changes."); - _eventSyncDuration = _meter.CreateHistogram( - "aspnetcore.components.event.synchronous.duration", - unit: "s", - description: "Duration of processing browser event synchronously.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); - - _eventAsyncDuration = _meter.CreateHistogram( - "aspnetcore.components.event.asynchronous.duration", + _eventDuration = _meter.CreateHistogram( + "aspnetcore.components.event.duration", unit: "s", - description: "Duration of processing browser event asynchronously.", + description: "Duration of processing browser event.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _eventException = _meter.CreateCounter( @@ -69,16 +57,10 @@ public ComponentsMetrics(IMeterFactory meterFactory) unit: "{exceptions}", description: "Total number of exceptions during browser event processing."); - _parametersSyncDuration = _meter.CreateHistogram( - "aspnetcore.components.parameters.synchronous.duration", + _parametersDuration = _meter.CreateHistogram( + "aspnetcore.components.parameters.duration", unit: "s", - description: "Duration of processing component parameters synchronously.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); - - _parametersAsyncDuration = _meter.CreateHistogram( - "aspnetcore.components.parameters.asynchronous.duration", - unit: "s", - description: "Duration of processing component parameters asynchronously.", + description: "Duration of processing component parameters.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _parametersException = _meter.CreateCounter( @@ -86,12 +68,6 @@ public ComponentsMetrics(IMeterFactory meterFactory) unit: "{exceptions}", description: "Total number of exceptions during processing component parameters."); - _diffDuration = _meter.CreateHistogram( - "aspnetcore.components.rendering.diff.duration", - unit: "s", - description: "Duration of rendering component HTML diff.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); - _batchDuration = _meter.CreateHistogram( "aspnetcore.components.rendering.batch.duration", unit: "s", @@ -115,19 +91,6 @@ public void Navigation(string componentType, string route) _navigationCount.Add(1, tags); } - public void EventDurationSync(long startTimestamp, string? componentType, string? methodName, string? attributeName) - { - var tags = new TagList - { - { "component.type", componentType ?? "unknown" }, - { "component.method", methodName ?? "unknown" }, - { "attribute.name", attributeName ?? "unknown"} - }; - - var duration = Stopwatch.GetElapsedTime(startTimestamp); - _eventSyncDuration.Record(duration.TotalSeconds, tags); - } - public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName) { try @@ -142,7 +105,7 @@ public async Task CaptureEventDurationAsync(Task task, long startTimestamp, stri }; var duration = Stopwatch.GetElapsedTime(startTimestamp); - _eventAsyncDuration.Record(duration.TotalSeconds, tags); + _eventDuration.Record(duration.TotalSeconds, tags); } catch { @@ -150,17 +113,6 @@ public async Task CaptureEventDurationAsync(Task task, long startTimestamp, stri } } - public void ParametersDurationSync(long startTimestamp, string? componentType) - { - var tags = new TagList - { - { "component.type", componentType ?? "unknown" }, - }; - - var duration = Stopwatch.GetElapsedTime(startTimestamp); - _parametersSyncDuration.Record(duration.TotalSeconds, tags); - } - public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, string? componentType) { try @@ -173,7 +125,7 @@ public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, }; var duration = Stopwatch.GetElapsedTime(startTimestamp); - _parametersAsyncDuration.Record(duration.TotalSeconds, tags); + _parametersDuration.Record(duration.TotalSeconds, tags); } catch { @@ -181,18 +133,6 @@ public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, } } - public void DiffDuration(long startTimestamp, string? componentType, int diffLength) - { - var tags = new TagList - { - { "component.type", componentType ?? "unknown" }, - { "diff.length.bucket", BucketEditLength(diffLength) } - }; - - var duration = Stopwatch.GetElapsedTime(startTimestamp); - _diffDuration.Record(duration.TotalSeconds, tags); - } - public void BatchDuration(long startTimestamp, int diffLength) { var tags = new TagList 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/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 8ffc1369f684..d68640ea8b7b 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -505,7 +505,6 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie { var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; var methodName = callback.Delegate.Method?.Name; - ComponentMetrics.EventDurationSync(eventStartTimestamp, receiverName, methodName, attributeName); _ = ComponentMetrics.CaptureEventDurationAsync(task, eventStartTimestamp, receiverName, methodName, attributeName); } if (ComponentMetrics != null && ComponentMetrics.IsEventExceptionEnabled) diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index 201c74e10019..a4a91ae9e0d2 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -53,7 +53,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions(); } - if (_renderer.ComponentMetrics != null && (_renderer.ComponentMetrics.IsDiffDurationEnabled || _renderer.ComponentMetrics.IsStateDurationEnabled || _renderer.ComponentMetrics.IsStateExceptionEnabled)) + if (_renderer.ComponentMetrics != null && (_renderer.ComponentMetrics.IsParametersDurationEnabled || _renderer.ComponentMetrics.IsParametersExceptionEnabled)) { _componentTypeName = component.GetType().FullName; } @@ -108,7 +108,6 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re _nextRenderTree.Clear(); - var diffStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsDiffDurationEnabled ? Stopwatch.GetTimestamp() : 0; try { renderFragment(_nextRenderTree); @@ -125,8 +124,6 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re // We don't want to make errors from this be recoverable, because there's no legitimate reason for them to happen _nextRenderTree.AssertTreeIsValid(Component); - var startCount = batchBuilder.EditsBuffer.Count; - // Swap the old and new tree builders (CurrentRenderTree, _nextRenderTree) = (_nextRenderTree, CurrentRenderTree); @@ -138,11 +135,6 @@ internal void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment re CurrentRenderTree.GetFrames()); batchBuilder.UpdatedComponentDiffs.Append(diff); batchBuilder.InvalidateParameterViews(); - - if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsDiffDurationEnabled) - { - _renderer.ComponentMetrics.DiffDuration(diffStartTimestamp, _componentTypeName, batchBuilder.EditsBuffer.Count - startCount); - } } // Callers expect this method to always return a faulted task. @@ -249,24 +241,23 @@ private void SupplyCombinedParameters(ParameterView directAndCascadingParameters Task setParametersAsyncTask; try { - var stateStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var stateStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersDurationEnabled ? Stopwatch.GetTimestamp() : 0; setParametersAsyncTask = Component.SetParametersAsync(directAndCascadingParameters); // collect metrics - if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateDurationEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersDurationEnabled) { - _renderer.ComponentMetrics.ParametersDurationSync(stateStartTimestamp, _componentTypeName); _ = _renderer.ComponentMetrics.CaptureParametersDurationAsync(setParametersAsyncTask, stateStartTimestamp, _componentTypeName); } - if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateExceptionEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersExceptionEnabled) { _ = _renderer.ComponentMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); } } catch (Exception ex) { - if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsStateExceptionEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersExceptionEnabled) { _renderer.ComponentMetrics.PropertiesFailed(ex.GetType().FullName, _componentTypeName); } diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 8c4d5069b60d..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,11 +223,12 @@ 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) { - RecordDiagnostics(endpointRouteData.PageType.FullName, endpointRouteData.Template); + 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. @@ -238,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; } @@ -254,7 +258,7 @@ internal virtual void Refresh(bool isNavigationIntercepted) $"does not implement {typeof(IComponent).FullName}."); } - RecordDiagnostics(context.Handler.FullName, context.Entry.RoutePattern.RawText); + activity = RecordDiagnostics(context.Handler.FullName, context.Entry.RoutePattern.RawText); Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri); @@ -275,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. @@ -284,20 +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 void RecordDiagnostics(string componentType, string template) + private Activity? RecordDiagnostics(string componentType, string template) { - _renderHandle.ComponentActivitySource?.StartRouteActivity(componentType, 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) @@ -354,8 +370,6 @@ internal async ValueTask RunOnNavigateAsync(string path, bool isNavigationInterc private void OnLocationChanged(object sender, LocationChangedEventArgs args) { - _renderHandle.ComponentActivitySource?.StopRouteActivity(); - _locationAbsolute = args.Location; if (_renderHandle.IsInitialized && Routes != null) { diff --git a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs b/src/Components/Components/test/Rendering/ComponentsMetricsTest.cs similarity index 62% rename from src/Components/Components/test/Rendering/RenderingMetricsTest.cs rename to src/Components/Components/test/Rendering/ComponentsMetricsTest.cs index c750ebec5ce4..15a468cc819d 100644 --- a/src/Components/Components/test/Rendering/RenderingMetricsTest.cs +++ b/src/Components/Components/test/Rendering/ComponentsMetricsTest.cs @@ -32,35 +32,13 @@ public void Constructor_CreatesMetersCorrectly() Assert.Equal(ComponentsMetrics.MeterName, _meterFactory.Meters[0].Name); } - [Fact] - public void EventDurationSync_RecordsDuration() - { - // Arrange - var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var eventSyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); - - // Act - var startTime = Stopwatch.GetTimestamp(); - Thread.Sleep(10); // Add a small delay to ensure a measurable duration - componentsMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); - - // Assert - var measurements = eventSyncDurationCollector.GetMeasurementSnapshot(); - - Assert.Single(measurements); - Assert.True(measurements[0].Value > 0); - Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); - Assert.Equal("OnClick", measurements[0].Tags["attribute.name"]); - } - [Fact] public async Task CaptureEventDurationAsync_RecordsDuration() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.duration"); // Act var startTime = Stopwatch.GetTimestamp(); @@ -77,34 +55,13 @@ public async Task CaptureEventDurationAsync_RecordsDuration() Assert.Equal("MyMethod", measurements[0].Tags["component.method"]); } - [Fact] - public void ParametersDurationSync_RecordsDuration() - { - // Arrange - var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); - - // Act - var startTime = Stopwatch.GetTimestamp(); - Thread.Sleep(10); // Add a small delay to ensure a measurable duration - componentsMetrics.ParametersDurationSync(startTime, "TestComponent"); - - // Assert - var measurements = parametersSyncDurationCollector.GetMeasurementSnapshot(); - - Assert.Single(measurements); - Assert.True(measurements[0].Value > 0); - Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); - } - [Fact] public async Task CaptureParametersDurationAsync_RecordsDuration() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.parameters.duration"); // Act var startTime = Stopwatch.GetTimestamp(); @@ -119,28 +76,6 @@ public async Task CaptureParametersDurationAsync_RecordsDuration() Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); } - [Fact] - public void DiffDuration_RecordsDuration() - { - // Arrange - var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var diffDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); - - // Act - var startTime = Stopwatch.GetTimestamp(); - Thread.Sleep(10); // Add a small delay to ensure a measurable duration - componentsMetrics.DiffDuration(startTime, "TestComponent", 5); - - // Assert - var measurements = diffDurationCollector.GetMeasurementSnapshot(); - - Assert.Single(measurements); - Assert.True(measurements[0].Value > 0); - Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); - Assert.Equal(5, measurements[0].Tags["diff.length.bucket"]); - } - [Fact] public void BatchDuration_RecordsDuration() { @@ -168,7 +103,7 @@ public void EventFailed_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); // Create a mock EventCallback var callback = new EventCallback(new TestComponent(), (Action)(() => { })); @@ -192,7 +127,7 @@ public async Task CaptureEventFailedAsync_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); // Create a mock EventCallback var callback = new EventCallback(new TestComponent(), (Action)(() => { })); @@ -219,7 +154,7 @@ public void PropertiesFailed_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception"); // Act componentsMetrics.PropertiesFailed("ArgumentException", "TestComponent"); @@ -239,7 +174,7 @@ public async Task CapturePropertiesFailedAsync_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception"); // Create a task that throws an exception var task = Task.FromException(new InvalidOperationException()); @@ -304,20 +239,14 @@ public void EnabledProperties_ReflectMeterState() var componentsMetrics = new ComponentsMetrics(_meterFactory); // Create collectors to ensure the meters are enabled - using var eventSyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.synchronous.duration"); using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.duration"); using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.event.exception"); - using var parametersSyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.synchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.asynchronous.duration"); + ComponentsMetrics.MeterName, "aspnetcore.components.parameters.duration"); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.parameters.exception"); - using var diffDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); + 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, @@ -326,70 +255,12 @@ public void EnabledProperties_ReflectMeterState() // Assert Assert.True(componentsMetrics.IsEventDurationEnabled); Assert.True(componentsMetrics.IsEventExceptionEnabled); - Assert.True(componentsMetrics.IsStateDurationEnabled); - Assert.True(componentsMetrics.IsStateExceptionEnabled); - Assert.True(componentsMetrics.IsDiffDurationEnabled); + Assert.True(componentsMetrics.IsParametersDurationEnabled); + Assert.True(componentsMetrics.IsParametersExceptionEnabled); Assert.True(componentsMetrics.IsBatchDurationEnabled); Assert.True(componentsMetrics.IsBatchExceptionEnabled); } - [Fact] - public void BucketEditLength_ReturnsCorrectBucket() - { - // Arrange - var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var diffDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.diff.duration"); - - // Act & Assert - Test different diff lengths - var startTime = Stopwatch.GetTimestamp(); - - // Test each bucket boundary - componentsMetrics.DiffDuration(startTime, "Component", 1); - componentsMetrics.DiffDuration(startTime, "Component", 2); - componentsMetrics.DiffDuration(startTime, "Component", 5); - componentsMetrics.DiffDuration(startTime, "Component", 10); - componentsMetrics.DiffDuration(startTime, "Component", 50); - componentsMetrics.DiffDuration(startTime, "Component", 100); - componentsMetrics.DiffDuration(startTime, "Component", 500); - componentsMetrics.DiffDuration(startTime, "Component", 1000); - componentsMetrics.DiffDuration(startTime, "Component", 10000); - componentsMetrics.DiffDuration(startTime, "Component", 20000); // Should be 10001 - - // Assert - var measurements = diffDurationCollector.GetMeasurementSnapshot(); - - Assert.Equal(10, measurements.Count); - Assert.Equal(1, measurements[0].Tags["diff.length.bucket"]); - Assert.Equal(2, measurements[1].Tags["diff.length.bucket"]); - Assert.Equal(5, measurements[2].Tags["diff.length.bucket"]); - Assert.Equal(10, measurements[3].Tags["diff.length.bucket"]); - Assert.Equal(50, measurements[4].Tags["diff.length.bucket"]); - Assert.Equal(100, measurements[5].Tags["diff.length.bucket"]); - Assert.Equal(500, measurements[6].Tags["diff.length.bucket"]); - Assert.Equal(1000, measurements[7].Tags["diff.length.bucket"]); - Assert.Equal(10000, measurements[8].Tags["diff.length.bucket"]); - Assert.Equal(10001, measurements[9].Tags["diff.length.bucket"]); - } - - [Fact] - public void Dispose_DisposesUnderlyingMeter() - { - // This test verifies that the meter is disposed when the metrics instance is disposed - // This is a bit tricky to test directly, so we'll use an indirect approach - - // Arrange - var componentsMetrics = new ComponentsMetrics(_meterFactory); - - // Act - componentsMetrics.Dispose(); - - // Try to use the disposed meter - this should not throw since TestMeterFactory - // doesn't actually dispose the meter in test contexts - var startTime = Stopwatch.GetTimestamp(); - componentsMetrics.EventDurationSync(startTime, "TestComponent", "MyMethod", "OnClick"); - } - // Helper class for mock components public class TestComponent : IComponent, IHandleEvent { 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 ab461d8b62a2..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,15 +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) @@ -164,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)); 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/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/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); } } From 0a4d488faaea0f547ba0d92cf956f6227e3b48b2 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 25 Apr 2025 20:21:00 +0200 Subject: [PATCH 09/26] IsAllDataRequested --- .../src/ComponentsActivitySource.cs | 139 +++++++++--------- 1 file changed, 73 insertions(+), 66 deletions(-) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 086de83edefc..592d3bee9164 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -24,7 +24,7 @@ internal class ComponentsActivitySource public static ActivityContext CaptureHttpContext() { var parentActivity = Activity.Current; - if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn") + if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded) { return parentActivity.Context; } @@ -34,28 +34,24 @@ public static ActivityContext CaptureHttpContext() public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext) { _circuitId = circuitId; - IEnumerable> tags = - [ - new("circuit.id", _circuitId ?? "unknown"), - ]; - var links = new List(); - if (httpContext != default) - { - _httpContext = httpContext; - links.Add(new ActivityLink(httpContext)); - } - - var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId:null, tags, links); + var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, null, null); if (activity is not null) { - activity.DisplayName = $"CIRCUIT {circuitId ?? "unknown"}"; + 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; - - Console.WriteLine($"StartCircuitActivity: {circuitId}"); - Console.WriteLine($"circuitContext: {_circuitContext.TraceId} {_circuitContext.SpanId} {_circuitContext.TraceState}"); - Console.WriteLine($"httpContext: {httpContext.TraceId} {httpContext.SpanId} {httpContext.TraceState}"); } return activity; } @@ -73,71 +69,82 @@ public void FailCircuitActivity(Activity activity, Exception ex) public Activity? StartRouteActivity(string componentType, string route) { - IEnumerable> tags = - [ - new("circuit.id", _circuitId ?? "unknown"), - new("component.type", componentType ?? "unknown"), - new("route", route ?? "unknown"), - ]; - var links = new List(); if (_httpContext == default) { _httpContext = CaptureHttpContext(); } - if (_httpContext != default) - { - links.Add(new ActivityLink(_httpContext)); - } - if (_circuitContext != default) - { - links.Add(new ActivityLink(_circuitContext)); - } - var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, tags, links); + var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, null, null); if (activity is not null) { - _routeContext = activity.Context; + 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 void StopRouteActivity(Activity activity) - { - _routeContext = default; - if (!activity.IsStopped) - { - activity.Stop(); - } - } - public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName) { - IEnumerable> tags = - [ - new("circuit.id", _circuitId ?? "unknown"), - new("component.type", componentType ?? "unknown"), - new("component.method", methodName ?? "unknown"), - new("attribute.name", attributeName ?? "unknown"), - ]; - var links = new List(); - if (_httpContext != default) - { - links.Add(new ActivityLink(_httpContext)); - } - if (_circuitContext != default) - { - links.Add(new ActivityLink(_circuitContext)); - } - if (_routeContext != default) - { - links.Add(new ActivityLink(_routeContext)); - } - - var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, tags, links); + 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(); } From cf2ab99ee288483caaecdc51afdacd401013714c Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 28 Apr 2025 10:20:50 +0200 Subject: [PATCH 10/26] - ComponentsActivitySourceTest - cleanup --- .../src/ComponentsActivitySource.cs | 17 +- .../test/ComponentsActivitySourceTest.cs | 251 ++++++++++++++++++ .../{Rendering => }/ComponentsMetricsTest.cs | 10 +- .../test/Circuits/CircuitIdFactoryTest.cs | 2 +- 4 files changed, 266 insertions(+), 14 deletions(-) create mode 100644 src/Components/Components/test/ComponentsActivitySourceTest.cs rename src/Components/Components/test/{Rendering => }/ComponentsMetricsTest.cs (95%) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 592d3bee9164..28efad532e15 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -11,8 +11,9 @@ namespace Microsoft.AspNetCore.Components; internal class ComponentsActivitySource { internal const string Name = "Microsoft.AspNetCore.Components"; - internal const string OnEventName = $"{Name}.OnEvent"; + internal const string OnCircuitName = $"{Name}.OnCircuit"; internal const string OnRouteName = $"{Name}.OnRoute"; + internal const string OnEventName = $"{Name}.OnEvent"; private ActivityContext _httpContext; private ActivityContext _circuitContext; @@ -35,7 +36,7 @@ public static ActivityContext CaptureHttpContext() { _circuitId = circuitId; - var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, null, null); + var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Server, parentId: null, null, null); if (activity is not null) { if (activity.IsAllDataRequested) @@ -56,10 +57,10 @@ public static ActivityContext CaptureHttpContext() return activity; } - public void FailCircuitActivity(Activity activity, Exception ex) + public void FailCircuitActivity(Activity? activity, Exception ex) { _circuitContext = default; - if (!activity.IsStopped) + if (activity != null && !activity.IsStopped) { activity.SetTag("error.type", ex.GetType().FullName); activity.SetStatus(ActivityStatusCode.Error); @@ -151,9 +152,9 @@ public void FailCircuitActivity(Activity activity, Exception ex) return activity; } - public static void FailEventActivity(Activity activity, Exception ex) + public static void FailEventActivity(Activity? activity, Exception ex) { - if (!activity.IsStopped) + if (activity != null && !activity.IsStopped) { activity.SetTag("error.type", ex.GetType().FullName); activity.SetStatus(ActivityStatusCode.Error); @@ -161,12 +162,12 @@ public static void FailEventActivity(Activity activity, Exception ex) } } - public static async Task CaptureEventStopAsync(Task task, Activity activity) + public static async Task CaptureEventStopAsync(Task task, Activity? activity) { try { await task; - activity.Stop(); + activity?.Stop(); } catch (Exception ex) { diff --git a/src/Components/Components/test/ComponentsActivitySourceTest.cs b/src/Components/Components/test/ComponentsActivitySourceTest.cs new file mode 100644 index 000000000000..159ea4fdb046 --- /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 _activities; + + public ComponentsActivitySourceTest() + { + _activities = new List(); + _listener = new ActivityListener + { + ShouldListenTo = source => source.Name == ComponentsActivitySource.Name, + Sample = (ref ActivityCreationOptions 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.Server, activity.Kind); + Assert.True(activity.IsAllDataRequested); + Assert.Equal(circuitId, activity.GetTagItem("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.Server, activity.Kind); + Assert.True(activity.IsAllDataRequested); + Assert.Equal(componentType, activity.GetTagItem("component.type")); + Assert.Equal(route, activity.GetTagItem("route")); + Assert.Equal("test-circuit-id", activity.GetTagItem("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.Server, activity.Kind); + Assert.True(activity.IsAllDataRequested); + Assert.Equal(componentType, activity.GetTagItem("component.type")); + Assert.Equal(methodName, activity.GetTagItem("component.method")); + Assert.Equal(attributeName, activity.GetTagItem("attribute.name")); + Assert.Equal("test-circuit-id", activity.GetTagItem("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 -> unknown", 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 -> unknown.unknown", activity.DisplayName); + } +} diff --git a/src/Components/Components/test/Rendering/ComponentsMetricsTest.cs b/src/Components/Components/test/ComponentsMetricsTest.cs similarity index 95% rename from src/Components/Components/test/Rendering/ComponentsMetricsTest.cs rename to src/Components/Components/test/ComponentsMetricsTest.cs index 15a468cc819d..f49bb4f4d0df 100644 --- a/src/Components/Components/test/Rendering/ComponentsMetricsTest.cs +++ b/src/Components/Components/test/ComponentsMetricsTest.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.InternalTesting; using Moq; -namespace Microsoft.AspNetCore.Components.Rendering; +namespace Microsoft.AspNetCore.Components; public class ComponentsMetricsTest { @@ -106,7 +106,7 @@ public void EventFailed_RecordsException() ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); // Create a mock EventCallback - var callback = new EventCallback(new TestComponent(), (Action)(() => { })); + var callback = new EventCallback(new TestComponent(), () => { }); // Act componentsMetrics.EventFailed("ArgumentException", callback, "OnClick"); @@ -118,7 +118,7 @@ public void EventFailed_RecordsException() 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"]); + Assert.Contains("Microsoft.AspNetCore.Components.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); } [Fact] @@ -130,7 +130,7 @@ public async Task CaptureEventFailedAsync_RecordsException() ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); // Create a mock EventCallback - var callback = new EventCallback(new TestComponent(), (Action)(() => { })); + var callback = new EventCallback(new TestComponent(), () => { }); // Create a task that throws an exception var task = Task.FromException(new InvalidOperationException()); @@ -145,7 +145,7 @@ public async Task CaptureEventFailedAsync_RecordsException() 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"]); + Assert.Contains("Microsoft.AspNetCore.Components.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); } [Fact] 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() From 44fa2078137e6d83075693123bd5c7a747627709 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Wed, 30 Apr 2025 12:22:03 +0200 Subject: [PATCH 11/26] Update src/Components/Components/src/ComponentsMetrics.cs Co-authored-by: Noah Falk --- src/Components/Components/src/ComponentsMetrics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 609707e7f1ed..510344be61a1 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -54,7 +54,7 @@ public ComponentsMetrics(IMeterFactory meterFactory) _eventException = _meter.CreateCounter( "aspnetcore.components.event.exception", - unit: "{exceptions}", + unit: "{exception}", description: "Total number of exceptions during browser event processing."); _parametersDuration = _meter.CreateHistogram( From 105b02f6eb0c0e211e444dece91af9a49317aa67 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Wed, 30 Apr 2025 12:22:16 +0200 Subject: [PATCH 12/26] Update src/Components/Components/src/ComponentsMetrics.cs Co-authored-by: Noah Falk --- src/Components/Components/src/ComponentsMetrics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 510344be61a1..dd28b5b3466e 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -75,7 +75,7 @@ public ComponentsMetrics(IMeterFactory meterFactory) advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _batchException = _meter.CreateCounter( - "aspnetcore.components.rendering.batch.exception", + "aspnetcore.components.rendering.batch.exceptions", unit: "{exceptions}", description: "Total number of exceptions during batch rendering."); } From 63f8a69229280d83fced07d56e3c2b5c60d87544 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Wed, 30 Apr 2025 12:22:27 +0200 Subject: [PATCH 13/26] Update src/Components/Components/src/ComponentsMetrics.cs Co-authored-by: Noah Falk --- src/Components/Components/src/ComponentsMetrics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index dd28b5b3466e..67469ce109b4 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -76,7 +76,7 @@ public ComponentsMetrics(IMeterFactory meterFactory) _batchException = _meter.CreateCounter( "aspnetcore.components.rendering.batch.exceptions", - unit: "{exceptions}", + unit: "{exception}", description: "Total number of exceptions during batch rendering."); } From 860931d43c825ccb5c601cc84d00ad830f1ae9b9 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 30 Apr 2025 13:44:20 +0200 Subject: [PATCH 14/26] feedback --- .../Components/src/ComponentsMetrics.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 67469ce109b4..51395656573a 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -11,7 +11,9 @@ 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 _navigationCount; @@ -40,10 +42,11 @@ public ComponentsMetrics(IMeterFactory meterFactory) Debug.Assert(meterFactory != null); _meter = meterFactory.Create(MeterName); + _lifeCycleMeter = meterFactory.Create(LifecycleMeterName); _navigationCount = _meter.CreateCounter( - "aspnetcore.components.navigation.count", - unit: "{exceptions}", + "aspnetcore.components.navigation", + unit: "{route}", description: "Total number of route changes."); _eventDuration = _meter.CreateHistogram( @@ -53,22 +56,22 @@ public ComponentsMetrics(IMeterFactory meterFactory) advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _eventException = _meter.CreateCounter( - "aspnetcore.components.event.exception", + "aspnetcore.components.event.exceptions", unit: "{exception}", description: "Total number of exceptions during browser event processing."); - _parametersDuration = _meter.CreateHistogram( + _parametersDuration = _lifeCycleMeter.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}", + _parametersException = _lifeCycleMeter.CreateCounter( + "aspnetcore.components.parameters.exceptions", + unit: "{exception}", description: "Total number of exceptions during processing component parameters."); - _batchDuration = _meter.CreateHistogram( + _batchDuration = _lifeCycleMeter.CreateHistogram( "aspnetcore.components.rendering.batch.duration", unit: "s", description: "Duration of rendering batch.", @@ -231,5 +234,6 @@ private static int BucketEditLength(int batchLength) public void Dispose() { _meter.Dispose(); + _lifeCycleMeter.Dispose(); } } From de229145c5ee73f19d04d9045f36af4e0048f4d4 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 30 Apr 2025 15:08:29 +0200 Subject: [PATCH 15/26] fix tests --- .../Components/src/ComponentsMetrics.cs | 2 +- .../Components/test/ComponentsMetricsTest.cs | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 51395656573a..444652f64c15 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -77,7 +77,7 @@ public ComponentsMetrics(IMeterFactory meterFactory) description: "Duration of rendering batch.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); - _batchException = _meter.CreateCounter( + _batchException = _lifeCycleMeter.CreateCounter( "aspnetcore.components.rendering.batch.exceptions", unit: "{exception}", description: "Total number of exceptions during batch rendering."); diff --git a/src/Components/Components/test/ComponentsMetricsTest.cs b/src/Components/Components/test/ComponentsMetricsTest.cs index f49bb4f4d0df..a32b9ca68ec8 100644 --- a/src/Components/Components/test/ComponentsMetricsTest.cs +++ b/src/Components/Components/test/ComponentsMetricsTest.cs @@ -28,7 +28,7 @@ public void Constructor_CreatesMetersCorrectly() var componentsMetrics = new ComponentsMetrics(_meterFactory); // Assert - Assert.Single(_meterFactory.Meters); + Assert.Equal(2, _meterFactory.Meters.Count); Assert.Equal(ComponentsMetrics.MeterName, _meterFactory.Meters[0].Name); } @@ -61,7 +61,7 @@ public async Task CaptureParametersDurationAsync_RecordsDuration() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.parameters.duration"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.parameters.duration"); // Act var startTime = Stopwatch.GetTimestamp(); @@ -82,7 +82,7 @@ public void BatchDuration_RecordsDuration() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.duration"); // Act var startTime = Stopwatch.GetTimestamp(); @@ -103,7 +103,7 @@ public void EventFailed_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.exceptions"); // Create a mock EventCallback var callback = new EventCallback(new TestComponent(), () => { }); @@ -127,7 +127,7 @@ public async Task CaptureEventFailedAsync_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.exceptions"); // Create a mock EventCallback var callback = new EventCallback(new TestComponent(), () => { }); @@ -154,7 +154,7 @@ public void PropertiesFailed_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.parameters.exceptions"); // Act componentsMetrics.PropertiesFailed("ArgumentException", "TestComponent"); @@ -174,7 +174,7 @@ public async Task CapturePropertiesFailedAsync_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.parameters.exceptions"); // Create a task that throws an exception var task = Task.FromException(new InvalidOperationException()); @@ -197,7 +197,7 @@ public void BatchFailed_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.exceptions"); // Act componentsMetrics.BatchFailed("ArgumentException"); @@ -216,7 +216,7 @@ public async Task CaptureBatchFailedAsync_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var batchExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.exceptions"); // Create a task that throws an exception var task = Task.FromException(new InvalidOperationException()); @@ -242,15 +242,15 @@ public void EnabledProperties_ReflectMeterState() using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, ComponentsMetrics.MeterName, "aspnetcore.components.event.duration"); using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event.exception"); + ComponentsMetrics.MeterName, "aspnetcore.components.event.exceptions"); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.parameters.duration"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.parameters.duration"); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.parameters.exception"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.parameters.exceptions"); using var batchDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.duration"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.duration"); using var batchExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.rendering.batch.exception"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.exceptions"); // Assert Assert.True(componentsMetrics.IsEventDurationEnabled); From 5ca497daf0990e82137e70bd8287801d856a80f6 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 30 Apr 2025 15:22:26 +0200 Subject: [PATCH 16/26] update_parameters feedback --- src/Components/Components/src/ComponentsMetrics.cs | 4 ++-- .../Components/test/ComponentsMetricsTest.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 444652f64c15..45e8185eee4a 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -61,13 +61,13 @@ public ComponentsMetrics(IMeterFactory meterFactory) description: "Total number of exceptions during browser event processing."); _parametersDuration = _lifeCycleMeter.CreateHistogram( - "aspnetcore.components.parameters.duration", + "aspnetcore.components.update_parameters.duration", unit: "s", description: "Duration of processing component parameters.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _parametersException = _lifeCycleMeter.CreateCounter( - "aspnetcore.components.parameters.exceptions", + "aspnetcore.components.update_parameters.exceptions", unit: "{exception}", description: "Total number of exceptions during processing component parameters."); diff --git a/src/Components/Components/test/ComponentsMetricsTest.cs b/src/Components/Components/test/ComponentsMetricsTest.cs index a32b9ca68ec8..5322c073a6e2 100644 --- a/src/Components/Components/test/ComponentsMetricsTest.cs +++ b/src/Components/Components/test/ComponentsMetricsTest.cs @@ -61,7 +61,7 @@ public async Task CaptureParametersDurationAsync_RecordsDuration() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.parameters.duration"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration"); // Act var startTime = Stopwatch.GetTimestamp(); @@ -154,7 +154,7 @@ public void PropertiesFailed_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.parameters.exceptions"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.exceptions"); // Act componentsMetrics.PropertiesFailed("ArgumentException", "TestComponent"); @@ -174,7 +174,7 @@ public async Task CapturePropertiesFailedAsync_RecordsException() // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.parameters.exceptions"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.exceptions"); // Create a task that throws an exception var task = Task.FromException(new InvalidOperationException()); @@ -244,9 +244,9 @@ public void EnabledProperties_ReflectMeterState() using var eventExceptionCollector = new MetricCollector(_meterFactory, ComponentsMetrics.MeterName, "aspnetcore.components.event.exceptions"); using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.parameters.duration"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration"); using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.parameters.exceptions"); + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.exceptions"); using var batchDurationCollector = new MetricCollector(_meterFactory, ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.duration"); using var batchExceptionCollector = new MetricCollector(_meterFactory, From 2ae95e01362015e45fd23e069a12e37d5c5d463f Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 2 May 2025 10:37:55 +0200 Subject: [PATCH 17/26] Update src/Components/Components/src/ComponentsActivitySource.cs Co-authored-by: Sam Spencer <54915162+samsp-msft@users.noreply.github.com> --- src/Components/Components/src/ComponentsActivitySource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 28efad532e15..a59781728cba 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -50,7 +50,7 @@ public static ActivityContext CaptureHttpContext() activity.AddLink(new ActivityLink(httpContext)); } } - activity.DisplayName = $"CIRCUIT {circuitId ?? ""}"; + activity.DisplayName = $"Circuit {circuitId ?? ""}"; activity.Start(); _circuitContext = activity.Context; } From b9331d9f8957d6ac6d772e891cdaf176c76a1185 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 2 May 2025 10:38:45 +0200 Subject: [PATCH 18/26] Update src/Components/Components/src/ComponentsActivitySource.cs Co-authored-by: Sam Spencer <54915162+samsp-msft@users.noreply.github.com> --- src/Components/Components/src/ComponentsActivitySource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index a59781728cba..7840d2a0a67e 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -102,7 +102,7 @@ public void FailCircuitActivity(Activity? activity, Exception ex) } } - activity.DisplayName = $"ROUTE {route ?? "unknown"} -> {componentType ?? "unknown"}"; + activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}"; activity.Start(); _routeContext = activity.Context; } From 96f1d6ae1bf0851274cbd6ffc7c2bce4f9373a97 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Fri, 2 May 2025 10:39:00 +0200 Subject: [PATCH 19/26] Update src/Components/Components/src/ComponentsActivitySource.cs Co-authored-by: Sam Spencer <54915162+samsp-msft@users.noreply.github.com> --- src/Components/Components/src/ComponentsActivitySource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 7840d2a0a67e..54aa09fabe64 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -146,7 +146,7 @@ public void FailCircuitActivity(Activity? activity, Exception ex) } } - activity.DisplayName = $"EVENT {attributeName ?? "unknown"} -> {componentType ?? "unknown"}.{methodName ?? "unknown"}"; + activity.DisplayName = $"Event {attributeName ?? "[unknown]"} -> {componentType ?? "[unknown]"}.{methodName ?? "[unknown]"}"; activity.Start(); } return activity; From 1c61f503f6e2228d41c0b62974dc83699ec37918 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 2 May 2025 13:09:32 +0200 Subject: [PATCH 20/26] feedback --- src/Components/Components/src/ComponentsActivitySource.cs | 8 ++++---- src/Components/Components/src/ComponentsMetrics.cs | 6 +++--- src/Components/Components/test/ComponentsMetricsTest.cs | 2 +- src/Components/Server/src/Circuits/CircuitMetrics.cs | 2 +- src/Shared/Metrics/MetricsConstants.cs | 8 +++++++- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 54aa09fabe64..f0e6aceb7ce5 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -11,9 +11,9 @@ namespace Microsoft.AspNetCore.Components; 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"; + internal const string OnCircuitName = $"{Name}.CircuitStart"; + internal const string OnRouteName = $"{Name}.RouteChange"; + internal const string OnEventName = $"{Name}.Event"; private ActivityContext _httpContext; private ActivityContext _circuitContext; @@ -146,7 +146,7 @@ public void FailCircuitActivity(Activity? activity, Exception ex) } } - activity.DisplayName = $"Event {attributeName ?? "[unknown]"} -> {componentType ?? "[unknown]"}.{methodName ?? "[unknown]"}"; + activity.DisplayName = $"Event {attributeName ?? "[unknown attribute]"} -> {componentType ?? "[unknown component]"}.{methodName ?? "[unknown method]"}"; activity.Start(); } return activity; diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 45e8185eee4a..4aa1e2f8e81c 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -64,7 +64,7 @@ public ComponentsMetrics(IMeterFactory meterFactory) "aspnetcore.components.update_parameters.duration", unit: "s", description: "Duration of processing component parameters.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorParametersUpdateSecondsBucketBoundaries }); _parametersException = _lifeCycleMeter.CreateCounter( "aspnetcore.components.update_parameters.exceptions", @@ -75,7 +75,7 @@ public ComponentsMetrics(IMeterFactory meterFactory) "aspnetcore.components.rendering.batch.duration", unit: "s", description: "Duration of rendering batch.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorBatchDiffSecondsBucketBoundaries }); _batchException = _lifeCycleMeter.CreateCounter( "aspnetcore.components.rendering.batch.exceptions", @@ -140,7 +140,7 @@ public void BatchDuration(long startTimestamp, int diffLength) { var tags = new TagList { - { "diff.length.bucket", BucketEditLength(diffLength) } + { "diff.approximate.length", BucketEditLength(diffLength) } }; var duration = Stopwatch.GetElapsedTime(startTimestamp); diff --git a/src/Components/Components/test/ComponentsMetricsTest.cs b/src/Components/Components/test/ComponentsMetricsTest.cs index 5322c073a6e2..07aa27f7429c 100644 --- a/src/Components/Components/test/ComponentsMetricsTest.cs +++ b/src/Components/Components/test/ComponentsMetricsTest.cs @@ -94,7 +94,7 @@ public void BatchDuration_RecordsDuration() Assert.Single(measurements); Assert.True(measurements[0].Value > 0); - Assert.Equal(50, measurements[0].Tags["diff.length.bucket"]); + Assert.Equal(50, measurements[0].Tags["diff.approximate.length"]); } [Fact] diff --git a/src/Components/Server/src/Circuits/CircuitMetrics.cs b/src/Components/Server/src/Circuits/CircuitMetrics.cs index da7b8e9d297b..8f7bfff2e28d 100644 --- a/src/Components/Server/src/Circuits/CircuitMetrics.cs +++ b/src/Components/Server/src/Circuits/CircuitMetrics.cs @@ -42,7 +42,7 @@ public CircuitMetrics(IMeterFactory meterFactory) "aspnetcore.components.circuits.duration", unit: "s", description: "Duration of circuit.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.VeryLongSecondsBucketBoundaries }); + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorCircuitSecondsBucketBoundaries }); } public void OnCircuitOpened() diff --git a/src/Shared/Metrics/MetricsConstants.cs b/src/Shared/Metrics/MetricsConstants.cs index cdb338f1d7a0..f9b6fed7119b 100644 --- a/src/Shared/Metrics/MetricsConstants.cs +++ b/src/Shared/Metrics/MetricsConstants.cs @@ -11,6 +11,12 @@ 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 rendering, which should be very fast. + public static readonly IReadOnlyList 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]; + + // For blazor rendering, which should be very fast. + public static readonly IReadOnlyList BlazorBatchDiffSecondsBucketBoundaries = [0.000001, 0.00001, 0.0001, 0.001, 0.01, 0.1, 1]; + // 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]; + public static readonly IReadOnlyList 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]; } From bda2aec3a2f70f306d9a1abed6da0b669c123996 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 May 2025 12:30:00 +0200 Subject: [PATCH 21/26] ActivityKind.Internal --- .../Components/src/ComponentsActivitySource.cs | 6 +++--- .../test/ComponentsActivitySourceTest.cs | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index f0e6aceb7ce5..1f8caa04edc3 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -36,7 +36,7 @@ public static ActivityContext CaptureHttpContext() { _circuitId = circuitId; - var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Server, parentId: null, null, null); + var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null); if (activity is not null) { if (activity.IsAllDataRequested) @@ -75,7 +75,7 @@ public void FailCircuitActivity(Activity? activity, Exception ex) _httpContext = CaptureHttpContext(); } - var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Server, parentId: null, null, null); + var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null); if (activity is not null) { if (activity.IsAllDataRequested) @@ -111,7 +111,7 @@ public void FailCircuitActivity(Activity? activity, Exception ex) public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName) { - var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Server, parentId: null, null, null); + var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Internal, parentId: null, null, null); if (activity is not null) { if (activity.IsAllDataRequested) diff --git a/src/Components/Components/test/ComponentsActivitySourceTest.cs b/src/Components/Components/test/ComponentsActivitySourceTest.cs index 159ea4fdb046..981aea991ca9 100644 --- a/src/Components/Components/test/ComponentsActivitySourceTest.cs +++ b/src/Components/Components/test/ComponentsActivitySourceTest.cs @@ -74,8 +74,8 @@ public void StartCircuitActivity_CreatesAndStartsActivity() // Assert Assert.NotNull(activity); Assert.Equal(ComponentsActivitySource.OnCircuitName, activity.OperationName); - Assert.Equal($"CIRCUIT {circuitId}", activity.DisplayName); - Assert.Equal(ActivityKind.Server, activity.Kind); + Assert.Equal($"Circuit {circuitId}", activity.DisplayName); + Assert.Equal(ActivityKind.Internal, activity.Kind); Assert.True(activity.IsAllDataRequested); Assert.Equal(circuitId, activity.GetTagItem("circuit.id")); Assert.Contains(activity.Links, link => link.Context == httpContext); @@ -118,8 +118,8 @@ public void StartRouteActivity_CreatesAndStartsActivity() // Assert Assert.NotNull(activity); Assert.Equal(ComponentsActivitySource.OnRouteName, activity.OperationName); - Assert.Equal($"ROUTE {route} -> {componentType}", activity.DisplayName); - Assert.Equal(ActivityKind.Server, activity.Kind); + Assert.Equal($"Route {route} -> {componentType}", activity.DisplayName); + Assert.Equal(ActivityKind.Internal, activity.Kind); Assert.True(activity.IsAllDataRequested); Assert.Equal(componentType, activity.GetTagItem("component.type")); Assert.Equal(route, activity.GetTagItem("route")); @@ -146,8 +146,8 @@ public void StartEventActivity_CreatesAndStartsActivity() // Assert Assert.NotNull(activity); Assert.Equal(ComponentsActivitySource.OnEventName, activity.OperationName); - Assert.Equal($"EVENT {attributeName} -> {componentType}.{methodName}", activity.DisplayName); - Assert.Equal(ActivityKind.Server, activity.Kind); + Assert.Equal($"Event {attributeName} -> {componentType}.{methodName}", activity.DisplayName); + Assert.Equal(ActivityKind.Internal, activity.Kind); Assert.True(activity.IsAllDataRequested); Assert.Equal(componentType, activity.GetTagItem("component.type")); Assert.Equal(methodName, activity.GetTagItem("component.method")); @@ -218,7 +218,7 @@ public void StartCircuitActivity_HandlesNullValues() // Assert Assert.NotNull(activity); - Assert.Equal("CIRCUIT ", activity.DisplayName); + Assert.Equal("Circuit ", activity.DisplayName); } [Fact] @@ -232,7 +232,7 @@ public void StartRouteActivity_HandlesNullValues() // Assert Assert.NotNull(activity); - Assert.Equal("ROUTE unknown -> unknown", activity.DisplayName); + Assert.Equal("Route [unknown path] -> [unknown component]", activity.DisplayName); } [Fact] @@ -246,6 +246,6 @@ public void StartEventActivity_HandlesNullValues() // Assert Assert.NotNull(activity); - Assert.Equal("EVENT unknown -> unknown.unknown", activity.DisplayName); + Assert.Equal("Event [unknown attribute] -> [unknown component].[unknown method]", activity.DisplayName); } } From 84dbfd2d3533dc9995ebd709d360bd845e37f552 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 6 May 2025 15:08:28 +0200 Subject: [PATCH 22/26] aspnetcore.components prefix for tags --- .../src/ComponentsActivitySource.cs | 16 +++++++------- .../Components/src/ComponentsMetrics.cs | 20 ++++++++--------- .../test/ComponentsActivitySourceTest.cs | 16 +++++++------- .../Components/test/ComponentsMetricsTest.cs | 22 +++++++++---------- .../Server/src/Circuits/CircuitMetrics.cs | 6 ++--- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 1f8caa04edc3..a375d3b179a1 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -43,7 +43,7 @@ public static ActivityContext CaptureHttpContext() { if (_circuitId != null) { - activity.SetTag("circuit.id", _circuitId); + activity.SetTag("aspnetcore.components.circuit.id", _circuitId); } if (httpContext != default) { @@ -82,15 +82,15 @@ public void FailCircuitActivity(Activity? activity, Exception ex) { if (_circuitId != null) { - activity.SetTag("circuit.id", _circuitId); + activity.SetTag("aspnetcore.components.circuit.id", _circuitId); } if (componentType != null) { - activity.SetTag("component.type", componentType); + activity.SetTag("aspnetcore.components.type", componentType); } if (route != null) { - activity.SetTag("route", route); + activity.SetTag("aspnetcore.components.route", route); } if (_httpContext != default) { @@ -118,19 +118,19 @@ public void FailCircuitActivity(Activity? activity, Exception ex) { if (_circuitId != null) { - activity.SetTag("circuit.id", _circuitId); + activity.SetTag("aspnetcore.components.circuit.id", _circuitId); } if (componentType != null) { - activity.SetTag("component.type", componentType); + activity.SetTag("aspnetcore.components.type", componentType); } if (methodName != null) { - activity.SetTag("component.method", methodName); + activity.SetTag("aspnetcore.components.method", methodName); } if (attributeName != null) { - activity.SetTag("attribute.name", attributeName); + activity.SetTag("aspnetcore.components.attribute.name", attributeName); } if (_httpContext != default) { diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 4aa1e2f8e81c..538f6d78a7d2 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -87,8 +87,8 @@ public void Navigation(string componentType, string route) { var tags = new TagList { - { "component.type", componentType ?? "unknown" }, - { "route", route ?? "unknown" }, + { "aspnetcore.components.type", componentType ?? "unknown" }, + { "aspnetcore.components.route", route ?? "unknown" }, }; _navigationCount.Add(1, tags); @@ -102,9 +102,9 @@ public async Task CaptureEventDurationAsync(Task task, long startTimestamp, stri var tags = new TagList { - { "component.type", componentType ?? "unknown" }, - { "component.method", methodName ?? "unknown" }, - { "attribute.name", attributeName ?? "unknown" } + { "aspnetcore.components.type", componentType ?? "unknown" }, + { "aspnetcore.components.method", methodName ?? "unknown" }, + { "aspnetcore.components.attribute.name", attributeName ?? "unknown" } }; var duration = Stopwatch.GetElapsedTime(startTimestamp); @@ -124,7 +124,7 @@ public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, var tags = new TagList { - { "component.type", componentType ?? "unknown" }, + { "aspnetcore.components.type", componentType ?? "unknown" }, }; var duration = Stopwatch.GetElapsedTime(startTimestamp); @@ -140,7 +140,7 @@ public void BatchDuration(long startTimestamp, int diffLength) { var tags = new TagList { - { "diff.approximate.length", BucketEditLength(diffLength) } + { "aspnetcore.components.diff.approximate.length", BucketEditLength(diffLength) } }; var duration = Stopwatch.GetElapsedTime(startTimestamp); @@ -152,8 +152,8 @@ public void EventFailed(string? exceptionType, EventCallback callback, string? a var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate?.Target?.GetType())?.FullName; var tags = new TagList { - { "component.type", receiverName ?? "unknown" }, - { "attribute.name", attributeName ?? "unknown"}, + { "aspnetcore.components.type", receiverName ?? "unknown" }, + { "aspnetcore.components.attribute.name", attributeName ?? "unknown"}, { "error.type", exceptionType ?? "unknown"} }; _eventException.Add(1, tags); @@ -175,7 +175,7 @@ public void PropertiesFailed(string? exceptionType, string? componentType) { var tags = new TagList { - { "component.type", componentType ?? "unknown" }, + { "aspnetcore.components.type", componentType ?? "unknown" }, { "error.type", exceptionType ?? "unknown"} }; _parametersException.Add(1, tags); diff --git a/src/Components/Components/test/ComponentsActivitySourceTest.cs b/src/Components/Components/test/ComponentsActivitySourceTest.cs index 981aea991ca9..660bf9428264 100644 --- a/src/Components/Components/test/ComponentsActivitySourceTest.cs +++ b/src/Components/Components/test/ComponentsActivitySourceTest.cs @@ -77,7 +77,7 @@ public void StartCircuitActivity_CreatesAndStartsActivity() Assert.Equal($"Circuit {circuitId}", activity.DisplayName); Assert.Equal(ActivityKind.Internal, activity.Kind); Assert.True(activity.IsAllDataRequested); - Assert.Equal(circuitId, activity.GetTagItem("circuit.id")); + Assert.Equal(circuitId, activity.GetTagItem("aspnetcore.components.circuit.id")); Assert.Contains(activity.Links, link => link.Context == httpContext); Assert.False(activity.IsStopped); } @@ -121,9 +121,9 @@ public void StartRouteActivity_CreatesAndStartsActivity() Assert.Equal($"Route {route} -> {componentType}", activity.DisplayName); Assert.Equal(ActivityKind.Internal, activity.Kind); Assert.True(activity.IsAllDataRequested); - Assert.Equal(componentType, activity.GetTagItem("component.type")); - Assert.Equal(route, activity.GetTagItem("route")); - Assert.Equal("test-circuit-id", activity.GetTagItem("circuit.id")); + 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); } @@ -149,10 +149,10 @@ public void StartEventActivity_CreatesAndStartsActivity() Assert.Equal($"Event {attributeName} -> {componentType}.{methodName}", activity.DisplayName); Assert.Equal(ActivityKind.Internal, activity.Kind); Assert.True(activity.IsAllDataRequested); - Assert.Equal(componentType, activity.GetTagItem("component.type")); - Assert.Equal(methodName, activity.GetTagItem("component.method")); - Assert.Equal(attributeName, activity.GetTagItem("attribute.name")); - Assert.Equal("test-circuit-id", activity.GetTagItem("circuit.id")); + 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); } diff --git a/src/Components/Components/test/ComponentsMetricsTest.cs b/src/Components/Components/test/ComponentsMetricsTest.cs index 07aa27f7429c..cbec0b5a884e 100644 --- a/src/Components/Components/test/ComponentsMetricsTest.cs +++ b/src/Components/Components/test/ComponentsMetricsTest.cs @@ -50,9 +50,9 @@ public async Task CaptureEventDurationAsync_RecordsDuration() 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"]); + Assert.Equal("TestComponent", measurements[0].Tags["aspnetcore.components.type"]); + Assert.Equal("OnClickAsync", measurements[0].Tags["aspnetcore.components.attribute.name"]); + Assert.Equal("MyMethod", measurements[0].Tags["aspnetcore.components.method"]); } [Fact] @@ -73,7 +73,7 @@ public async Task CaptureParametersDurationAsync_RecordsDuration() Assert.Single(measurements); Assert.True(measurements[0].Value > 0); - Assert.Equal("TestComponent", measurements[0].Tags["component.type"]); + Assert.Equal("TestComponent", measurements[0].Tags["aspnetcore.components.type"]); } [Fact] @@ -94,7 +94,7 @@ public void BatchDuration_RecordsDuration() Assert.Single(measurements); Assert.True(measurements[0].Value > 0); - Assert.Equal(50, measurements[0].Tags["diff.approximate.length"]); + Assert.Equal(50, measurements[0].Tags["aspnetcore.components.diff.approximate.length"]); } [Fact] @@ -117,8 +117,8 @@ public void EventFailed_RecordsException() 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.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); + Assert.Equal("OnClick", measurements[0].Tags["aspnetcore.components.attribute.name"]); + Assert.Contains("Microsoft.AspNetCore.Components.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["aspnetcore.components.type"]); } [Fact] @@ -144,8 +144,8 @@ public async Task CaptureEventFailedAsync_RecordsException() 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.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["component.type"]); + Assert.Equal("OnClickAsync", measurements[0].Tags["aspnetcore.components.attribute.name"]); + Assert.Contains("Microsoft.AspNetCore.Components.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["aspnetcore.components.type"]); } [Fact] @@ -165,7 +165,7 @@ public void PropertiesFailed_RecordsException() 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"]); + Assert.Equal("TestComponent", measurements[0].Tags["aspnetcore.components.type"]); } [Fact] @@ -188,7 +188,7 @@ public async Task CapturePropertiesFailedAsync_RecordsException() 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"]); + Assert.Equal("TestComponent", measurements[0].Tags["aspnetcore.components.type"]); } [Fact] diff --git a/src/Components/Server/src/Circuits/CircuitMetrics.cs b/src/Components/Server/src/Circuits/CircuitMetrics.cs index 8f7bfff2e28d..b52c0ba878b8 100644 --- a/src/Components/Server/src/Circuits/CircuitMetrics.cs +++ b/src/Components/Server/src/Circuits/CircuitMetrics.cs @@ -25,17 +25,17 @@ public CircuitMetrics(IMeterFactory meterFactory) _circuitTotalCounter = _meter.CreateCounter( "aspnetcore.components.circuits.count", - unit: "{circuits}", + unit: "{circuit}", description: "Total number of circuits."); _circuitActiveCounter = _meter.CreateUpDownCounter( "aspnetcore.components.circuits.active_circuits", - unit: "{circuits}", + unit: "{circuit}", description: "Number of active circuits."); _circuitConnectedCounter = _meter.CreateUpDownCounter( "aspnetcore.components.circuits.connected_circuits", - unit: "{circuits}", + unit: "{circuit}", description: "Number of disconnected circuits."); _circuitDuration = _meter.CreateHistogram( From 3b475a469a2cc975d39fa1139648d56a407b23b9 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 7 May 2025 16:08:59 +0200 Subject: [PATCH 23/26] - merge exception and duration metrics - rename aspnetcore.components.circuit to aspnetcore.components.circuits - rename circuits.active_circuits to circuit.active and connected_circuits to circuit.connected - rename aspnetcore.components.event.duration to aspnetcore.components.event_handler and include exceptions - rename aspnetcore.components.update_parameters.duration to aspnetcore.components.update_parameters and include exceptions - rename aspnetcore.components.rendering.batch.duration to aspnetcore.components.render_diff and include exceptions --- .../Components/src/ComponentsMetrics.cs | 156 +++----- .../Components/src/RenderTree/Renderer.cs | 30 +- .../src/Rendering/ComponentState.cs | 18 +- .../Components/test/ComponentsMetricsTest.cs | 357 ++++++++++++------ .../Server/src/Circuits/CircuitMetrics.cs | 14 +- .../test/Circuits/CircuitMetricsTest.cs | 33 +- src/Shared/Metrics/MetricsConstants.cs | 5 +- 7 files changed, 335 insertions(+), 278 deletions(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index 538f6d78a7d2..abdbf2d07c66 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -18,24 +18,16 @@ internal sealed class ComponentsMetrics : IDisposable 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 IsEventEnabled => _eventDuration.Enabled; - public bool IsParametersDurationEnabled => _parametersDuration.Enabled; - public bool IsParametersExceptionEnabled => _parametersException.Enabled; + public bool IsParametersEnabled => _parametersDuration.Enabled; - public bool IsBatchDurationEnabled => _batchDuration.Enabled; - public bool IsBatchExceptionEnabled => _batchException.Enabled; + public bool IsBatchEnabled => _batchDuration.Enabled; public ComponentsMetrics(IMeterFactory meterFactory) { @@ -50,37 +42,22 @@ public ComponentsMetrics(IMeterFactory meterFactory) description: "Total number of route changes."); _eventDuration = _meter.CreateHistogram( - "aspnetcore.components.event.duration", + "aspnetcore.components.event_handler", unit: "s", description: "Duration of processing browser event.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); - _eventException = _meter.CreateCounter( - "aspnetcore.components.event.exceptions", - unit: "{exception}", - description: "Total number of exceptions during browser event processing."); - _parametersDuration = _lifeCycleMeter.CreateHistogram( - "aspnetcore.components.update_parameters.duration", + "aspnetcore.components.update_parameters", unit: "s", description: "Duration of processing component parameters.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorParametersUpdateSecondsBucketBoundaries }); - - _parametersException = _lifeCycleMeter.CreateCounter( - "aspnetcore.components.update_parameters.exceptions", - unit: "{exception}", - description: "Total number of exceptions during processing component parameters."); + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries }); _batchDuration = _lifeCycleMeter.CreateHistogram( - "aspnetcore.components.rendering.batch.duration", + "aspnetcore.components.render_diff", unit: "s", - description: "Duration of rendering batch.", - advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorBatchDiffSecondsBucketBoundaries }); - - _batchException = _lifeCycleMeter.CreateCounter( - "aspnetcore.components.rendering.batch.exceptions", - unit: "{exception}", - description: "Total number of exceptions during batch rendering."); + description: "Duration of rendering DOM update including network and browser time.", + advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries }); } public void Navigation(string componentType, string route) @@ -94,134 +71,109 @@ public void Navigation(string componentType, string route) _navigationCount.Add(1, tags); } - public async Task CaptureEventDurationAsync(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName) + public async Task CaptureEventDuration(Task task, long startTimestamp, string? componentType, string? methodName, string? attributeName) { - try - { - await task; - - var tags = new TagList - { - { "aspnetcore.components.type", componentType ?? "unknown" }, - { "aspnetcore.components.method", methodName ?? "unknown" }, - { "aspnetcore.components.attribute.name", attributeName ?? "unknown" } - }; - - var duration = Stopwatch.GetElapsedTime(startTimestamp); - _eventDuration.Record(duration.TotalSeconds, tags); - } - catch + var tags = new TagList { - // none - } - } + { "aspnetcore.components.type", componentType ?? "unknown" }, + { "aspnetcore.components.method", methodName ?? "unknown" }, + { "aspnetcore.components.attribute.name", attributeName ?? "unknown" } + }; - public async Task CaptureParametersDurationAsync(Task task, long startTimestamp, string? componentType) - { try { await task; - - var tags = new TagList - { - { "aspnetcore.components.type", componentType ?? "unknown" }, - }; - - var duration = Stopwatch.GetElapsedTime(startTimestamp); - _parametersDuration.Record(duration.TotalSeconds, tags); } - catch + catch (Exception ex) { - // none + tags.Add("error.type", ex.GetType().FullName ?? "unknown"); } + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _eventDuration.Record(duration.TotalSeconds, tags); } - public void BatchDuration(long startTimestamp, int diffLength) + public void FailEventSync(Exception ex, long startTimestamp, string? componentType, string? methodName, string? attributeName) { var tags = new TagList { - { "aspnetcore.components.diff.approximate.length", BucketEditLength(diffLength) } + { "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); - _batchDuration.Record(duration.TotalSeconds, tags); + _eventDuration.Record(duration.TotalSeconds, tags); } - public void EventFailed(string? exceptionType, EventCallback callback, string? attributeName) + public async Task CaptureParametersDuration(Task task, long startTimestamp, string? componentType) { - var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate?.Target?.GetType())?.FullName; var tags = new TagList { - { "aspnetcore.components.type", receiverName ?? "unknown" }, - { "aspnetcore.components.attribute.name", attributeName ?? "unknown"}, - { "error.type", exceptionType ?? "unknown"} + { "aspnetcore.components.type", componentType ?? "unknown" }, }; - _eventException.Add(1, tags); - } - public async Task CaptureEventFailedAsync(Task task, EventCallback callback, string? attributeName) - { try { await task; } - catch (Exception ex) + catch(Exception ex) { - EventFailed(ex.GetType().Name, callback, attributeName); + tags.Add("error.type", ex.GetType().FullName ?? "unknown"); } + var duration = Stopwatch.GetElapsedTime(startTimestamp); + _parametersDuration.Record(duration.TotalSeconds, tags); } - public void PropertiesFailed(string? exceptionType, string? componentType) + 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", exceptionType ?? "unknown"} + { "error.type", ex.GetType().FullName ?? "unknown" } }; - _parametersException.Add(1, tags); - } - - public async Task CapturePropertiesFailedAsync(Task task, string? componentType) - { - try - { - await task; - } - catch (Exception ex) - { - PropertiesFailed(ex.GetType().Name, componentType); - } + _parametersDuration.Record(duration.TotalSeconds, tags); } - public void BatchFailed(string? exceptionType) + public async Task CaptureBatchDuration(Task task, long startTimestamp, int diffLength) { var tags = new TagList { - { "error.type", exceptionType ?? "unknown"} + { "aspnetcore.components.diff.length", BucketDiffLength(diffLength) } }; - _batchException.Add(1, tags); - } - public async Task CaptureBatchFailedAsync(Task task) - { try { await task; } catch (Exception ex) { - BatchFailed(ex.GetType().Name); + 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 BucketEditLength(int batchLength) + private static int BucketDiffLength(int diffLength) { - return batchLength switch + return diffLength switch { <= 1 => 1, <= 2 => 2, <= 5 => 5, <= 10 => 10, + <= 20 => 20, <= 50 => 50, <= 100 => 100, <= 500 => 500, diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index d68640ea8b7b..34868b2f5de0 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -457,7 +457,7 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie activity = ComponentActivitySource.StartEventActivity(receiverName, methodName, attributeName); } - var eventStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsEventDurationEnabled ? Stopwatch.GetTimestamp() : 0; + 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. @@ -501,15 +501,11 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie task = callback.InvokeAsync(eventArgs); // collect metrics - if (ComponentMetrics != null && ComponentMetrics.IsEventDurationEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsEventEnabled) { 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); + _ = ComponentMetrics.CaptureEventDuration(task, eventStartTimestamp, receiverName, methodName, attributeName); } // stop activity/trace @@ -520,9 +516,11 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie } catch (Exception e) { - if (ComponentMetrics != null && ComponentMetrics.IsEventExceptionEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsEventEnabled) { - ComponentMetrics.EventFailed(e.GetType().FullName, callback, attributeName); + var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; + var methodName = callback.Delegate.Method?.Name; + ComponentMetrics.FailEventSync(e, eventStartTimestamp, receiverName, methodName, attributeName); } if (ComponentActivitySource != null && activity != null) @@ -813,7 +811,7 @@ private void ProcessRenderQueue() _isBatchInProgress = true; var updateDisplayTask = Task.CompletedTask; - var batchStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsBatchDurationEnabled ? Stopwatch.GetTimestamp() : 0; + var batchStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsBatchEnabled ? Stopwatch.GetTimestamp() : 0; try { @@ -846,20 +844,16 @@ private void ProcessRenderQueue() // 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) + if (ComponentMetrics != null && ComponentMetrics.IsBatchEnabled) { - _ = ComponentMetrics.CaptureBatchFailedAsync(updateDisplayTask); + _ = ComponentMetrics.CaptureBatchDuration(updateDisplayTask, batchStartTimestamp, batch.UpdatedComponents.Count); } } catch (Exception e) { - if (ComponentMetrics != null && ComponentMetrics.IsBatchExceptionEnabled) + if (ComponentMetrics != null && ComponentMetrics.IsBatchEnabled) { - ComponentMetrics.BatchFailed(e.GetType().Name); + ComponentMetrics.FailBatchSync(e, batchStartTimestamp); } // Ensure we catch errors while running the render functions of the components. diff --git a/src/Components/Components/src/Rendering/ComponentState.cs b/src/Components/Components/src/Rendering/ComponentState.cs index a4a91ae9e0d2..c1ee3c2f1646 100644 --- a/src/Components/Components/src/Rendering/ComponentState.cs +++ b/src/Components/Components/src/Rendering/ComponentState.cs @@ -53,7 +53,7 @@ public ComponentState(Renderer renderer, int componentId, IComponent component, _hasAnyCascadingParameterSubscriptions = AddCascadingParameterSubscriptions(); } - if (_renderer.ComponentMetrics != null && (_renderer.ComponentMetrics.IsParametersDurationEnabled || _renderer.ComponentMetrics.IsParametersExceptionEnabled)) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersEnabled) { _componentTypeName = component.GetType().FullName; } @@ -237,29 +237,25 @@ internal void NotifyCascadingValueChanged(in ParameterViewLifetime lifetime) // a consistent set to the recipient. private void SupplyCombinedParameters(ParameterView directAndCascadingParameters) { + var parametersStartTimestamp = _renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersEnabled ? Stopwatch.GetTimestamp() : 0; + // 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) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersEnabled) { - _ = _renderer.ComponentMetrics.CapturePropertiesFailedAsync(setParametersAsyncTask, _componentTypeName); + _ = _renderer.ComponentMetrics.CaptureParametersDuration(setParametersAsyncTask, parametersStartTimestamp, _componentTypeName); } } catch (Exception ex) { - if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersExceptionEnabled) + if (_renderer.ComponentMetrics != null && _renderer.ComponentMetrics.IsParametersEnabled) { - _renderer.ComponentMetrics.PropertiesFailed(ex.GetType().FullName, _componentTypeName); + _renderer.ComponentMetrics.FailParametersSync(ex, parametersStartTimestamp, _componentTypeName); } setParametersAsyncTask = Task.FromException(ex); diff --git a/src/Components/Components/test/ComponentsMetricsTest.cs b/src/Components/Components/test/ComponentsMetricsTest.cs index cbec0b5a884e..e8b466d5e45d 100644 --- a/src/Components/Components/test/ComponentsMetricsTest.cs +++ b/src/Components/Components/test/ComponentsMetricsTest.cs @@ -29,243 +29,374 @@ public void Constructor_CreatesMetersCorrectly() // Assert Assert.Equal(2, _meterFactory.Meters.Count); - Assert.Equal(ComponentsMetrics.MeterName, _meterFactory.Meters[0].Name); + Assert.Contains(_meterFactory.Meters, m => m.Name == ComponentsMetrics.MeterName); + Assert.Contains(_meterFactory.Meters, m => m.Name == ComponentsMetrics.LifecycleMeterName); } [Fact] - public async Task CaptureEventDurationAsync_RecordsDuration() + public void Navigation_RecordsMetric() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var eventAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event.duration"); + using var navigationCounter = new MetricCollector(_meterFactory, + ComponentsMetrics.MeterName, "aspnetcore.components.navigation"); // Act - var startTime = Stopwatch.GetTimestamp(); - var task = Task.Delay(10); // Create a delay task - await componentsMetrics.CaptureEventDurationAsync(task, startTime, "TestComponent", "MyMethod", "OnClickAsync"); + componentsMetrics.Navigation("TestComponent", "/test-route"); // Assert - var measurements = eventAsyncDurationCollector.GetMeasurementSnapshot(); + var measurements = navigationCounter.GetMeasurementSnapshot(); Assert.Single(measurements); - Assert.True(measurements[0].Value > 0); - Assert.Equal("TestComponent", measurements[0].Tags["aspnetcore.components.type"]); - Assert.Equal("OnClickAsync", measurements[0].Tags["aspnetcore.components.attribute.name"]); - Assert.Equal("MyMethod", measurements[0].Tags["aspnetcore.components.method"]); + 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(_meterFactory, + ComponentsMetrics.MeterName, "aspnetcore.components.navigation"); + + // Act & Assert + Assert.True(componentsMetrics.IsNavigationEnabled); } [Fact] - public async Task CaptureParametersDurationAsync_RecordsDuration() + public async Task CaptureEventDuration_RecordsSuccessMetric() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration"); + using var eventDurationHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.MeterName, "aspnetcore.components.event_handler"); // Act - var startTime = Stopwatch.GetTimestamp(); - var task = Task.Delay(10); // Create a delay task - await componentsMetrics.CaptureParametersDurationAsync(task, startTime, "TestComponent"); + 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 = parametersAsyncDurationCollector.GetMeasurementSnapshot(); + var measurements = eventDurationHistogram.GetMeasurementSnapshot(); Assert.Single(measurements); Assert.True(measurements[0].Value > 0); - Assert.Equal("TestComponent", measurements[0].Tags["aspnetcore.components.type"]); + 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 void BatchDuration_RecordsDuration() + public async Task CaptureEventDuration_RecordsErrorMetric() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var batchDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.duration"); + using var eventDurationHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.MeterName, "aspnetcore.components.event_handler"); // Act - var startTime = Stopwatch.GetTimestamp(); - Thread.Sleep(10); // Add a small delay to ensure a measurable duration - componentsMetrics.BatchDuration(startTime, 50); + 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 = batchDurationCollector.GetMeasurementSnapshot(); + var measurements = eventDurationHistogram.GetMeasurementSnapshot(); Assert.Single(measurements); Assert.True(measurements[0].Value > 0); - Assert.Equal(50, measurements[0].Tags["aspnetcore.components.diff.approximate.length"]); + 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 EventFailed_RecordsException() + public void FailEventSync_RecordsErrorMetric() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event.exceptions"); - - // Create a mock EventCallback - var callback = new EventCallback(new TestComponent(), () => { }); + using var eventDurationHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.MeterName, "aspnetcore.components.event_handler"); + var exception = new InvalidOperationException(); // Act - componentsMetrics.EventFailed("ArgumentException", callback, "OnClick"); + var startTimestamp = Stopwatch.GetTimestamp(); + componentsMetrics.FailEventSync(exception, startTimestamp, + "TestComponent", "OnClick", "onclick"); // Assert - var measurements = eventExceptionCollector.GetMeasurementSnapshot(); + var measurements = eventDurationHistogram.GetMeasurementSnapshot(); Assert.Single(measurements); - Assert.Equal(1, measurements[0].Value); - Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]); - Assert.Equal("OnClick", measurements[0].Tags["aspnetcore.components.attribute.name"]); - Assert.Contains("Microsoft.AspNetCore.Components.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["aspnetcore.components.type"]); + 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 async Task CaptureEventFailedAsync_RecordsException() + public void IsEventEnabled_ReturnsCorrectState() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var eventExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.MeterName, "aspnetcore.components.event.exceptions"); - // Create a mock EventCallback - var callback = new EventCallback(new TestComponent(), () => { }); + // Create a collector to ensure the meter is enabled + using var eventDurationHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.MeterName, "aspnetcore.components.event_handler"); + + // Act & Assert + Assert.True(componentsMetrics.IsEventEnabled); + } - // Create a task that throws an exception - var task = Task.FromException(new InvalidOperationException()); + [Fact] + public async Task CaptureParametersDuration_RecordsSuccessMetric() + { + // Arrange + var componentsMetrics = new ComponentsMetrics(_meterFactory); + using var parametersDurationHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters"); // Act - await componentsMetrics.CaptureEventFailedAsync(task, callback, "OnClickAsync"); + var startTimestamp = Stopwatch.GetTimestamp(); + await Task.Delay(10); // Small delay to ensure measureable duration + await componentsMetrics.CaptureParametersDuration(Task.CompletedTask, startTimestamp, "TestComponent"); // Assert - var measurements = eventExceptionCollector.GetMeasurementSnapshot(); + var measurements = parametersDurationHistogram.GetMeasurementSnapshot(); Assert.Single(measurements); - Assert.Equal(1, measurements[0].Value); - Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]); - Assert.Equal("OnClickAsync", measurements[0].Tags["aspnetcore.components.attribute.name"]); - Assert.Contains("Microsoft.AspNetCore.Components.ComponentsMetricsTest+TestComponent", (string)measurements[0].Tags["aspnetcore.components.type"]); + 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 void PropertiesFailed_RecordsException() + public async Task CaptureParametersDuration_RecordsErrorMetric() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.exceptions"); + using var parametersDurationHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters"); // Act - componentsMetrics.PropertiesFailed("ArgumentException", "TestComponent"); + 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 = parametersExceptionCollector.GetMeasurementSnapshot(); + var measurements = parametersDurationHistogram.GetMeasurementSnapshot(); Assert.Single(measurements); - Assert.Equal(1, measurements[0].Value); - Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]); - Assert.Equal("TestComponent", measurements[0].Tags["aspnetcore.components.type"]); + 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 async Task CapturePropertiesFailedAsync_RecordsException() + public void FailParametersSync_RecordsErrorMetric() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.exceptions"); - - // Create a task that throws an exception - var task = Task.FromException(new InvalidOperationException()); + using var parametersDurationHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters"); + var exception = new InvalidOperationException(); // Act - await componentsMetrics.CapturePropertiesFailedAsync(task, "TestComponent"); + var startTimestamp = Stopwatch.GetTimestamp(); + componentsMetrics.FailParametersSync(exception, startTimestamp, "TestComponent"); // Assert - var measurements = parametersExceptionCollector.GetMeasurementSnapshot(); + var measurements = parametersDurationHistogram.GetMeasurementSnapshot(); Assert.Single(measurements); - Assert.Equal(1, measurements[0].Value); - Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]); - Assert.Equal("TestComponent", measurements[0].Tags["aspnetcore.components.type"]); + 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(_meterFactory, + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters"); + + // Act & Assert + Assert.True(componentsMetrics.IsParametersEnabled); } [Fact] - public void BatchFailed_RecordsException() + public async Task CaptureBatchDuration_RecordsSuccessMetric() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var batchExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.exceptions"); + using var batchDurationHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff"); // Act - componentsMetrics.BatchFailed("ArgumentException"); + var startTimestamp = Stopwatch.GetTimestamp(); + await Task.Delay(10); // Small delay to ensure measureable duration + await componentsMetrics.CaptureBatchDuration(Task.CompletedTask, startTimestamp, 25); // Assert - var measurements = batchExceptionCollector.GetMeasurementSnapshot(); + var measurements = batchDurationHistogram.GetMeasurementSnapshot(); Assert.Single(measurements); - Assert.Equal(1, measurements[0].Value); - Assert.Equal("ArgumentException", measurements[0].Tags["error.type"]); + 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 CaptureBatchFailedAsync_RecordsException() + public async Task CaptureBatchDuration_RecordsErrorMetric() { // Arrange var componentsMetrics = new ComponentsMetrics(_meterFactory); - using var batchExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.exceptions"); + using var batchDurationHistogram = new MetricCollector(_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(); - // Create a task that throws an exception - var task = Task.FromException(new InvalidOperationException()); + 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(_meterFactory, + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.render_diff"); + var exception = new InvalidOperationException(); // Act - await componentsMetrics.CaptureBatchFailedAsync(task); + var startTimestamp = Stopwatch.GetTimestamp(); + componentsMetrics.FailBatchSync(exception, startTimestamp); // Assert - var measurements = batchExceptionCollector.GetMeasurementSnapshot(); + var measurements = batchDurationHistogram.GetMeasurementSnapshot(); Assert.Single(measurements); - Assert.Equal(1, measurements[0].Value); - Assert.Equal("InvalidOperationException", measurements[0].Tags["error.type"]); + 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 EnabledProperties_ReflectMeterState() + public void IsBatchEnabled_ReturnsCorrectState() { // 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.exceptions"); - using var parametersAsyncDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.duration"); - using var parametersExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters.exceptions"); - using var batchDurationCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.duration"); - using var batchExceptionCollector = new MetricCollector(_meterFactory, - ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.rendering.batch.exceptions"); + // Create a collector to ensure the meter is enabled + using var batchDurationHistogram = new MetricCollector(_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(_meterFactory, + ComponentsMetrics.MeterName, "aspnetcore.components.navigation"); + using var eventDurationHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.MeterName, "aspnetcore.components.event_handler"); + using var parametersDurationHistogram = new MetricCollector(_meterFactory, + ComponentsMetrics.LifecycleMeterName, "aspnetcore.components.update_parameters"); + using var batchDurationHistogram = new MetricCollector(_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 - Assert.True(componentsMetrics.IsEventDurationEnabled); - Assert.True(componentsMetrics.IsEventExceptionEnabled); - Assert.True(componentsMetrics.IsParametersDurationEnabled); - Assert.True(componentsMetrics.IsParametersExceptionEnabled); - Assert.True(componentsMetrics.IsBatchDurationEnabled); - Assert.True(componentsMetrics.IsBatchExceptionEnabled); + 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)); } - // Helper class for mock components - public class TestComponent : IComponent, IHandleEvent + [Fact] + public void Dispose_DisposesAllMeters() { - public void Attach(RenderHandle renderHandle) { } - public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => Task.CompletedTask; - public Task SetParametersAsync(ParameterView parameters) => Task.CompletedTask; + // 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/Server/src/Circuits/CircuitMetrics.cs b/src/Components/Server/src/Circuits/CircuitMetrics.cs index b52c0ba878b8..3a4ce6e4a96f 100644 --- a/src/Components/Server/src/Circuits/CircuitMetrics.cs +++ b/src/Components/Server/src/Circuits/CircuitMetrics.cs @@ -24,24 +24,24 @@ public CircuitMetrics(IMeterFactory meterFactory) _meter = meterFactory.Create(MeterName); _circuitTotalCounter = _meter.CreateCounter( - "aspnetcore.components.circuits.count", + "aspnetcore.components.circuit.count", unit: "{circuit}", description: "Total number of circuits."); _circuitActiveCounter = _meter.CreateUpDownCounter( - "aspnetcore.components.circuits.active_circuits", + "aspnetcore.components.circuit.active", unit: "{circuit}", - description: "Number of active circuits."); + description: "Number of active circuits in memory."); _circuitConnectedCounter = _meter.CreateUpDownCounter( - "aspnetcore.components.circuits.connected_circuits", + "aspnetcore.components.circuit.connected", unit: "{circuit}", - description: "Number of disconnected circuits."); + description: "Number of circuits connected to client."); _circuitDuration = _meter.CreateHistogram( - "aspnetcore.components.circuits.duration", + "aspnetcore.components.circuit.duration", unit: "s", - description: "Duration of circuit.", + description: "Duration of circuit and their total count.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorCircuitSecondsBucketBoundaries }); } diff --git a/src/Components/Server/test/Circuits/CircuitMetricsTest.cs b/src/Components/Server/test/Circuits/CircuitMetricsTest.cs index 9124d5d64294..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(_meterFactory, - CircuitMetrics.MeterName, "aspnetcore.components.circuits.count"); using var activeCircuitCounter = new MetricCollector(_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(_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(_meterFactory, - CircuitMetrics.MeterName, "aspnetcore.components.circuits.connected_circuits"); + CircuitMetrics.MeterName, "aspnetcore.components.circuit.connected"); // Act circuitMetrics.OnConnectionDown(); @@ -100,11 +94,11 @@ public async Task OnCircuitDown_UpdatesCountersAndRecordsDuration() // Arrange var circuitMetrics = new CircuitMetrics(_meterFactory); using var activeCircuitCounter = new MetricCollector(_meterFactory, - CircuitMetrics.MeterName, "aspnetcore.components.circuits.active_circuits"); + CircuitMetrics.MeterName, "aspnetcore.components.circuit.active"); using var connectedCircuitCounter = new MetricCollector(_meterFactory, - CircuitMetrics.MeterName, "aspnetcore.components.circuits.connected_circuits"); + CircuitMetrics.MeterName, "aspnetcore.components.circuit.connected"); using var circuitDurationCollector = new MetricCollector(_meterFactory, - CircuitMetrics.MeterName, "aspnetcore.components.circuits.duration"); + CircuitMetrics.MeterName, "aspnetcore.components.circuit.duration"); // Act var startTime = Stopwatch.GetTimestamp(); @@ -135,7 +129,7 @@ public void IsDurationEnabled_ReturnsMeterEnabledState() // Create a collector to ensure the meter is enabled using var circuitDurationCollector = new MetricCollector(_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(_meterFactory, - CircuitMetrics.MeterName, "aspnetcore.components.circuits.count"); using var activeCircuitCounter = new MetricCollector(_meterFactory, - CircuitMetrics.MeterName, "aspnetcore.components.circuits.active_circuits"); + CircuitMetrics.MeterName, "aspnetcore.components.circuit.active"); using var connectedCircuitCounter = new MetricCollector(_meterFactory, - CircuitMetrics.MeterName, "aspnetcore.components.circuits.connected_circuits"); + CircuitMetrics.MeterName, "aspnetcore.components.circuit.connected"); using var circuitDurationCollector = new MetricCollector(_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/Shared/Metrics/MetricsConstants.cs b/src/Shared/Metrics/MetricsConstants.cs index f9b6fed7119b..cebbaba0e6a9 100644 --- a/src/Shared/Metrics/MetricsConstants.cs +++ b/src/Shared/Metrics/MetricsConstants.cs @@ -12,10 +12,7 @@ internal static class MetricsConstants 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 rendering, which should be very fast. - public static readonly IReadOnlyList 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]; - - // For blazor rendering, which should be very fast. - public static readonly IReadOnlyList BlazorBatchDiffSecondsBucketBoundaries = [0.000001, 0.00001, 0.0001, 0.001, 0.01, 0.1, 1]; + public static readonly IReadOnlyList 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 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]; From 2ea50aaa893ac58246fdd71b29481cbdf40e6750 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 7 May 2025 16:21:55 +0200 Subject: [PATCH 24/26] Event -> HandleEvent trace rename --- src/Components/Components/src/ComponentsActivitySource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index a375d3b179a1..7079bf7743a4 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -13,7 +13,7 @@ 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}.Event"; + internal const string OnEventName = $"{Name}.HandleEvent"; private ActivityContext _httpContext; private ActivityContext _circuitContext; From 971f4842674c5769935224e3dd37d7e6f6f5b81f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 7 May 2025 17:05:03 +0200 Subject: [PATCH 25/26] remove aspnetcore.components.circuit.count --- src/Components/Server/src/Circuits/CircuitMetrics.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Components/Server/src/Circuits/CircuitMetrics.cs b/src/Components/Server/src/Circuits/CircuitMetrics.cs index 3a4ce6e4a96f..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 _circuitTotalCounter; private readonly UpDownCounter _circuitActiveCounter; private readonly UpDownCounter _circuitConnectedCounter; private readonly Histogram _circuitDuration; @@ -23,11 +22,6 @@ public CircuitMetrics(IMeterFactory meterFactory) _meter = meterFactory.Create(MeterName); - _circuitTotalCounter = _meter.CreateCounter( - "aspnetcore.components.circuit.count", - unit: "{circuit}", - description: "Total number of circuits."); - _circuitActiveCounter = _meter.CreateUpDownCounter( "aspnetcore.components.circuit.active", unit: "{circuit}", @@ -41,7 +35,7 @@ public CircuitMetrics(IMeterFactory meterFactory) _circuitDuration = _meter.CreateHistogram( "aspnetcore.components.circuit.duration", unit: "s", - description: "Duration of circuit and their total count.", + description: "Duration of circuit lifetime and their total count.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorCircuitSecondsBucketBoundaries }); } @@ -51,10 +45,6 @@ public void OnCircuitOpened() { _circuitActiveCounter.Add(1); } - if (_circuitTotalCounter.Enabled) - { - _circuitTotalCounter.Add(1); - } } public void OnConnectionUp() From db970133168dd9d994e381a06e7cfaaa532d163f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 7 May 2025 18:45:07 +0200 Subject: [PATCH 26/26] Improve descriptions --- src/Components/Components/src/ComponentsMetrics.cs | 6 +++--- .../Components/src/RenderTree/Renderer.cs | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Components/Components/src/ComponentsMetrics.cs b/src/Components/Components/src/ComponentsMetrics.cs index abdbf2d07c66..712d9e395b4c 100644 --- a/src/Components/Components/src/ComponentsMetrics.cs +++ b/src/Components/Components/src/ComponentsMetrics.cs @@ -44,19 +44,19 @@ public ComponentsMetrics(IMeterFactory meterFactory) _eventDuration = _meter.CreateHistogram( "aspnetcore.components.event_handler", unit: "s", - description: "Duration of processing browser event.", + description: "Duration of processing browser event. It includes business logic of the component but not affected child components.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.ShortSecondsBucketBoundaries }); _parametersDuration = _lifeCycleMeter.CreateHistogram( "aspnetcore.components.update_parameters", unit: "s", - description: "Duration of processing component parameters.", + description: "Duration of processing component parameters. It includes business logic of the component.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries }); _batchDuration = _lifeCycleMeter.CreateHistogram( "aspnetcore.components.render_diff", unit: "s", - description: "Duration of rendering DOM update including network and browser time.", + description: "Duration of rendering component tree and producing HTML diff. It includes business logic of the changed components.", advice: new InstrumentAdvice { HistogramBucketBoundaries = MetricsConstants.BlazorRenderingSecondsBucketBoundaries }); } diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 34868b2f5de0..177eccf9bf50 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -450,10 +450,12 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie // collect trace Activity? activity = null; + string receiverName = null; + string methodName = null; if (ComponentActivitySource != null) { - var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; - var methodName = callback.Delegate.Method?.Name; + receiverName ??= (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; + methodName ??= callback.Delegate.Method?.Name; activity = ComponentActivitySource.StartEventActivity(receiverName, methodName, attributeName); } @@ -503,8 +505,8 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie // collect metrics if (ComponentMetrics != null && ComponentMetrics.IsEventEnabled) { - var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; - var methodName = callback.Delegate.Method?.Name; + receiverName ??= (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; + methodName ??= callback.Delegate.Method?.Name; _ = ComponentMetrics.CaptureEventDuration(task, eventStartTimestamp, receiverName, methodName, attributeName); } @@ -518,8 +520,8 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie { if (ComponentMetrics != null && ComponentMetrics.IsEventEnabled) { - var receiverName = (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; - var methodName = callback.Delegate.Method?.Name; + receiverName ??= (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName; + methodName ??= callback.Delegate.Method?.Name; ComponentMetrics.FailEventSync(e, eventStartTimestamp, receiverName, methodName, attributeName); }