Skip to content

[blazor] Diagnostic tracing - feedback #62286

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 66 additions & 100 deletions src/Components/Components/src/ComponentsActivitySource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Microsoft.AspNetCore.Components.Infrastructure;

namespace Microsoft.AspNetCore.Components;

Expand All @@ -11,115 +12,66 @@ 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 ActivitySource ActivitySource { get; } = new ActivitySource(Name);
private ComponentsActivityLinkStore? _componentsActivityLinkStore;

public static ActivityContext CaptureHttpContext()
public void Init(ComponentsActivityLinkStore store)
{
var parentActivity = Activity.Current;
if (parentActivity is not null && parentActivity.OperationName == "Microsoft.AspNetCore.Hosting.HttpRequestIn" && parentActivity.Recorded)
{
return parentActivity.Context;
}
return default;
_componentsActivityLinkStore = store;
}

public Activity? StartCircuitActivity(string circuitId, ActivityContext httpContext)
public ComponentsActivityHandle StartRouteActivity(string componentType, string route)
{
_circuitId = circuitId;

var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null);
var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null);
if (activity is not null)
{
if (activity.IsAllDataRequested)
{
if (_circuitId != null)
{
activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
}
if (httpContext != default)
{
activity.AddLink(new ActivityLink(httpContext));
}
}
activity.DisplayName = $"Circuit {circuitId ?? ""}";
var httpActivity = Activity.Current;
activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}";
Activity.Current = null; // do not inherit the parent activity
activity.Start();
_circuitContext = activity.Context;
}
return activity;
}

public void FailCircuitActivity(Activity? activity, Exception ex)
{
_circuitContext = default;
if (activity != null && !activity.IsStopped)
{
activity.SetTag("error.type", ex.GetType().FullName);
activity.SetStatus(ActivityStatusCode.Error);
activity.Stop();
}
}

public Activity? StartRouteActivity(string componentType, string route)
{
if (_httpContext == default)
{
_httpContext = CaptureHttpContext();
}

var activity = ActivitySource.CreateActivity(OnRouteName, ActivityKind.Internal, parentId: null, null, null);
if (activity is not null)
{
if (activity.IsAllDataRequested)
{
if (_circuitId != null)
{
activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
}
if (componentType != null)
{
activity.SetTag("aspnetcore.components.type", componentType);
}
if (route != null)
{
activity.SetTag("aspnetcore.components.route", route);
}
if (_httpContext != default)
{
activity.AddLink(new ActivityLink(_httpContext));
}
if (_circuitContext != default)
{
activity.AddLink(new ActivityLink(_circuitContext));

// store self link
_componentsActivityLinkStore!.SetActivityContext(ComponentsActivityLinkStore.Route, activity.Context,
new KeyValuePair<string, object?>("aspnetcore.components.route", route));
}
}

activity.DisplayName = $"Route {route ?? "[unknown path]"} -> {componentType ?? "[unknown component]"}";
activity.Start();
_routeContext = activity.Context;
return new ComponentsActivityHandle { Activity = activity, Previous = httpActivity };
}
return activity;
return default;
}

public void StopRouteActivity(ComponentsActivityHandle activityHandle, Exception? ex)
{
StopComponentActivity(ComponentsActivityLinkStore.Route, activityHandle, ex);
}

public Activity? StartEventActivity(string? componentType, string? methodName, string? attributeName)
public ComponentsActivityHandle StartEventActivity(string? componentType, string? methodName, string? attributeName)
{
var activity = ActivitySource.CreateActivity(OnEventName, ActivityKind.Internal, parentId: null, null, null);

if (activity is not null)
{
var previousActivity = Activity.Current;
activity.DisplayName = $"Event {attributeName ?? "[unknown attribute]"} -> {componentType ?? "[unknown component]"}.{methodName ?? "[unknown method]"}";
Activity.Current = null; // do not inherit the parent activity
activity.Start();

if (activity.IsAllDataRequested)
{
if (_circuitId != null)
{
activity.SetTag("aspnetcore.components.circuit.id", _circuitId);
}
if (componentType != null)
{
activity.SetTag("aspnetcore.components.type", componentType);
Expand All @@ -132,46 +84,60 @@ 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)
{
activity.AddLink(new ActivityLink(_circuitContext));
}
if (_routeContext != default)
{
activity.AddLink(new ActivityLink(_routeContext));
}
}

activity.DisplayName = $"Event {attributeName ?? "[unknown attribute]"} -> {componentType ?? "[unknown component]"}.{methodName ?? "[unknown method]"}";
activity.Start();
return new ComponentsActivityHandle { Activity = activity, Previous = previousActivity };
}
return activity;
return default;
}

public static void FailEventActivity(Activity? activity, Exception ex)
public void StopEventActivity(ComponentsActivityHandle activityHandle, Exception? ex)
{
if (activity != null && !activity.IsStopped)
{
activity.SetTag("error.type", ex.GetType().FullName);
activity.SetStatus(ActivityStatusCode.Error);
activity.Stop();
}
StopComponentActivity(ComponentsActivityLinkStore.Event, activityHandle, ex);
}

