From baffc4a96f4693c69e0e07d468ef46bd56968416 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 7 Jul 2025 10:30:10 +0800 Subject: [PATCH 1/3] Add dashboard telemetry to interaction service --- .../Controls/ResourceDetails.razor.cs | 2 +- .../Components/Controls/SpanDetails.razor.cs | 3 +- .../Controls/StructuredLogDetails.razor.cs | 3 +- .../Interactions/InteractionsProvider.cs | 39 +++++++++++++++++-- .../Components/Pages/ConsoleLogs.razor.cs | 2 +- .../Components/Pages/Error.razor.cs | 2 +- .../Components/Pages/Login.razor.cs | 3 +- .../Components/Pages/Metrics.razor.cs | 2 +- .../Components/Pages/NotFound.razor.cs | 3 +- .../Components/Pages/Resources.razor.cs | 2 +- .../Components/Pages/StructuredLogs.razor.cs | 2 +- .../Components/Pages/TraceDetail.razor.cs | 3 +- .../Components/Pages/Traces.razor.cs | 3 +- .../Telemetry/TelemetryComponentIds.cs | 26 +++++++++++++ 14 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 src/Aspire.Dashboard/Telemetry/TelemetryComponentIds.cs diff --git a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor.cs b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor.cs index 1703884443a..4295d634c64 100644 --- a/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor.cs @@ -279,7 +279,7 @@ public Task OnViewRelationshipAsync(ResourceDetailRelationshipViewModel relation } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Control, nameof(ResourceDetails)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Control, TelemetryComponentIds.ResourceDetails); public void UpdateTelemetryProperties() { diff --git a/src/Aspire.Dashboard/Components/Controls/SpanDetails.razor.cs b/src/Aspire.Dashboard/Components/Controls/SpanDetails.razor.cs index b8afe9f0100..59d7e25471e 100644 --- a/src/Aspire.Dashboard/Components/Controls/SpanDetails.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/SpanDetails.razor.cs @@ -6,6 +6,7 @@ using Aspire.Dashboard.Model.Otlp; using Aspire.Dashboard.Otlp.Model; using Aspire.Dashboard.Otlp.Storage; +using Aspire.Dashboard.Telemetry; using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; @@ -145,7 +146,7 @@ public async Task OnViewDetailsAsync(SpanLinkViewModel linkVM) } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Control, nameof(SpanDetails)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Control, TelemetryComponentIds.SpanDetails); public void Dispose() { diff --git a/src/Aspire.Dashboard/Components/Controls/StructuredLogDetails.razor.cs b/src/Aspire.Dashboard/Components/Controls/StructuredLogDetails.razor.cs index 0907c87737f..7f574033191 100644 --- a/src/Aspire.Dashboard/Components/Controls/StructuredLogDetails.razor.cs +++ b/src/Aspire.Dashboard/Components/Controls/StructuredLogDetails.razor.cs @@ -4,6 +4,7 @@ using Aspire.Dashboard.Components.Pages; using Aspire.Dashboard.Model; using Aspire.Dashboard.Model.Otlp; +using Aspire.Dashboard.Telemetry; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; @@ -149,7 +150,7 @@ private static bool HasTelemetryBaggage(string value) } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Control, nameof(StructuredLogDetails)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Control, TelemetryComponentIds.StructuredLogDetails); public void Dispose() { diff --git a/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs b/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs index 32658f155d3..438b355dd52 100644 --- a/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs +++ b/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs @@ -5,7 +5,9 @@ using System.Diagnostics; using System.Net; using Aspire.Dashboard.Components.Dialogs; +using Aspire.Dashboard.Components.Pages; using Aspire.Dashboard.Model; +using Aspire.Dashboard.Telemetry; using Aspire.Dashboard.Utils; using Aspire.DashboardService.Proto.V1; using Markdig; @@ -22,8 +24,20 @@ public class InteractionsProvider : ComponentBase, IAsyncDisposable { private static readonly MarkdownPipeline s_markdownPipeline = MarkdownHelpers.CreateMarkdownPipelineBuilder().Build(); - internal record InteractionMessageBarReference(int InteractionId, Message Message); - internal record InteractionDialogReference(int InteractionId, IDialogReference Dialog); + internal record InteractionMessageBarReference(int InteractionId, Message Message, ComponentTelemetryContext TelemetryContext) : IDisposable + { + public void Dispose() + { + TelemetryContext?.Dispose(); + } + } + internal record InteractionDialogReference(int InteractionId, IDialogReference Dialog, ComponentTelemetryContext TelemetryContext) : IDisposable + { + public void Dispose() + { + TelemetryContext?.Dispose(); + } + } private readonly CancellationTokenSource _cts = new(); private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); @@ -53,6 +67,9 @@ internal record InteractionDialogReference(int InteractionId, IDialogReference D [Inject] public required ILogger Logger { get; init; } + [Inject] + public required ComponentTelemetryContextProvider TelemetryContextProvider { get; init; } + protected override void OnInitialized() { // Exit quickly if the dashboard client is not enabled. For example, the dashboard is running in the standalone container. @@ -119,6 +136,7 @@ private async Task InteractionsDisplayAsync() _pendingInteractions.RemoveAt(0); Func> openDialog; + string dialogComponentId; if (item.MessageBox is { } messageBox) { @@ -184,6 +202,7 @@ private async Task InteractionsDisplayAsync() break; } + dialogComponentId = TelemetryComponentIds.InteractionMessageBox; openDialog = dialogService => ShowMessageBoxAsync(dialogService, content, dialogParameters); } else if (item.InputsDialog is { } inputs) @@ -222,6 +241,7 @@ private async Task InteractionsDisplayAsync() } }); + dialogComponentId = TelemetryComponentIds.InteractionInputsDialog; openDialog = dialogService => dialogService.ShowDialogAsync(vm, dialogParameters); } else @@ -236,7 +256,8 @@ await InvokeAsync(async () => }); Debug.Assert(currentDialogReference != null, "Dialog should have been created in UI thread."); - _interactionDialogReference = new InteractionDialogReference(item.InteractionId, currentDialogReference); + + _interactionDialogReference = new InteractionDialogReference(item.InteractionId, currentDialogReference, CreateTelemetryContext(dialogComponentId)); } finally { @@ -254,6 +275,7 @@ await InvokeAsync(async () => { if (_interactionDialogReference?.Dialog == currentDialogReference) { + _interactionDialogReference.Dispose(); _interactionDialogReference = null; } } @@ -270,6 +292,13 @@ await InvokeAsync(async () => } } + private ComponentTelemetryContext CreateTelemetryContext(string componentId) + { + var telemetryContext = new ComponentTelemetryContext(ComponentType.Control, componentId); + TelemetryContextProvider.Initialize(telemetryContext); + return telemetryContext; + } + private static string GetMessageHtml(WatchInteractionsResponseUpdate item) { if (!item.EnableMessageMarkdown) @@ -401,6 +430,7 @@ await InvokeAsync(async () => } _openMessageBars.Remove(item.InteractionId); + openMessageBar.Dispose(); await DashboardClient.SendInteractionRequestAsync(request, _cts.Token).ConfigureAwait(false); } @@ -409,7 +439,7 @@ await InvokeAsync(async () => }); Debug.Assert(message != null, "Message should have been created in UI thread."); - _openMessageBars.Add(new InteractionMessageBarReference(item.InteractionId, message)); + _openMessageBars.Add(new InteractionMessageBarReference(item.InteractionId, message, CreateTelemetryContext(TelemetryComponentIds.InteractionMessageBar))); break; case WatchInteractionsResponseUpdate.KindOneofCase.Complete: // Complete interaction. @@ -428,6 +458,7 @@ await InvokeAsync(async () => } finally { + _interactionDialogReference.Dispose(); _interactionDialogReference = null; } } diff --git a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs index 6c69026fd8c..c6250596e24 100644 --- a/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/ConsoleLogs.razor.cs @@ -796,7 +796,7 @@ public ConsoleLogsPageState ConvertViewModelToSerializable() } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, nameof(ConsoleLogs)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, TelemetryComponentIds.ConsoleLogs); public void UpdateTelemetryProperties() { diff --git a/src/Aspire.Dashboard/Components/Pages/Error.razor.cs b/src/Aspire.Dashboard/Components/Pages/Error.razor.cs index f155248431b..d6f66c603c9 100644 --- a/src/Aspire.Dashboard/Components/Pages/Error.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Error.razor.cs @@ -28,7 +28,7 @@ protected override void OnInitialized() public required ComponentTelemetryContextProvider TelemetryContextProvider { get; init; } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, nameof(Error)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, TelemetryComponentIds.Error); protected override void OnParametersSet() { diff --git a/src/Aspire.Dashboard/Components/Pages/Login.razor.cs b/src/Aspire.Dashboard/Components/Pages/Login.razor.cs index 350c9dc8ba1..76fb3cc825f 100644 --- a/src/Aspire.Dashboard/Components/Pages/Login.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Login.razor.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Dashboard.Model; +using Aspire.Dashboard.Telemetry; using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; @@ -117,5 +118,5 @@ public async ValueTask DisposeAsync() } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, nameof(Login)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, TelemetryComponentIds.Login); } diff --git a/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs b/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs index 15d624a52b9..56835d15b18 100644 --- a/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Metrics.razor.cs @@ -340,7 +340,7 @@ public void Dispose() } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, nameof(Metrics)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, TelemetryComponentIds.Metrics); public void UpdateTelemetryProperties() { diff --git a/src/Aspire.Dashboard/Components/Pages/NotFound.razor.cs b/src/Aspire.Dashboard/Components/Pages/NotFound.razor.cs index 033a56403d5..30fe47ae37b 100644 --- a/src/Aspire.Dashboard/Components/Pages/NotFound.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/NotFound.razor.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 Aspire.Dashboard.Telemetry; using Microsoft.AspNetCore.Components; namespace Aspire.Dashboard.Components.Pages; @@ -11,7 +12,7 @@ public partial class NotFound : IComponentWithTelemetry, IDisposable public required ComponentTelemetryContextProvider TelemetryContextProvider { get; init; } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, nameof(NotFound)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, TelemetryComponentIds.NotFound); protected override void OnInitialized() { diff --git a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs index 5f624dc0bd8..826d9d689fc 100644 --- a/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Resources.razor.cs @@ -888,7 +888,7 @@ private async Task ContextMenuClosed(Microsoft.AspNetCore.Components.Web.MouseEv } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, nameof(Resources)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, TelemetryComponentIds.Resources); public void UpdateTelemetryProperties() { diff --git a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs index 03f6eb71d3f..970e5eda546 100644 --- a/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs @@ -489,7 +489,7 @@ public class StructuredLogsPageState } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, nameof(StructuredLogs)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, TelemetryComponentIds.StructuredLogs); public void UpdateTelemetryProperties() { diff --git a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs index ea7afc6d250..da3b6c38d0b 100644 --- a/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs @@ -13,6 +13,7 @@ using Microsoft.FluentUI.AspNetCore.Components; using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons; using Microsoft.JSInterop; +using Aspire.Dashboard.Telemetry; namespace Aspire.Dashboard.Components.Pages; @@ -346,5 +347,5 @@ public void Dispose() } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, nameof(TraceDetail)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, TelemetryComponentIds.TraceDetail); } diff --git a/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs b/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs index 6dc38bb34dc..dae6666d92f 100644 --- a/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs +++ b/src/Aspire.Dashboard/Components/Pages/Traces.razor.cs @@ -11,6 +11,7 @@ using Aspire.Dashboard.Otlp.Model; using Aspire.Dashboard.Otlp.Storage; using Aspire.Dashboard.Resources; +using Aspire.Dashboard.Telemetry; using Aspire.Dashboard.Utils; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Options; @@ -381,5 +382,5 @@ public class TracesPageState } // IComponentWithTelemetry impl - public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, nameof(Traces)); + public ComponentTelemetryContext TelemetryContext { get; } = new(ComponentType.Page, TelemetryComponentIds.Traces); } diff --git a/src/Aspire.Dashboard/Telemetry/TelemetryComponentIds.cs b/src/Aspire.Dashboard/Telemetry/TelemetryComponentIds.cs new file mode 100644 index 00000000000..c1e1651c27e --- /dev/null +++ b/src/Aspire.Dashboard/Telemetry/TelemetryComponentIds.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Dashboard.Telemetry; + +public static class TelemetryComponentIds +{ + // Pages + public const string Resources = nameof(Resources); + public const string Login = nameof(Login); + public const string Traces = nameof(Traces); + public const string StructuredLogs = nameof(StructuredLogs); + public const string NotFound = nameof(NotFound); + public const string Metrics = nameof(Metrics); + public const string Error = nameof(Error); + public const string ConsoleLogs = nameof(ConsoleLogs); + + // Controls + public const string TraceDetail = nameof(TraceDetail); + public const string StructuredLogDetails = nameof(StructuredLogDetails); + public const string SpanDetails = nameof(SpanDetails); + public const string ResourceDetails = nameof(ResourceDetails); + public const string InteractionMessageBox = nameof(InteractionMessageBox); + public const string InteractionMessageBar = nameof(InteractionMessageBar); + public const string InteractionInputsDialog = nameof(InteractionInputsDialog); +} From 5dd0c7503b3a938af74bcac65c1a2ee9277a7447 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 7 Jul 2025 10:35:17 +0800 Subject: [PATCH 2/3] Clean up --- .../Components/Interactions/InteractionsProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs b/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs index 438b355dd52..a2a24fde76d 100644 --- a/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs +++ b/src/Aspire.Dashboard/Components/Interactions/InteractionsProvider.cs @@ -256,7 +256,6 @@ await InvokeAsync(async () => }); Debug.Assert(currentDialogReference != null, "Dialog should have been created in UI thread."); - _interactionDialogReference = new InteractionDialogReference(item.InteractionId, currentDialogReference, CreateTelemetryContext(dialogComponentId)); } finally From dd9cb2525a90a5d88ebd2292aff3d3a98a61dc35 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Mon, 7 Jul 2025 10:48:31 +0800 Subject: [PATCH 3/3] Fix tests --- .../Interactions/InteractionsProviderTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Aspire.Dashboard.Components.Tests/Interactions/InteractionsProviderTests.cs b/tests/Aspire.Dashboard.Components.Tests/Interactions/InteractionsProviderTests.cs index 9fd7cc1b205..88c62484006 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Interactions/InteractionsProviderTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Interactions/InteractionsProviderTests.cs @@ -2,8 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Channels; +using Aspire.Dashboard.Components.Pages; using Aspire.Dashboard.Components.Tests.Shared; using Aspire.Dashboard.Model; +using Aspire.Dashboard.Telemetry; +using Aspire.Dashboard.Tests; using Aspire.DashboardService.Proto.V1; using Bunit; using Microsoft.AspNetCore.InternalTesting; @@ -346,5 +349,8 @@ private void SetupInteractionProviderServices(TestDashboardClient? dashboardClie Services.AddSingleton(dialogService ?? new TestDialogService()); Services.AddSingleton(); Services.AddSingleton(dashboardClient ?? new TestDashboardClient()); + Services.AddSingleton(); + Services.AddSingleton(); + Services.AddSingleton(); } }