Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,7 @@
OnResize="@(r => _manager.SetWidthFraction(r.Orientation == Orientation.Horizontal ? r.Panel1Fraction : 1))">
<DetailsTitleTemplate>
@{
var eventName = OtlpHelpers.GetValue(context!.LogEntry.Attributes, "event.name")
?? OtlpHelpers.GetValue(context!.LogEntry.Attributes, "logrecord.event.name")
?? Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsEntryDetails)];
var eventName = StructureLogsDetailsViewModel.GetEventName(context!.LogEntry, Loc);
}

<div class="pane-details-title" title="@($"{eventName} ({context!.LogEntry.Scope.Name})")">
Expand Down
82 changes: 70 additions & 12 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
@using Aspire.Dashboard.Components.Controls.Grid
@using Aspire.Dashboard.Resources
@using Aspire.Dashboard.Utils
@inject IStringLocalizer<Dashboard.Resources.TraceDetail> Loc
@inject IStringLocalizer<ControlsStrings> ControlStringsLoc

<PageTitle>
<ApplicationName
Expand All @@ -22,7 +20,7 @@
<AspirePageContentLayout
AddNewlineOnToolbar="true"
MobileToolbarButtonText="@Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailMobileToolbarButtonText)]"
IsSummaryDetailsViewOpen="@(SelectedSpan is not null)">
IsSummaryDetailsViewOpen="@(SelectedData is not null)">
<PageTitleSection>
<div class="page-header">
<h1>
Expand Down Expand Up @@ -73,16 +71,31 @@
</ToolbarSection>
<MainSection>
<SummaryDetailsView
ShowDetails="SelectedSpan is not null"
ShowDetails="SelectedData is not null"
OnDismiss="@(() => ClearSelectedSpanAsync(causedByUserAction: true))"
ViewKey="TraceDetail"
SelectedValue="@SelectedSpan"
SelectedValue="@SelectedData"
OnResize="@(r => _manager.SetWidthFraction(r.Orientation == Orientation.Horizontal ? r.Panel1Fraction : 1))">
<DetailsTitleTemplate>
@{ var shortedSpanId = OtlpHelpers.ToShortenedId(context!.Span.SpanId); }
<div class="pane-details-title" title="@($"{context!.Title} ({shortedSpanId})")">
@context!.Title
<span class="pane-details-subtext">@shortedSpanId</span>
@{
string? title = null;
string? subtitle = null;

if (context?.SpanViewModel is { } spanVm)
{
title = spanVm.Title;
subtitle = OtlpHelpers.ToShortenedId(spanVm.Span.SpanId);
}
else if (context?.LogEntryViewModel is { } logEntryVm)
{
title = StructureLogsDetailsViewModel.GetEventName(logEntryVm.LogEntry, StructuredLogsLoc);
subtitle = logEntryVm.LogEntry.Scope.Name;
}
}