public static async Task CaptureEventStopAsync(Task task, Activity? activity)
public async Task CaptureEventStopAsync(Task task, ComponentsActivityHandle activityHandle)
{
try
{
await task;
activity?.Stop();
StopEventActivity(activityHandle, null);
}
catch (Exception ex)
{
FailEventActivity(activity, ex);
StopEventActivity(activityHandle, ex);
}
}

private void StopComponentActivity(string category, ComponentsActivityHandle activityHandle, Exception? ex)
{
var activity = activityHandle.Activity;
if (activity != null && !activity.IsStopped)
{
if (ex != null)
{
activity.SetTag("error.type", ex.GetType().FullName);
activity.SetStatus(ActivityStatusCode.Error);
}
if (activity.IsAllDataRequested)
{
_componentsActivityLinkStore!.AddActivityContexts(category, activity);
}
activity.Stop();

if (Activity.Current == null && activityHandle.Previous != null && !activityHandle.Previous.IsStopped)
{
Activity.Current = activityHandle.Previous;
}
}
}
}

/// <summary>
/// Named tuple for restoring the previous activity after stopping the current one.
/// </summary>
internal struct ComponentsActivityHandle
{
public Activity? Previous;
public Activity? Activity;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<Compile Include="$(SharedSourceRoot)Debugger\DictionaryDebugView.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)UrlDecoder\UrlDecoder.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)Metrics\MetricsConstants.cs" LinkBase="Shared" />
<Compile Include="$(SharedSourceRoot)Components\ComponentsActivityLinkStore.cs" LinkBase="Shared" />
</ItemGroup>

<Import Project="Microsoft.AspNetCore.Components.Routing.targets" />
Expand Down Expand Up @@ -79,7 +80,6 @@

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Web" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Server" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Blazor.Build.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Authorization.Tests" />
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Forms.Tests" />
Expand Down
18 changes: 12 additions & 6 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Infrastructure;
using Microsoft.AspNetCore.Components.Reflection;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -15,6 +16,8 @@

namespace Microsoft.AspNetCore.Components.RenderTree;

using CategoryLink = Tuple<ActivityContext, KeyValuePair<string, object?>?>;

/// <summary>
/// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside
/// of the Blazor framework. These types will change in a future release.
Expand All @@ -37,6 +40,8 @@ public abstract partial class Renderer : IDisposable, IAsyncDisposable
private readonly ComponentFactory _componentFactory;
private readonly ComponentsMetrics? _componentsMetrics;
private readonly ComponentsActivitySource? _componentsActivitySource;
private readonly object _activityLinksStore = new Dictionary<string, CategoryLink>(StringComparer.OrdinalIgnoreCase);
internal object ActivityLinksStore => _activityLinksStore;

private Dictionary<int, ParameterView>? _rootComponentsLatestParameters;
private Task? _ongoingQuiescenceTask;
Expand Down Expand Up @@ -96,6 +101,7 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory,
_componentFactory = new ComponentFactory(componentActivator, this);
_componentsMetrics = serviceProvider.GetService<ComponentsMetrics>();
_componentsActivitySource = serviceProvider.GetService<ComponentsActivitySource>();
_componentsActivitySource?.Init(new ComponentsActivityLinkStore(this));

ServiceProviderCascadingValueSuppliers = serviceProvider.GetService<ICascadingValueSupplier>() is null
? Array.Empty<ICascadingValueSupplier>()
Expand Down Expand Up @@ -448,14 +454,14 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
var (renderedByComponentId, callback, attributeName) = GetRequiredEventBindingEntry(eventHandlerId);

// collect trace
Activity? activity = null;
ComponentsActivityHandle activityHandle = default;
string receiverName = null;
string methodName = null;
if (ComponentActivitySource != null)
{
receiverName ??= (callback.Receiver?.GetType() ?? callback.Delegate.Target?.GetType())?.FullName;
methodName ??= callback.Delegate.Method?.Name;
activity = ComponentActivitySource.StartEventActivity(receiverName, methodName, attributeName);
activityHandle = ComponentActivitySource.StartEventActivity(receiverName, methodName, attributeName);
}

var eventStartTimestamp = ComponentMetrics != null && ComponentMetrics.IsEventEnabled ? Stopwatch.GetTimestamp() : 0;
Expand Down Expand Up @@ -510,9 +516,9 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
}

// stop activity/trace
if (ComponentActivitySource != null && activity != null)
if (ComponentActivitySource != null && activityHandle.Activity != null)
{
_ = ComponentsActivitySource.CaptureEventStopAsync(task, activity);
_ = ComponentActivitySource.CaptureEventStopAsync(task, activityHandle);
}
}
catch (Exception e)
Expand All @@ -524,9 +530,9 @@ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fie
ComponentMetrics.FailEventSync(e, eventStartTimestamp, receiverName, methodName, attributeName);
}

if (ComponentActivitySource != null && activity != null)
if (ComponentActivitySource != null && activityHandle.Activity != null)
{
ComponentsActivitySource.FailEventActivity(activity, e);
ComponentActivitySource.StopEventActivity(activityHandle, e);
}

HandleExceptionViaErrorBoundary(e, receiverComponentState);
Expand Down
Loading
Loading