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.