<div class="pane-details-title" title="@($"{title} ({subtitle})")">
@title
<span class="pane-details-subtext">@subtitle</span>
</div>
</DetailsTitleTemplate>
<Summary>
Expand Down Expand Up @@ -203,9 +216,47 @@
</div>
</HeaderCellItemTemplate>
<ChildContent>
@{
var spanColor = @ColorGenerator.Instance.GetColorHexByKey(GetResourceName(context.Span.Source));
}
<div class="ticks">
<div class="span-container" style="grid-template-columns: @context.LeftOffset.ToString("F2", CultureInfo.InvariantCulture)% @context.Width.ToString("F2", CultureInfo.InvariantCulture)% min-content;">
<div class="span-bar" style="grid-column: 2; background: @ColorGenerator.Instance.GetColorHexByKey(GetResourceName(context.Span.Source));"></div>
<div class="span-container" style="position: relative; grid-template-columns: @context.LeftOffset.ToString("F2", CultureInfo.InvariantCulture)% @context.Width.ToString("F2", CultureInfo.InvariantCulture)% min-content;">
<div class="span-button-container">
@foreach (var item in context.SpanLogs)
{
var buttonId = $"{context.Span.SpanId}-{item.LogEntry.InternalId}";
var eventName = StructureLogsDetailsViewModel.GetEventName(item.LogEntry, StructuredLogsLoc);
var isSelected = SelectedData?.LogEntryViewModel?.LogEntry.InternalId == item.LogEntry.InternalId;

<button id="@buttonId"

Copilot AI Jul 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log entry buttons lack a visible label or aria-label, which makes them inaccessible to screen readers. Add an aria-label attribute (for example, aria-label="<event name>") to each button to provide an accessible name.

Copilot uses AI. Check for mistakes.
class="@($"span-log-entry-button {(isSelected ? "span-log-entry-selected" : null)}")"
data-span-color="@spanColor"
style="@($"left: {@item.LeftOffset.ToString("F2", CultureInfo.InvariantCulture)}%; --span-color: {spanColor}")"
@onclick="@(() => ToggleSpanLogsAsync(item.LogEntry))"
@onclick:stopPropagation="true">
</button>
<FluentTooltip Anchor="@buttonId" MaxWidth="400px">
<div class="log-tooltip-title-container">
<span class="log-tooltip-title">@eventName</span> <span class="log-tooltip-subtitle">@item.LogEntry.Scope.Name</span><br />
</div>
<table class="log-tooltip-table">
<tr>
<td>Level</td>
<td>@item.LogEntry.Severity</td>
</tr>
<tr>
<td>Timestamp</td>
<td>@FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, item.LogEntry.TimeStamp, MillisecondsDisplay.Truncated)</td>
</tr>
<tr>
<td>Message</td>
<td>@OtlpHelpers.TruncateString(item.LogEntry.Message, maxLength: 200)</td>
</tr>
</table>
</FluentTooltip>
}
</div>
<div class="span-bar" style="grid-column: 2; background: @spanColor;"></div>
<div class="span-bar-label @(context.LabelIsRight ? "span-bar-label-right" : "span-bar-label-left")">
<span class="span-bar-label-detail">@SpanWaterfallViewModel.GetTitle(context.Span, _applications)</span>
<span>@DurationFormatter.FormatDuration(context.Span.Duration)</span>
Expand Down Expand Up @@ -233,7 +284,14 @@
</GridColumnManager>
</Summary>
<Details>
<SpanDetails ViewModel="context" />
@if (context?.SpanViewModel is { } spanVm)
{
<SpanDetails ViewModel="spanVm" />
}
else if (context?.LogEntryViewModel is { } logEntryVm)
{
<StructuredLogDetails ViewModel="logEntryVm" />
}
</Details>
</SummaryDetailsView>
</MainSection>
Expand Down
62 changes: 54 additions & 8 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Aspire.Dashboard.Resources;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using Microsoft.FluentUI.AspNetCore.Components;
using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons;
using Microsoft.JSInterop;
using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons;

namespace Aspire.Dashboard.Components.Pages;

Expand Down Expand Up @@ -64,6 +66,15 @@ public partial class TraceDetail : ComponentBase, IComponentWithTelemetry, IDisp
[Inject]
public required ComponentTelemetryContextProvider TelemetryContextProvider { get; init; }

[Inject]
public required IStringLocalizer<Dashboard.Resources.TraceDetail> Loc { get; init; }

[Inject]
public required IStringLocalizer<Dashboard.Resources.StructuredLogs> StructuredLogsLoc { get; init; }

[Inject]
public required IStringLocalizer<ControlsStrings> ControlStringsLoc { get; init; }

protected override void OnInitialized()
{
TelemetryContextProvider.Initialize(TelemetryContext);
Expand Down Expand Up @@ -195,8 +206,22 @@ private void UpdateDetailViewData()
return;
}

var logsContext = new GetLogsContext
{
ApplicationKey = null,
Count = int.MaxValue,
StartIndex = 0,
Filters = [new TelemetryFilter
{
Field = KnownStructuredLogFields.TraceIdField,
Condition = FilterCondition.Equals,
Value = _trace.TraceId
}]
};
var result = TelemetryRepository.GetLogs(logsContext);

Logger.LogInformation("Trace '{TraceId}' has {SpanCount} spans.", _trace.TraceId, _trace.Spans.Count);
_spanWaterfallViewModels = SpanWaterfallViewModel.Create(_trace, new SpanWaterfallViewModel.TraceDetailState(OutgoingPeerResolvers.ToArray(), _collapsedSpanIds));
_spanWaterfallViewModels = SpanWaterfallViewModel.Create(_trace, result.Items, new SpanWaterfallViewModel.TraceDetailState(OutgoingPeerResolvers.ToArray(), _collapsedSpanIds));
_maxDepth = _spanWaterfallViewModels.Max(s => s.Depth);

var apps = new HashSet<OtlpApplication>();
Expand All @@ -213,7 +238,7 @@ private void UpdateDetailViewData()

