diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 7079bf7743a4..249f994004aa 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -11,79 +11,36 @@ namespace Microsoft.AspNetCore.Components; internal class ComponentsActivitySource { internal const string Name = "Microsoft.AspNetCore.Components"; - internal const string OnCircuitName = $"{Name}.CircuitStart"; internal const string OnRouteName = $"{Name}.RouteChange"; internal const string OnEventName = $"{Name}.HandleEvent"; - private ActivityContext _httpContext; - private ActivityContext _circuitContext; - private string? _circuitId; private ActivityContext _routeContext; + private Activity? _capturedActivity; private ActivitySource ActivitySource { get; } = new ActivitySource(Name); - public static ActivityContext CaptureHttpContext() + /// + /// Initializes the ComponentsActivitySource with a captured activity for linking. + /// + /// Activity to link with component activities. + public void Initialize(Activity? capturedActivity) { - var parentActivity = Activity.Current; - if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded) - { - return parentActivity.Context; - } - return default; - } - - public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext) - { - _circuitId = circuitId; - - var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null); - if (activity is not null) - { - if (activity.IsAllDataRequested) - { - if (_circuitId != null) - { - activity.SetTag("aspnetcore.components.circuit.id", _circuitId); - } - if (httpContext != default) - { - activity.AddLink(new ActivityLink(httpContext)); - } - } - activity.DisplayName = $"Circuit {circuitId ?? ""}"; - activity.Start(); - _circuitContext = activity.Context; - } - return activity; - } - - public void FailCircuitActivity(Activity? activity, Exception ex) - { - _circuitContext = default; - if (activity != null && !activity.IsStopped) - { - activity.SetTag("error.type", ex.GetType().FullName); - activity.SetStatus(ActivityStatusCode.Error); - activity.Stop(); - } + _capturedActivity = capturedActivity; } public Activity? StartRouteActivity(string componentType, string route) { - if (_httpContext == default) - { - _httpContext = CaptureHttpContext(); - } - var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null); if (activity is not null) { if (activity.IsAllDataRequested) { - if (_circuitId != null) + // Copy any circuit ID from captured activity if present + if (_capturedActivity != null && _capturedActivity.GetTagItem("aspnetcore.components.circuit.id") is string circuitId) { - activity.SetTag("aspnetcore.components.circuit.id", _circuitId); + activity.SetTag("aspnetcore.components.circuit.id", circuitId); } + if (componentType != null) { activity.SetTag("aspnetcore.components.type", componentType); @@ -92,13 +49,9 @@ public void FailCircuitActivity(Activity? activity, Exception ex) { activity.SetTag("aspnetcore.components.route", route); } - if (_httpContext != default) + if (_capturedActivity != null) { - activity.AddLink(new ActivityLink(_httpContext)); - } - if (_circuitContext != default) - { - activity.AddLink(new ActivityLink(_circuitContext)); + activity.AddLink(new ActivityLink(_capturedActivity.Context)); } } @@ -116,10 +69,12 @@ public void FailCircuitActivity(Activity? activity, Exception ex) { if (activity.IsAllDataRequested) { - if (_circuitId != null) + // Copy any circuit ID from captured activity if present + if (_capturedActivity != null && _capturedActivity.GetTagItem("aspnetcore.components.circuit.id") is string circuitId) { - activity.SetTag("aspnetcore.components.circuit.id", _circuitId); + activity.SetTag("aspnetcore.components.circuit.id", circuitId); } + if (componentType != null) { activity.SetTag("aspnetcore.components.type", componentType); @@ -132,13 +87,9 @@ public void FailCircuitActivity(Activity? activity, Exception ex) { activity.SetTag("aspnetcore.components.attribute.name", attributeName); } - if (_httpContext != default) - { - activity.AddLink(new ActivityLink(_httpContext)); - } - if (_circuitContext != default) + if (_capturedActivity != null) { - activity.AddLink(new ActivityLink(_circuitContext)); + activity.AddLink(new ActivityLink(_capturedActivity.Context)); } if (_routeContext != default) { diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index 07fcc360fd7e..ca3286f8b6c2 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -79,7 +79,6 @@ - diff --git a/src/Components/Server/src/Circuits/CircuitActivitySource.cs b/src/Components/Server/src/Circuits/CircuitActivitySource.cs new file mode 100644 index 000000000000..60e84f57b45a --- /dev/null +++ b/src/Components/Server/src/Circuits/CircuitActivitySource.cs @@ -0,0 +1,86 @@ +// 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.Server.Circuits; + +/// +/// Activity source for circuit-related activities. +/// +public class CircuitActivitySource +{ + internal const string Name = "Microsoft.AspNetCore.Components.Server"; + internal const string OnCircuitName = $"{Name}.CircuitStart"; + + private ActivitySource ActivitySource { get; } = new ActivitySource(Name); + + /// + /// Creates and starts a new activity for circuit initialization. + /// + /// The ID of the circuit being initialized. + /// The HTTP context associated with the request that created the circuit. + /// The created activity. + public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext) + { + var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null); + if (activity is not null) + { + if (activity.IsAllDataRequested) + { + if (circuitId != null) + { + activity.SetTag("aspnetcore.components.circuit.id", circuitId); + } + if (httpContext != default) + { + activity.AddLink(new ActivityLink(httpContext)); + } + } + activity.DisplayName = $"Circuit {circuitId ?? ""}"; + activity.Start(); + } + return activity; + } + + /// + /// Stops a circuit activity that was previously started. + /// + /// The activity to stop. + public void StopCircuitActivity(Activity? activity) + { + if (activity != null && !activity.IsStopped) + { + activity.Stop(); + } + } + + /// + /// Marks a circuit activity as failed and stops it. + /// + /// The activity to mark as failed. + /// The exception that caused the failure. + public void FailCircuitActivity(Activity? activity, Exception ex) + { + if (activity != null && !activity.IsStopped) + { + activity.SetTag("error.type", ex.GetType().FullName); + activity.SetStatus(ActivityStatusCode.Error); + activity.Stop(); + } + } + + /// + /// Captures the current HTTP context activity. + /// + /// The captured HTTP context activity. + public static ActivityContext CaptureHttpContext() + { + var parentActivity = Activity.Current; + if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded) + { + return parentActivity.Context; + } + return default; + } +} \ No newline at end of file diff --git a/src/Components/Server/src/Circuits/CircuitActivitySourceServiceCollectionExtensions.cs b/src/Components/Server/src/Circuits/CircuitActivitySourceServiceCollectionExtensions.cs new file mode 100644 index 000000000000..a1a9b032e53f --- /dev/null +++ b/src/Components/Server/src/Circuits/CircuitActivitySourceServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +// 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.Server.Circuits; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for adding to the service collection. +/// +internal static class CircuitActivitySourceServiceCollectionExtensions +{ + /// + /// Adds to the service collection. + /// + /// The . + /// The . + public static IServiceCollection AddCircuitActivitySource(this IServiceCollection services) + { + services.TryAddSingleton(); + return services; + } +} \ No newline at end of file diff --git a/src/Components/Server/src/Circuits/CircuitFactory.cs b/src/Components/Server/src/Circuits/CircuitFactory.cs index 6683c2e20d75..e20b7b85e742 100644 --- a/src/Components/Server/src/Circuits/CircuitFactory.cs +++ b/src/Components/Server/src/Circuits/CircuitFactory.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.Linq; using System.Security.Claims; using Microsoft.AspNetCore.Components.Infrastructure; @@ -45,7 +46,8 @@ public async ValueTask CreateCircuitHostAsync( string uri, ClaimsPrincipal user, IPersistentComponentStateStore store, - ResourceAssetCollection resourceCollection) + ResourceAssetCollection resourceCollection, + Activity? circuitActivity = null) { var scope = _scopeFactory.CreateAsyncScope(); var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService(); @@ -67,6 +69,11 @@ public async ValueTask CreateCircuitHostAsync( navigationManager.Initialize(baseUri, uri); } var componentsActivitySource = scope.ServiceProvider.GetService(); + + // We don't need to explicitly initialize the ComponentsActivitySource here anymore + // since the RemoteRenderer will capture Activity.Current and initialize it + + var circuitActivitySource = scope.ServiceProvider.GetService(); if (components.Count > 0) { @@ -99,8 +106,10 @@ public async ValueTask CreateCircuitHostAsync( .OrderBy(h => h.Order) .ToArray(); + var circuitId = _circuitIdFactory.CreateCircuitId(); + var circuitHost = new CircuitHost( - _circuitIdFactory.CreateCircuitId(), + circuitId, scope, _options, client, @@ -110,7 +119,8 @@ public async ValueTask CreateCircuitHostAsync( navigationManager, circuitHandlers, _circuitMetrics, - componentsActivitySource, + circuitActivitySource, + circuitActivity, _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 38b50461ce3e..b97a21128ba5 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -25,7 +25,8 @@ internal partial class CircuitHost : IAsyncDisposable private readonly RemoteNavigationManager _navigationManager; private readonly ILogger _logger; private readonly CircuitMetrics? _circuitMetrics; - private readonly ComponentsActivitySource? _componentsActivitySource; + private readonly CircuitActivitySource? _circuitActivitySource; + private readonly Activity? _circuitActivity; private Func, Task> _dispatchInboundActivity; private CircuitHandler[] _circuitHandlers; private bool _initialized; @@ -52,7 +53,8 @@ public CircuitHost( RemoteNavigationManager navigationManager, CircuitHandler[] circuitHandlers, CircuitMetrics? circuitMetrics, - ComponentsActivitySource? componentsActivitySource, + CircuitActivitySource? circuitActivitySource, + Activity? circuitActivity, ILogger logger) { CircuitId = circuitId; @@ -71,7 +73,8 @@ public CircuitHost( _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager)); _circuitHandlers = circuitHandlers ?? throw new ArgumentNullException(nameof(circuitHandlers)); _circuitMetrics = circuitMetrics; - _componentsActivitySource = componentsActivitySource; + _circuitActivitySource = circuitActivitySource; + _circuitActivity = circuitActivity; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); Services = scope.ServiceProvider; @@ -124,7 +127,6 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, A { _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 @@ -169,12 +171,10 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, A _isFirstUpdate = Descriptors.Count == 0; Log.InitializationSucceeded(_logger); - - activity?.Stop(); } catch (Exception ex) { - _componentsActivitySource?.FailCircuitActivity(activity, ex); + _circuitActivitySource?.FailCircuitActivity(_circuitActivity, ex); // Report errors asynchronously. InitializeAsync is designed not to throw. Log.InitializationFailed(_logger, ex); @@ -337,6 +337,9 @@ private async Task OnCircuitDownAsync(CancellationToken cancellationToken) { Log.CircuitClosed(_logger, CircuitId); _circuitMetrics?.OnCircuitDown(_startTime, Stopwatch.GetTimestamp()); + + // Stop the circuit activity when the circuit is closed + _circuitActivitySource?.StopCircuitActivity(_circuitActivity); List exceptions = null; diff --git a/src/Components/Server/src/Circuits/ICircuitFactory.cs b/src/Components/Server/src/Circuits/ICircuitFactory.cs index f2627dfd2ad5..b66df27e1572 100644 --- a/src/Components/Server/src/Circuits/ICircuitFactory.cs +++ b/src/Components/Server/src/Circuits/ICircuitFactory.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.Security.Claims; namespace Microsoft.AspNetCore.Components.Server.Circuits; @@ -14,5 +15,6 @@ ValueTask CreateCircuitHostAsync( string uri, ClaimsPrincipal user, IPersistentComponentStateStore store, - ResourceAssetCollection resourceCollection); + ResourceAssetCollection resourceCollection, + Activity? circuitActivity = null); } diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 7f2345bff74a..3c894fa8ac3f 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.Components.RenderTree; @@ -28,6 +29,7 @@ internal partial class RemoteRenderer : WebRenderer internal readonly ConcurrentQueue _unacknowledgedRenderBatches = new ConcurrentQueue(); private long _nextRenderId = 1; private bool _disposing; + private readonly Activity? _capturedActivity; /// /// Notifies when a rendering exception occurred. @@ -54,6 +56,11 @@ public RemoteRenderer( _serverComponentDeserializer = serverComponentDeserializer; _logger = logger; _resourceCollection = resourceCollection; + _capturedActivity = Activity.Current; // Capture the current activity + + // Initialize ComponentsActivitySource with the captured activity + var componentsActivitySource = serviceProvider.GetService(); + componentsActivitySource?.Initialize(_capturedActivity); ElementReferenceContext = jsRuntime.ElementReferenceContext; } @@ -369,7 +376,7 @@ private async Task CaptureAsyncExceptions(Task task) } } - private static new partial class Log + private static 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 84561349ee48..93c8a64e75c8 100644 --- a/src/Components/Server/src/ComponentHub.cs +++ b/src/Components/Server/src/ComponentHub.cs @@ -45,6 +45,7 @@ internal sealed partial class ComponentHub : Hub private readonly ICircuitHandleRegistry _circuitHandleRegistry; private readonly ILogger _logger; private readonly ActivityContext _httpContext; + private readonly CircuitActivitySource _circuitActivitySource; public ComponentHub( IServerComponentDeserializer serializer, @@ -53,6 +54,7 @@ public ComponentHub( CircuitIdFactory circuitIdFactory, CircuitRegistry circuitRegistry, ICircuitHandleRegistry circuitHandleRegistry, + CircuitActivitySource circuitActivitySource, ILogger logger) { _serverComponentSerializer = serializer; @@ -61,8 +63,9 @@ public ComponentHub( _circuitIdFactory = circuitIdFactory; _circuitRegistry = circuitRegistry; _circuitHandleRegistry = circuitHandleRegistry; + _circuitActivitySource = circuitActivitySource; _logger = logger; - _httpContext = ComponentsActivitySource.CaptureHttpContext(); + _httpContext = CaptureHttpContext(); } /// @@ -122,7 +125,13 @@ public async ValueTask StartCircuit(string baseUri, string uri, string s try { + // Create the circuit ID early so it can be added to the activity var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId); + var circuitId = _circuitIdFactory.CreateCircuitId(); + + // Start circuit activity here in ComponentHub + var circuitActivity = _circuitActivitySource.StartCircuitActivity(circuitId.Id, _httpContext); + var store = !string.IsNullOrEmpty(applicationState) ? new ProtectedPrerenderComponentApplicationStore(applicationState, _dataProtectionProvider) : new ProtectedPrerenderComponentApplicationStore(_dataProtectionProvider); @@ -134,7 +143,8 @@ public async ValueTask StartCircuit(string baseUri, string uri, string s uri, Context.User, store, - resourceCollection); + resourceCollection, + circuitActivity); // Fire-and-forget the initialization process, because we can't block the // SignalR message loop (we'd get a deadlock if any of the initialization @@ -420,4 +430,14 @@ public static void InvalidCircuitId(ILogger logger, string circuitSecret) InvalidCircuitIdCore(logger, circuitSecret); } } + + private static ActivityContext CaptureHttpContext() + { + var parentActivity = Activity.Current; + if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded) + { + return parentActivity.Context; + } + return default; + } } diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs index 4c6eb34d27f6..2462b0ebb9ef 100644 --- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Forms; +using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Components.Server.BlazorPack; @@ -64,7 +65,7 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); @@ -77,6 +78,12 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti services.TryAddSingleton(); + // Add CircuitActivitySource for circuit-related tracing + services.AddCircuitActivitySource(); + + // Add ComponentsActivitySource for component-level tracing + ComponentsMetricsServiceCollectionExtensions.AddComponentsTracing(services); + // Standard blazor hosting services implementations // // These intentionally replace the non-interactive versions included in MVC.