diff --git a/src/Components/Components/src/ComponentsActivitySource.cs b/src/Components/Components/src/ComponentsActivitySource.cs index 7079bf7743a4..bf17ea8ae0ee 100644 --- a/src/Components/Components/src/ComponentsActivitySource.cs +++ b/src/Components/Components/src/ComponentsActivitySource.cs @@ -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; @@ -11,79 +12,29 @@ 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); @@ -91,35 +42,36 @@ public void FailCircuitActivity(Activity? activity, Exception ex) 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("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); @@ -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; + } + } + } +} + +/// +/// Named tuple for restoring the previous activity after stopping the current one. +/// +internal struct ComponentsActivityHandle +{ + public Activity? Previous; + public Activity? Activity; } diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index 07fcc360fd7e..ea4689586d69 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -24,6 +24,7 @@ + @@ -79,7 +80,6 @@ - diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index ce14869a5d45..d0a62f240059 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -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; @@ -15,6 +16,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree; +using CategoryLink = Tuple?>; + /// /// 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. @@ -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(StringComparer.OrdinalIgnoreCase); + internal object ActivityLinksStore => _activityLinksStore; private Dictionary? _rootComponentsLatestParameters; private Task? _ongoingQuiescenceTask; @@ -96,6 +101,7 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, _componentFactory = new ComponentFactory(componentActivator, this); _componentsMetrics = serviceProvider.GetService(); _componentsActivitySource = serviceProvider.GetService(); + _componentsActivitySource?.Init(new ComponentsActivityLinkStore(this)); ServiceProviderCascadingValueSuppliers = serviceProvider.GetService() is null ? Array.Empty() @@ -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; @@ -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) @@ -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); diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 51fb3fc823d2..7a73ead53ea9 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -3,7 +3,6 @@ #nullable disable warnings -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; @@ -223,12 +222,12 @@ internal virtual void Refresh(bool isNavigationIntercepted) var relativePath = NavigationManager.ToBaseRelativePath(_locationAbsolute.AsSpan()); var locationPathSpan = TrimQueryOrHash(relativePath); var locationPath = $"/{locationPathSpan}"; - Activity? activity; + ComponentsActivityHandle activityHandle; // In order to avoid routing twice we check for RouteData if (RoutingStateProvider?.RouteData is { } endpointRouteData) { - activity = RecordDiagnostics(endpointRouteData.PageType.FullName, endpointRouteData.Template); + activityHandle = 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. @@ -241,7 +240,7 @@ internal virtual void Refresh(bool isNavigationIntercepted) endpointRouteData = RouteTable.ProcessParameters(endpointRouteData); _renderHandle.Render(Found(endpointRouteData)); - activity?.Stop(); + _renderHandle.ComponentActivitySource?.StopRouteActivity(activityHandle, null); return; } @@ -258,7 +257,7 @@ internal virtual void Refresh(bool isNavigationIntercepted) $"does not implement {typeof(IComponent).FullName}."); } - activity = RecordDiagnostics(context.Handler.FullName, context.Entry.RoutePattern.RawText); + activityHandle = RecordDiagnostics(context.Handler.FullName, context.Entry.RoutePattern.RawText); Log.NavigatingToComponent(_logger, context.Handler, locationPath, _baseUri); @@ -279,7 +278,7 @@ internal virtual void Refresh(bool isNavigationIntercepted) { if (!isNavigationIntercepted) { - activity = RecordDiagnostics("NotFound", "NotFound"); + activityHandle = RecordDiagnostics("NotFound", "NotFound"); Log.DisplayingNotFound(_logger, locationPath, _baseUri); @@ -290,22 +289,21 @@ internal virtual void Refresh(bool isNavigationIntercepted) } else { - activity = RecordDiagnostics("External", "External"); + activityHandle = RecordDiagnostics("External", "External"); Log.NavigatingToExternalUri(_logger, _locationAbsolute, locationPath, _baseUri); NavigationManager.NavigateTo(_locationAbsolute, forceLoad: true); } } - activity?.Stop(); - + _renderHandle.ComponentActivitySource?.StopRouteActivity(activityHandle, null); } - private Activity? RecordDiagnostics(string componentType, string template) + private ComponentsActivityHandle RecordDiagnostics(string componentType, string template) { - Activity? activity = null; + ComponentsActivityHandle activityHandle = default; if (_renderHandle.ComponentActivitySource != null) { - activity = _renderHandle.ComponentActivitySource.StartRouteActivity(componentType, template); + activityHandle = _renderHandle.ComponentActivitySource.StartRouteActivity(componentType, template); } if (_renderHandle.ComponentMetrics != null && _renderHandle.ComponentMetrics.IsNavigationEnabled) @@ -313,7 +311,7 @@ internal virtual void Refresh(bool isNavigationIntercepted) _renderHandle.ComponentMetrics.Navigation(componentType, template); } - return activity; + return activityHandle; } private static void DefaultNotFoundContent(RenderTreeBuilder builder) diff --git a/src/Components/Components/test/ComponentsActivitySourceTest.cs b/src/Components/Components/test/ComponentsActivitySourceTest.cs index 660bf9428264..f51b5fac4f89 100644 --- a/src/Components/Components/test/ComponentsActivitySourceTest.cs +++ b/src/Components/Components/test/ComponentsActivitySourceTest.cs @@ -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; @@ -28,92 +29,29 @@ public void Constructor_CreatesActivitySourceCorrectly() { // Arrange & Act var componentsActivitySource = new ComponentsActivitySource(); + var linkstore = new ComponentsActivityLinkStore(null); + componentsActivitySource.Init(linkstore); // 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.Internal, activity.Kind); - Assert.True(activity.IsAllDataRequested); - Assert.Equal(circuitId, activity.GetTagItem("aspnetcore.components.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 linkstore = new ComponentsActivityLinkStore(null); + componentsActivitySource.Init(linkstore); var componentType = "TestComponent"; var route = "/test-route"; // First set up a circuit context - componentsActivitySource.StartCircuitActivity("test-circuit-id", default); + linkstore.SetActivityContext(ComponentsActivityLinkStore.Circuit, new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded), new KeyValuePair("aspnetcore.components.circuit.id", "test-circuit-id")); // Act - var activity = componentsActivitySource.StartRouteActivity(componentType, route); + var activityHandle = componentsActivitySource.StartRouteActivity(componentType, route); + var activity = activityHandle.Activity; // Assert Assert.NotNull(activity); @@ -123,8 +61,13 @@ public void StartRouteActivity_CreatesAndStartsActivity() Assert.True(activity.IsAllDataRequested); 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); + + componentsActivitySource.StopRouteActivity(activityHandle, null); + Assert.True(activity.IsStopped); + Assert.Equal("test-circuit-id", activity.GetTagItem("aspnetcore.components.circuit.id")); + Assert.Single(activity.Links); + } [Fact] @@ -132,16 +75,19 @@ public void StartEventActivity_CreatesAndStartsActivity() { // Arrange var componentsActivitySource = new ComponentsActivitySource(); + var linkstore = new ComponentsActivityLinkStore(null); + componentsActivitySource.Init(linkstore); var componentType = "TestComponent"; var methodName = "OnClick"; var attributeName = "onclick"; // First set up a circuit and route context - componentsActivitySource.StartCircuitActivity("test-circuit-id", default); + linkstore.SetActivityContext(ComponentsActivityLinkStore.Circuit, default, new KeyValuePair("aspnetcore.components.circuit.id", "test-circuit-id")); componentsActivitySource.StartRouteActivity("ParentComponent", "/parent"); // Act - var activity = componentsActivitySource.StartEventActivity(componentType, methodName, attributeName); + var activityHandle = componentsActivitySource.StartEventActivity(componentType, methodName, attributeName); + var activity = activityHandle.Activity; // Assert Assert.NotNull(activity); @@ -152,8 +98,12 @@ public void StartEventActivity_CreatesAndStartsActivity() 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); + + componentsActivitySource.StopRouteActivity(activityHandle, null); + Assert.True(activity.IsStopped); + Assert.Equal("test-circuit-id", activity.GetTagItem("aspnetcore.components.circuit.id")); + Assert.Empty(activity.Links); } [Fact] @@ -161,11 +111,14 @@ public void FailEventActivity_SetsErrorStatusAndStopsActivity() { // Arrange var componentsActivitySource = new ComponentsActivitySource(); - var activity = componentsActivitySource.StartEventActivity("TestComponent", "OnClick", "onclick"); + var linkstore = new ComponentsActivityLinkStore(null); + componentsActivitySource.Init(linkstore); + var activityHandle = componentsActivitySource.StartEventActivity("TestComponent", "OnClick", "onclick"); + var activity = activityHandle.Activity; var exception = new InvalidOperationException("Test exception"); // Act - ComponentsActivitySource.FailEventActivity(activity, exception); + componentsActivitySource.StopEventActivity(activityHandle, exception); // Assert Assert.True(activity!.IsStopped); @@ -178,11 +131,14 @@ public async Task CaptureEventStopAsync_StopsActivityOnSuccessfulTask() { // Arrange var componentsActivitySource = new ComponentsActivitySource(); - var activity = componentsActivitySource.StartEventActivity("TestComponent", "OnClick", "onclick"); + var linkstore = new ComponentsActivityLinkStore(null); + componentsActivitySource.Init(linkstore); + var activityHandle = componentsActivitySource.StartEventActivity("TestComponent", "OnClick", "onclick"); + var activity = activityHandle.Activity; var task = Task.CompletedTask; // Act - await ComponentsActivitySource.CaptureEventStopAsync(task, activity); + await componentsActivitySource.CaptureEventStopAsync(task, activityHandle); // Assert Assert.True(activity!.IsStopped); @@ -194,12 +150,15 @@ public async Task CaptureEventStopAsync_FailsActivityOnException() { // Arrange var componentsActivitySource = new ComponentsActivitySource(); - var activity = componentsActivitySource.StartEventActivity("TestComponent", "OnClick", "onclick"); + var linkstore = new ComponentsActivityLinkStore(null); + componentsActivitySource.Init(linkstore); + var activityHandle = componentsActivitySource.StartEventActivity("TestComponent", "OnClick", "onclick"); + var activity = activityHandle.Activity; var exception = new InvalidOperationException("Test exception"); var task = Task.FromException(exception); // Act - await ComponentsActivitySource.CaptureEventStopAsync(task, activity); + await componentsActivitySource.CaptureEventStopAsync(task, activityHandle); // Assert Assert.True(activity!.IsStopped); @@ -207,28 +166,17 @@ public async Task CaptureEventStopAsync_FailsActivityOnException() 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(); + var linkstore = new ComponentsActivityLinkStore(null); + componentsActivitySource.Init(linkstore); // Act - var activity = componentsActivitySource.StartRouteActivity(null, null); + var activityHandle = componentsActivitySource.StartRouteActivity(null, null); + var activity = activityHandle.Activity; // Assert Assert.NotNull(activity); @@ -240,9 +188,12 @@ public void StartEventActivity_HandlesNullValues() { // Arrange var componentsActivitySource = new ComponentsActivitySource(); + var linkstore = new ComponentsActivityLinkStore(null); + componentsActivitySource.Init(linkstore); // Act - var activity = componentsActivitySource.StartEventActivity(null, null, null); + var activityHandle = componentsActivitySource.StartEventActivity(null, null, null); + var activity = activityHandle.Activity; // Assert Assert.NotNull(activity); diff --git a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj index aa481325046c..40ad0ccab23a 100644 --- a/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj +++ b/src/Components/Endpoints/src/Microsoft.AspNetCore.Components.Endpoints.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index c357d5ea2d80..a52f6e274410 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -7,6 +7,7 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Components.Endpoints.Rendering; +using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -21,10 +22,12 @@ internal partial class RazorComponentEndpointInvoker : IRazorComponentEndpointIn { private readonly EndpointHtmlRenderer _renderer; private readonly ILogger _logger; + private readonly ComponentsActivityLinkStore _activityLinkStore; public RazorComponentEndpointInvoker(EndpointHtmlRenderer renderer, ILogger logger) { _renderer = renderer; + _activityLinkStore = new ComponentsActivityLinkStore(renderer); _logger = logger; } @@ -41,6 +44,7 @@ private async Task RenderComponentCore(HttpContext context) var isErrorHandler = context.Features.Get() is not null; var hasStatusCodePage = context.Features.Get() is not null; var isReExecuted = context.Features.Get() is not null; + var httpActivityContext = context.Features.Get()?.Activity.Context ?? default; if (isErrorHandler) { Log.InteractivityDisabledForErrorHandling(_logger); @@ -80,6 +84,11 @@ private async Task RenderComponentCore(HttpContext context) return Task.CompletedTask; }); + if (httpActivityContext != default) + { + _activityLinkStore.SetActivityContext(ComponentsActivityLinkStore.Http, httpActivityContext, null); + } + await _renderer.InitializeStandardComponentServicesAsync( context, componentType: pageComponent, diff --git a/src/Components/Server/src/Circuits/CircuitActivitySource.cs b/src/Components/Server/src/Circuits/CircuitActivitySource.cs new file mode 100644 index 000000000000..ad554bad2e7c --- /dev/null +++ b/src/Components/Server/src/Circuits/CircuitActivitySource.cs @@ -0,0 +1,88 @@ +// 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 Microsoft.AspNetCore.Components.Infrastructure.Server; + +internal class CircuitActivitySource +{ + internal const string Name = "Microsoft.AspNetCore.Components.Server.Circuits"; + internal const string OnCircuitName = $"{Name}.CircuitStart"; + + private ComponentsActivityLinkStore? _activityLinkStore; + + private ActivitySource ActivitySource { get; } = new ActivitySource(Name); + + public void Init(ComponentsActivityLinkStore store) + { + _activityLinkStore = store; + } + + public CircuitActivityHandle StartCircuitActivity(string circuitId, ActivityContext httpActivityContext) + { + var activity = ActivitySource.CreateActivity(OnCircuitName, ActivityKind.Internal, parentId: null, null, null); + if (activity is not null) + { + var signalRActivity = Activity.Current; + activity.DisplayName = $"Circuit {circuitId ?? ""}"; + Activity.Current = null; // do not inherit the parent activity + activity.Start(); + + if (activity.IsAllDataRequested) + { + if (circuitId != null) + { + activity.SetTag("aspnetcore.components.circuit.id", circuitId); + + // store self link + _activityLinkStore.SetActivityContext(ComponentsActivityLinkStore.Circuit, activity.Context, + new KeyValuePair("aspnetcore.components.circuit.id", circuitId)); + } + if (httpActivityContext != default) + { + // store the http link + _activityLinkStore.SetActivityContext(ComponentsActivityLinkStore.Http, httpActivityContext, null); + } + if (signalRActivity != null && signalRActivity.Source.Name == "Microsoft.AspNetCore.SignalR.Server") + { + // store the SignalR link + _activityLinkStore.SetActivityContext(ComponentsActivityLinkStore.SignalR, signalRActivity.Context, null); + } + } + return new CircuitActivityHandle { Previous = signalRActivity, Activity = activity }; + } + return default; + } + + public void StopCircuitActivity(CircuitActivityHandle 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) + { + _activityLinkStore.AddActivityContexts(ComponentsActivityLinkStore.Circuit, activity); + } + activityHandle.Activity.Stop(); + + if (Activity.Current == null && activityHandle.Previous != null && !activityHandle.Previous.IsStopped) + { + Activity.Current = activityHandle.Previous; + } + } + } +} + +/// +/// Named tuple for restoring the previous activity after stopping the current one. +/// +internal struct CircuitActivityHandle +{ + public Activity? Previous; + public Activity? Activity; +} diff --git a/src/Components/Server/src/Circuits/CircuitFactory.cs b/src/Components/Server/src/Circuits/CircuitFactory.cs index 6683c2e20d75..9801ae5715c1 100644 --- a/src/Components/Server/src/Circuits/CircuitFactory.cs +++ b/src/Components/Server/src/Circuits/CircuitFactory.cs @@ -20,13 +20,13 @@ internal sealed partial class CircuitFactory : ICircuitFactory private readonly CircuitIdFactory _circuitIdFactory; private readonly CircuitOptions _options; private readonly ILogger _logger; - private readonly CircuitMetrics? _circuitMetrics; + private readonly CircuitMetrics _circuitMetrics; public CircuitFactory( IServiceScopeFactory scopeFactory, ILoggerFactory loggerFactory, CircuitIdFactory circuitIdFactory, - CircuitMetrics? circuitMetrics, + CircuitMetrics circuitMetrics, IOptions options) { _scopeFactory = scopeFactory; @@ -54,6 +54,8 @@ public async ValueTask CreateCircuitHostAsync( var navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService(); var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService(); var scrollToLocationHash = (RemoteScrollToLocationHash)scope.ServiceProvider.GetRequiredService(); + var circuitActivitySource = scope.ServiceProvider.GetRequiredService(); + if (client.Connected) { navigationManager.AttachJsRuntime(jsRuntime); @@ -66,7 +68,6 @@ public async ValueTask CreateCircuitHostAsync( { navigationManager.Initialize(baseUri, uri); } - var componentsActivitySource = scope.ServiceProvider.GetService(); if (components.Count > 0) { @@ -91,6 +92,8 @@ public async ValueTask CreateCircuitHostAsync( jsComponentInterop, resourceCollection); + circuitActivitySource.Init(new Infrastructure.Server.ComponentsActivityLinkStore(renderer)); + // In Blazor Server we have already restored the app state, so we can get the handlers from DI. // In Blazor Web the state is provided in the first call to UpdateRootComponents, so we need to // delay creating the handlers until then. Otherwise, a handler would be able to access the state @@ -110,7 +113,7 @@ public async ValueTask CreateCircuitHostAsync( navigationManager, circuitHandlers, _circuitMetrics, - componentsActivitySource, + circuitActivitySource, _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 3f47d11a8f3a..af8c67d9c5da 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -24,8 +24,8 @@ internal partial class CircuitHost : IAsyncDisposable private readonly CircuitOptions _options; private readonly RemoteNavigationManager _navigationManager; private readonly ILogger _logger; - private readonly CircuitMetrics? _circuitMetrics; - private readonly ComponentsActivitySource? _componentsActivitySource; + private readonly CircuitMetrics _circuitMetrics; + private readonly CircuitActivitySource _circuitActivitySource; private Func, Task> _dispatchInboundActivity; private CircuitHandler[] _circuitHandlers; private bool _initialized; @@ -52,8 +52,8 @@ public CircuitHost( RemoteJSRuntime jsRuntime, RemoteNavigationManager navigationManager, CircuitHandler[] circuitHandlers, - CircuitMetrics? circuitMetrics, - ComponentsActivitySource? componentsActivitySource, + CircuitMetrics circuitMetrics, + CircuitActivitySource circuitActivitySource, ILogger logger) { CircuitId = circuitId; @@ -72,7 +72,7 @@ public CircuitHost( _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager)); _circuitHandlers = circuitHandlers ?? throw new ArgumentNullException(nameof(circuitHandlers)); _circuitMetrics = circuitMetrics; - _componentsActivitySource = componentsActivitySource; + _circuitActivitySource = circuitActivitySource; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); Services = scope.ServiceProvider; @@ -111,7 +111,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, ActivityContext httpContext, CancellationToken cancellationToken) + public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, ActivityContext httpActivityContext, CancellationToken cancellationToken) { Log.InitializationStarted(_logger); @@ -121,13 +121,14 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, A { throw new InvalidOperationException("The circuit host is already initialized."); } - Activity? activity = null; + + CircuitActivityHandle activityHandle = default; try { _initialized = true; // We're ready to accept incoming JSInterop calls from here on - activity = _componentsActivitySource?.StartCircuitActivity(CircuitId.Id, httpContext); + activityHandle = _circuitActivitySource.StartCircuitActivity(CircuitId.Id, httpActivityContext); _startTime = (_circuitMetrics != null && _circuitMetrics.IsDurationEnabled()) ? Stopwatch.GetTimestamp() : 0; // We only run the handlers in case we are in a Blazor Server scenario, which renders @@ -173,11 +174,11 @@ public Task InitializeAsync(ProtectedPrerenderComponentApplicationStore store, A Log.InitializationSucceeded(_logger); - activity?.Stop(); + _circuitActivitySource.StopCircuitActivity(activityHandle, null); } catch (Exception ex) { - _componentsActivitySource?.FailCircuitActivity(activity, ex); + _circuitActivitySource.StopCircuitActivity(activityHandle, ex); // Report errors asynchronously. InitializeAsync is designed not to throw. Log.InitializationFailed(_logger, ex); diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 7f2345bff74a..31b29206212b 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 internal override ResourceAssetCollection Assets => _resourceCollection ?? base.Assets; + protected override ResourceAssetCollection Assets => _resourceCollection ?? base.Assets; - protected internal override RendererInfo RendererInfo => _componentPlatform; + protected override RendererInfo RendererInfo => _componentPlatform; - protected internal override IComponentRenderMode? GetComponentRenderMode(IComponent component) => RenderMode.InteractiveServer; + protected 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 internal override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode) + protected 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 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 d91942c7711a..a12d9fdfceca 100644 --- a/src/Components/Server/src/ComponentHub.cs +++ b/src/Components/Server/src/ComponentHub.cs @@ -2,11 +2,11 @@ // 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; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; @@ -45,7 +45,6 @@ internal sealed partial class ComponentHub : Hub private readonly CircuitPersistenceManager _circuitPersistenceManager; private readonly ICircuitHandleRegistry _circuitHandleRegistry; private readonly ILogger _logger; - private readonly ActivityContext _httpContext; public ComponentHub( IServerComponentDeserializer serializer, @@ -65,7 +64,6 @@ public ComponentHub( _circuitPersistenceManager = circuitPersistenceProvider; _circuitHandleRegistry = circuitHandleRegistry; _logger = logger; - _httpContext = ComponentsActivitySource.CaptureHttpContext(); } /// @@ -143,7 +141,8 @@ 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, _httpContext, Context.ConnectionAborted); + var httpActivityContext = Context.GetHttpContext().Features.Get()?.Activity.Context ?? default; + _ = circuitHost.InitializeAsync(store, httpActivityContext, Context.ConnectionAborted); // It's safe to *publish* the circuit now because nothing will be able // to run inside it until after InitializeAsync completes. @@ -355,11 +354,13 @@ await NotifyClientError( store: null, resourceCollection); + var httpActivityContext = Context.GetHttpContext().Features.Get()?.Activity.Context ?? default; + // 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 // logic relied on receiving a subsequent message from SignalR), and it will // take care of its own errors anyway. - _ = circuitHost.InitializeAsync(store: null, _httpContext, Context.ConnectionAborted); + _ = circuitHost.InitializeAsync(store: null, httpActivityContext, Context.ConnectionAborted); circuitHost.AttachPersistedState(persistedCircuitState); diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs index a93fc5a9da95..7500a8162050 100644 --- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs @@ -95,6 +95,8 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti services.TryAddEnumerable(ServiceDescriptor.Singleton, CircuitOptionsJSInteropDetailedErrorsConfiguration>()); services.TryAddEnumerable(ServiceDescriptor.Singleton, CircuitOptionsJavaScriptInitializersConfiguration>()); + services.TryAddScoped(); + if (configure != null) { services.Configure(configure); diff --git a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj index f66329bd651a..a26f7d17f2d1 100644 --- a/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj +++ b/src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj @@ -28,6 +28,7 @@ + diff --git a/src/Components/Server/test/Circuits/CircuitActivitySourceTest.cs b/src/Components/Server/test/Circuits/CircuitActivitySourceTest.cs new file mode 100644 index 000000000000..292eab307959 --- /dev/null +++ b/src/Components/Server/test/Circuits/CircuitActivitySourceTest.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.JSInterop; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.InternalTesting; +using Moq; +using Microsoft.AspNetCore.Components.Infrastructure.Server; + +namespace Microsoft.AspNetCore.Components.Server.Circuits; + +public class CircuitActivitySourceTest +{ + private readonly ActivityListener _listener; + private readonly List _activities; + + public CircuitActivitySourceTest() + { + _activities = new List(); + _listener = new ActivityListener + { + ShouldListenTo = source => source.Name == CircuitActivitySource.Name, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + ActivityStarted = activity => _activities.Add(activity), + ActivityStopped = activity => { } + }; + ActivitySource.AddActivityListener(_listener); + } + + [Fact] + public void StartCircuitActivity_CreatesAndStartsActivity() + { + // Arrange + var circuitActivitySource = new CircuitActivitySource(); + var linkstore = new ComponentsActivityLinkStore(null); + circuitActivitySource.Init(linkstore); + var circuitId = "test-circuit-id"; + var httpContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded); + + // Act + var activityHandle = circuitActivitySource.StartCircuitActivity(circuitId, httpContext); + var activity = activityHandle.Activity; + + // Assert + Assert.NotNull(activity); + Assert.Equal(CircuitActivitySource.OnCircuitName, activity.OperationName); + Assert.Equal($"Circuit {circuitId}", activity.DisplayName); + Assert.Equal(ActivityKind.Internal, activity.Kind); + Assert.True(activity.IsAllDataRequested); + Assert.Equal(circuitId, activity.GetTagItem("aspnetcore.components.circuit.id")); + Assert.Empty(activity.Links); + Assert.False(activity.IsStopped); + + circuitActivitySource.StopCircuitActivity(activityHandle, null); + Assert.True(activity.IsStopped); + Assert.Contains(activity.Links, link => link.Context == httpContext); + } + + [Fact] + public void FailCircuitActivity_SetsErrorStatusAndStopsActivity() + { + // Arrange + var circuitActivitySource = new CircuitActivitySource(); + var linkstore = new ComponentsActivityLinkStore(null); + circuitActivitySource.Init(linkstore); + var circuitId = "test-circuit-id"; + var httpContext = default(ActivityContext); + var activityHandle = circuitActivitySource.StartCircuitActivity(circuitId, httpContext); + var activity = activityHandle.Activity; + var exception = new InvalidOperationException("Test exception"); + + // Act + circuitActivitySource.StopCircuitActivity(activityHandle, exception); + + // 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 circuitActivitySource = new CircuitActivitySource(); + var linkstore = new ComponentsActivityLinkStore(null); + circuitActivitySource.Init(linkstore); + + // Act + var activityHandle = circuitActivitySource.StartCircuitActivity(null, default); + var activity = activityHandle.Activity; + + // Assert + Assert.NotNull(activity); + Assert.Equal("Circuit ", activity.DisplayName); + } + +} diff --git a/src/Components/Server/test/Circuits/CircuitPersistenceManagerTest.cs b/src/Components/Server/test/Circuits/CircuitPersistenceManagerTest.cs index 51decd3cfb10..8f9d92ddb4fe 100644 --- a/src/Components/Server/test/Circuits/CircuitPersistenceManagerTest.cs +++ b/src/Components/Server/test/Circuits/CircuitPersistenceManagerTest.cs @@ -300,7 +300,7 @@ private async Task CreateCircuitHostAsync( NullLoggerFactory.Instance.CreateLogger()); var circuitHandlers = Array.Empty(); var circuitMetrics = new CircuitMetrics(new TestMeterFactory()); - var componentsActivitySource = new ComponentsActivitySource(); + var componentsActivitySource = new CircuitActivitySource(); var logger = NullLoggerFactory.Instance.CreateLogger(); var circuitHost = new CircuitHost( diff --git a/src/Components/Server/test/Circuits/TestCircuitHost.cs b/src/Components/Server/test/Circuits/TestCircuitHost.cs index 11e9f7ed4d65..c5818c55ce28 100644 --- a/src/Components/Server/test/Circuits/TestCircuitHost.cs +++ b/src/Components/Server/test/Circuits/TestCircuitHost.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.Metrics; +using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; @@ -16,8 +17,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, ComponentsActivitySource componentsActivitySource, ILogger logger) - : base(circuitId, scope, options, client, renderer, descriptors, jsRuntime, navigationManager, circuitHandlers, circuitMetrics, componentsActivitySource, logger) + private TestCircuitHost(CircuitId circuitId, AsyncServiceScope scope, CircuitOptions options, CircuitClientProxy client, RemoteRenderer renderer, IReadOnlyList descriptors, RemoteJSRuntime jsRuntime, RemoteNavigationManager navigationManager, CircuitHandler[] circuitHandlers, CircuitMetrics circuitMetrics, CircuitActivitySource circuitActivitySource, ILogger logger) + : base(circuitId, scope, options, client, renderer, descriptors, jsRuntime, navigationManager, circuitHandlers, circuitMetrics, circuitActivitySource, logger) { } @@ -33,13 +34,17 @@ public static CircuitHost Create( clientProxy = clientProxy ?? new CircuitClientProxy(Mock.Of(), Guid.NewGuid().ToString()); var jsRuntime = new RemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var navigationManager = new RemoteNavigationManager(Mock.Of>()); + var componentsActivitySource = new ComponentsActivitySource(); + var circuitActivitySource = new CircuitActivitySource(); var serviceProvider = new Mock(); serviceProvider .Setup(services => services.GetService(typeof(IJSRuntime))) .Returns(jsRuntime); + serviceProvider + .Setup(services => services.GetService(typeof(ComponentsActivitySource))) + .Returns(componentsActivitySource); var serverComponentDeserializer = Mock.Of(); var circuitMetrics = new CircuitMetrics(new TestMeterFactory()); - var componentsActivitySource = new ComponentsActivitySource(); if (remoteRenderer == null) { @@ -53,6 +58,8 @@ public static CircuitHost Create( jsRuntime, new CircuitJSComponentInterop(new CircuitOptions())); } + var linkstore = new Infrastructure.Server.ComponentsActivityLinkStore(remoteRenderer); + circuitActivitySource.Init(linkstore); handlers ??= Array.Empty(); return new TestCircuitHost( @@ -66,7 +73,7 @@ public static CircuitHost Create( navigationManager, handlers, circuitMetrics, - componentsActivitySource, + circuitActivitySource, NullLogger.Instance); } } diff --git a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj index 341fa72218a0..fb9321f6ded7 100644 --- a/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj +++ b/src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) diff --git a/src/Shared/Components/ComponentsActivityLinkStore.cs b/src/Shared/Components/ComponentsActivityLinkStore.cs new file mode 100644 index 000000000000..7ada7041a949 --- /dev/null +++ b/src/Shared/Components/ComponentsActivityLinkStore.cs @@ -0,0 +1,66 @@ +// 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.Runtime.CompilerServices; +using Microsoft.AspNetCore.Components.RenderTree; + +// this internal helper class is used in both Components and Components.Server projects as a different type +// the namespace is different to avoid conflicts with internalVisibleTo in unit tests +#if COMPONENTS +namespace Microsoft.AspNetCore.Components.Infrastructure; +#else +namespace Microsoft.AspNetCore.Components.Infrastructure.Server; +#endif + +using CategoryLink = Tuple?>; + +/// +/// Helper for storing links between diagnostic activities in Blazor components. +/// +internal class ComponentsActivityLinkStore +{ + public const string Http = "Http"; + public const string SignalR = "SignalR"; + public const string Route = "Route"; + public const string Circuit = "Circuit"; + public const string Event = "Event"; + + private readonly Dictionary _store; + + public ComponentsActivityLinkStore(Renderer? instance) + { + _store = instance == null + ? new Dictionary(StringComparer.OrdinalIgnoreCase) + : (Dictionary)GetActivityLinksStore(instance); + Debug.Assert(_store != null, "Activity links store should not be null."); + } + + public void SetActivityContext(string category, ActivityContext activityLink, KeyValuePair? tag) + { + _store[category] = new CategoryLink(activityLink, tag); + } + + public void AddActivityContexts(string exceptCategory, Activity targetActivity) + { + foreach (var kvp in _store) + { + if (kvp.Key != exceptCategory) + { + var link = kvp.Value.Item1; + var tag = kvp.Value.Item2; + if (link != default) + { + targetActivity.AddLink(new ActivityLink(link)); + } + if (tag != null) + { + targetActivity.SetTag(tag.Value.Key, tag.Value.Value); + } + } + } + } + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_ActivityLinksStore")] + static extern object GetActivityLinksStore(Renderer instance); +}