private async Task HandleAfterFilterBindAsync()
{
SelectedSpan = null;
SelectedData = null;
await InvokeAsync(StateHasChanged);

await InvokeAsync(_dataGrid.SafeRefreshDataAsync);
Expand Down Expand Up @@ -242,15 +267,21 @@ private void UpdateSubscription()
private string GetRowClass(SpanWaterfallViewModel viewModel)
{
// Test with id rather than the object reference because the data and view model objects are recreated on trace updates.
if (viewModel.Span.SpanId == SelectedSpan?.Span.SpanId)
if (viewModel.Span.SpanId == SelectedData?.SpanViewModel?.Span.SpanId)
{
return "selected-row";
}

return string.Empty;
}

public SpanDetailsViewModel? SelectedSpan { get; set; }
public SelectedDataItem? SelectedData { get; set; }

public class SelectedDataItem
{
public SpanDetailsViewModel? SpanViewModel { get; init; }
public StructureLogsDetailsViewModel? LogEntryViewModel { get; init; }
}

private async Task OnToggleCollapse(SpanWaterfallViewModel viewModel)
{
Expand All @@ -275,7 +306,7 @@ private async Task OnShowPropertiesAsync(SpanWaterfallViewModel viewModel, strin
{
_elementIdBeforeDetailsViewOpened = buttonId;

if (SelectedSpan?.Span.SpanId == viewModel.Span.SpanId)
if (SelectedData?.SpanViewModel?.Span.SpanId == viewModel.Span.SpanId)
{
await ClearSelectedSpanAsync();
}
Expand All @@ -300,7 +331,7 @@ private async Task OnShowPropertiesAsync(SpanWaterfallViewModel viewModel, strin
Backlinks = backlinks,
};

SelectedSpan = spanDetailsViewModel;
SelectedData = new SelectedDataItem { SpanViewModel = spanDetailsViewModel };
}
}

Expand All @@ -323,7 +354,7 @@ private SpanLinkViewModel CreateLinkViewModel(string traceId, string spanId, Key

private async Task ClearSelectedSpanAsync(bool causedByUserAction = false)
{
SelectedSpan = null;
SelectedData = null;

if (_elementIdBeforeDetailsViewOpened is not null && causedByUserAction)
{
Expand All @@ -335,6 +366,21 @@ private async Task ClearSelectedSpanAsync(bool causedByUserAction = false)

private string GetResourceName(OtlpApplicationView app) => OtlpApplication.GetResourceName(app, _applications);

private async Task ToggleSpanLogsAsync(OtlpLogEntry logEntry)
{
if (SelectedData?.LogEntryViewModel?.LogEntry.InternalId == logEntry.InternalId)
{
await ClearSelectedSpanAsync();
}
else
{
SelectedData = new SelectedDataItem
{
LogEntryViewModel = new StructureLogsDetailsViewModel { LogEntry = logEntry }
};
}
}

public void Dispose()
{
foreach (var subscription in _peerChangesSubscriptions)
Expand Down
72 changes: 72 additions & 0 deletions src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,75 @@
display: flex;
column-gap: 8px;
}

::deep .span-button-container {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
}

::deep .span-log-entry-button {
position: absolute;
width: 15px;
height: 15px;
overflow: hidden;
border-radius: 50%;
align-self: center;
opacity: 0.8;
transform: translateX(-50%);
background: color-mix(in srgb, var(--span-color), white 30%);
border-color: color-mix(in srgb, var(--span-color), black 50%);
}

::deep .span-log-entry-button:hover {
opacity: 1;
background: color-mix(in srgb, var(--span-color), white 50%);
}

::deep .span-log-entry-selected {
opacity: 1;
background: color-mix(in srgb, var(--span-color), white 50%);
width: 20px;
height: 20px;
}

::deep .log-tooltip-title {
font-weight: bold;
font-size: 16px;
}

::deep .log-tooltip-subtitle {
color: var(--foreground-subtext-rest);
font-size: 12px;
padding-left: 0.5rem;
}

::deep.log-tooltip-title-container {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
margin-bottom: 8px;
margin-top: 4px;
}

::deep.log-tooltip-table {
margin-bottom: 0;
width: 100%;
table-layout: fixed;
}

::deep.log-tooltip-table td:nth-child(1) {
width: 25%;
}

::deep.log-tooltip-table td {
padding: calc((var(--design-unit) + var(--focus-stroke-width) - var(--stroke-width))* 1px) 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
align-content: center;
border-bottom: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-divider-rest);
}
Loading