Skip to content

Commit 9fe8413

Browse files
JamesNKadamint
andauthored
Add logs to trace details (#10281)
Co-authored-by: Adam Ratzman <[email protected]>
1 parent b4063ed commit 9fe8413

File tree

9 files changed

+272
-32
lines changed

9 files changed

+272
-32
lines changed

src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,7 @@
104104
OnResize="@(r => _manager.SetWidthFraction(r.Orientation == Orientation.Horizontal ? r.Panel1Fraction : 1))">
105105
<DetailsTitleTemplate>
106106
@{
107-
var eventName = OtlpHelpers.GetValue(context!.LogEntry.Attributes, "event.name")
108-
?? OtlpHelpers.GetValue(context!.LogEntry.Attributes, "logrecord.event.name")
109-
?? Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsEntryDetails)];
107+
var eventName = StructureLogsDetailsViewModel.GetEventName(context!.LogEntry, Loc);
110108
}
111109

112110
<div class="pane-details-title" title="@($"{eventName} ({context!.LogEntry.Scope.Name})")">

src/Aspire.Dashboard/Components/Pages/TraceDetail.razor

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
@using Aspire.Dashboard.Components.Controls.Grid
77
@using Aspire.Dashboard.Resources
88
@using Aspire.Dashboard.Utils
9-
@inject IStringLocalizer<Dashboard.Resources.TraceDetail> Loc
10-
@inject IStringLocalizer<ControlsStrings> ControlStringsLoc
119

1210
<PageTitle>
1311
<ApplicationName
@@ -22,7 +20,7 @@
2220
<AspirePageContentLayout
2321
AddNewlineOnToolbar="true"
2422
MobileToolbarButtonText="@Loc[nameof(Dashboard.Resources.TraceDetail.TraceDetailMobileToolbarButtonText)]"
25-
IsSummaryDetailsViewOpen="@(SelectedSpan is not null)">
23+
IsSummaryDetailsViewOpen="@(SelectedData is not null)">
2624
<PageTitleSection>
2725
<div class="page-header">
2826
<h1>
@@ -73,16 +71,31 @@
7371
</ToolbarSection>
7472
<MainSection>
7573
<SummaryDetailsView
76-
ShowDetails="SelectedSpan is not null"
74+
ShowDetails="SelectedData is not null"
7775
OnDismiss="@(() => ClearSelectedSpanAsync(causedByUserAction: true))"
7876
ViewKey="TraceDetail"
79-
SelectedValue="@SelectedSpan"
77+
SelectedValue="@SelectedData"
8078
OnResize="@(r => _manager.SetWidthFraction(r.Orientation == Orientation.Horizontal ? r.Panel1Fraction : 1))">
8179
<DetailsTitleTemplate>
82-
@{ var shortedSpanId = OtlpHelpers.ToShortenedId(context!.Span.SpanId); }
83-
<div class="pane-details-title" title="@($"{context!.Title} ({shortedSpanId})")">
84-
@context!.Title
85-
<span class="pane-details-subtext">@shortedSpanId</span>
80+
@{
81+
string? title = null;
82+
string? subtitle = null;
83+
84+
if (context?.SpanViewModel is { } spanVm)
85+
{
86+
title = spanVm.Title;
87+
subtitle = OtlpHelpers.ToShortenedId(spanVm.Span.SpanId);
88+
}
89+
else if (context?.LogEntryViewModel is { } logEntryVm)
90+
{
91+
title = StructureLogsDetailsViewModel.GetEventName(logEntryVm.LogEntry, StructuredLogsLoc);
92+
subtitle = logEntryVm.LogEntry.Scope.Name;
93+
}
94+
}
95+
96+
<div class="pane-details-title" title="@($"{title} ({subtitle})")">
97+
@title
98+
<span class="pane-details-subtext">@subtitle</span>
8699
</div>
87100
</DetailsTitleTemplate>
88101
<Summary>
@@ -203,9 +216,58 @@
203216
</div>
204217
</HeaderCellItemTemplate>
205218
<ChildContent>
219+
@{
220+
var spanColor = @ColorGenerator.Instance.GetColorHexByKey(GetResourceName(context.Span.Source));
221+
}
206222
<div class="ticks">
207-
<div class="span-container" style="grid-template-columns: @context.LeftOffset.ToString("F2", CultureInfo.InvariantCulture)% @context.Width.ToString("F2", CultureInfo.InvariantCulture)% min-content;">
208-
<div class="span-bar" style="grid-column: 2; background: @ColorGenerator.Instance.GetColorHexByKey(GetResourceName(context.Span.Source));"></div>
223+
<div class="span-container" style="position: relative; grid-template-columns: @context.LeftOffset.ToString("F2", CultureInfo.InvariantCulture)% @context.Width.ToString("F2", CultureInfo.InvariantCulture)% min-content;">
224+
<div class="span-button-container">
225+
@foreach (var item in context.SpanLogs)
226+
{
227+
var buttonId = $"{context.Span.SpanId}-{item.LogEntry.InternalId}";
228+
var eventName = StructureLogsDetailsViewModel.GetEventName(item.LogEntry, StructuredLogsLoc);
229+
var isSelected = SelectedData?.LogEntryViewModel?.LogEntry.InternalId == item.LogEntry.InternalId;
230+
var htmlTooltip = item.Index < 500;
231+
232+
<button id="@buttonId"
233+
title="@(!htmlTooltip ? $"{eventName} - {item.LogEntry.Scope.Name}" : string.Empty)"
234+
aria-label="@StructuredLogsLoc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsEntryDetails)]"
235+
class="@($"span-log-entry-button {(isSelected ? "span-log-entry-selected" : null)}")"
236+
data-span-color="@spanColor"
237+
style="@($"left: {@item.LeftOffset.ToString("F2", CultureInfo.InvariantCulture)}%; --span-color: {spanColor}")"
238+
@onclick="@(() => ToggleSpanLogsAsync(item.LogEntry))"
239+
@onclick:stopPropagation="true">
240+
</button>
241+
@*
242+
There is a performance impact to having many tooltips on the page. Limit to 500 tooltips.
243+
The button continues to be displayed and clicking on it opens the details view.
244+
A standard browser tooltip is displayed instead of the FluentTooltip.
245+
*@
246+
@if (htmlTooltip)
247+
{
248+
<FluentTooltip Anchor="@buttonId" MaxWidth="400px">
249+
<div class="log-tooltip-title-container">
250+
<span class="log-tooltip-title">@eventName</span> <span class="log-tooltip-subtitle">@item.LogEntry.Scope.Name</span><br />
251+
</div>
252+
<table class="log-tooltip-table">
253+
<tr>
254+
<td>@StructuredLogsLoc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsLevelColumnHeader)]</td>
255+
<td>@item.LogEntry.Severity</td>
256+
</tr>
257+
<tr>
258+
<td>@StructuredLogsLoc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsTimestampColumnHeader)]</td>
259+
<td>@FormatHelpers.FormatTimeWithOptionalDate(TimeProvider, item.LogEntry.TimeStamp, MillisecondsDisplay.Truncated)</td>
260+
</tr>
261+
<tr>
262+
<td>@StructuredLogsLoc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsMessageColumnHeader)]</td>
263+
<td>@OtlpHelpers.TruncateString(item.LogEntry.Message, maxLength: 200)</td>
264+
</tr>
265+
</table>
266+
</FluentTooltip>
267+
}
268+
}
269+
</div>
270+
<div class="span-bar" style="grid-column: 2; background: @spanColor;"></div>
209271
<div class="span-bar-label @(context.LabelIsRight ? "span-bar-label-right" : "span-bar-label-left")">
210272
<span class="span-bar-label-detail">@SpanWaterfallViewModel.GetTitle(context.Span, _applications)</span>
211273
<span>@DurationFormatter.FormatDuration(context.Span.Duration)</span>
@@ -233,7 +295,14 @@
233295
</GridColumnManager>
234296
</Summary>
235297
<Details>
236-
<SpanDetails ViewModel="context" />
298+
@if (context?.SpanViewModel is { } spanVm)
299+
{
300+
<SpanDetails ViewModel="spanVm" />
301+
}
302+
else if (context?.LogEntryViewModel is { } logEntryVm)
303+
{
304+
<StructuredLogDetails ViewModel="logEntryVm" />
305+
}
237306
</Details>
238307
</SummaryDetailsView>
239308
</MainSection>

src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
using Aspire.Dashboard.Model.Otlp;
99
using Aspire.Dashboard.Otlp.Model;
1010
using Aspire.Dashboard.Otlp.Storage;
11+
using Aspire.Dashboard.Resources;
12+
using Aspire.Dashboard.Telemetry;
1113
using Aspire.Dashboard.Utils;
1214
using Microsoft.AspNetCore.Components;
15+
using Microsoft.Extensions.Localization;
1316
using Microsoft.FluentUI.AspNetCore.Components;
14-
using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons;
1517
using Microsoft.JSInterop;
16-
using Aspire.Dashboard.Telemetry;
18+
using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons;
1719

1820
namespace Aspire.Dashboard.Components.Pages;
1921

@@ -65,6 +67,15 @@ public partial class TraceDetail : ComponentBase, IComponentWithTelemetry, IDisp
6567
[Inject]
6668
public required ComponentTelemetryContextProvider TelemetryContextProvider { get; init; }
6769

70+
[Inject]
71+
public required IStringLocalizer<Dashboard.Resources.TraceDetail> Loc { get; init; }
72+
73+
[Inject]
74+
public required IStringLocalizer<Dashboard.Resources.StructuredLogs> StructuredLogsLoc { get; init; }
75+
76+
[Inject]
77+
public required IStringLocalizer<ControlsStrings> ControlStringsLoc { get; init; }
78+
6879
protected override void OnInitialized()
6980
{
7081
TelemetryContextProvider.Initialize(TelemetryContext);
@@ -196,8 +207,25 @@ private void UpdateDetailViewData()
196207
return;
197208
}
198209

210+
// Get logs for the trace. Note that there isn't a limit on this query so all logs are returned.
211+
// There is a limit on the number of logs stored by the dashboard so this is implicitly limited.
212+
// If there are performance issues with displaying all logs then consider adding a limit to this query.
213+
var logsContext = new GetLogsContext
214+
{
215+
ApplicationKey = null,
216+
Count = int.MaxValue,
217+
StartIndex = 0,
218+
Filters = [new TelemetryFilter
219+
{
220+
Field = KnownStructuredLogFields.TraceIdField,
221+
Condition = FilterCondition.Equals,
222+
Value = _trace.TraceId
223+
}]
224+
};
225+
var result = TelemetryRepository.GetLogs(logsContext);
226+
199227
Logger.LogInformation("Trace '{TraceId}' has {SpanCount} spans.", _trace.TraceId, _trace.Spans.Count);
200-
_spanWaterfallViewModels = SpanWaterfallViewModel.Create(_trace, new SpanWaterfallViewModel.TraceDetailState(OutgoingPeerResolvers.ToArray(), _collapsedSpanIds));
228+
_spanWaterfallViewModels = SpanWaterfallViewModel.Create(_trace, result.Items, new SpanWaterfallViewModel.TraceDetailState(OutgoingPeerResolvers.ToArray(), _collapsedSpanIds));
201229
_maxDepth = _spanWaterfallViewModels.Max(s => s.Depth);
202230

203231
var apps = new HashSet<OtlpApplication>();
@@ -214,7 +242,7 @@ private void UpdateDetailViewData()
214242

215243
private async Task HandleAfterFilterBindAsync()
216244
{
217-
SelectedSpan = null;
245+
SelectedData = null;
218246
await InvokeAsync(StateHasChanged);
219247

220248
await InvokeAsync(_dataGrid.SafeRefreshDataAsync);
@@ -243,15 +271,15 @@ private void UpdateSubscription()
243271
private string GetRowClass(SpanWaterfallViewModel viewModel)
244272
{
245273
// Test with id rather than the object reference because the data and view model objects are recreated on trace updates.
246-
if (viewModel.Span.SpanId == SelectedSpan?.Span.SpanId)
274+
if (viewModel.Span.SpanId == SelectedData?.SpanViewModel?.Span.SpanId)
247275
{
248276
return "selected-row";
249277
}
250278

251279
return string.Empty;
252280
}
253281

254-
public SpanDetailsViewModel? SelectedSpan { get; set; }
282+
public TraceDetailSelectedDataViewModel? SelectedData { get; set; }
255283

256284
private async Task OnToggleCollapse(SpanWaterfallViewModel viewModel)
257285
{
@@ -276,7 +304,7 @@ private async Task OnShowPropertiesAsync(SpanWaterfallViewModel viewModel, strin
276304
{
277305
_elementIdBeforeDetailsViewOpened = buttonId;
278306

279-
if (SelectedSpan?.Span.SpanId == viewModel.Span.SpanId)
307+
if (SelectedData?.SpanViewModel?.Span.SpanId == viewModel.Span.SpanId)
280308
{
281309
await ClearSelectedSpanAsync();
282310
}
@@ -301,7 +329,10 @@ private async Task OnShowPropertiesAsync(SpanWaterfallViewModel viewModel, strin
301329
Backlinks = backlinks,
302330
};
303331

304-
SelectedSpan = spanDetailsViewModel;
332+
SelectedData = new TraceDetailSelectedDataViewModel
333+
{
334+
SpanViewModel = spanDetailsViewModel
335+
};
305336
}
306337
}
307338

@@ -324,7 +355,7 @@ private SpanLinkViewModel CreateLinkViewModel(string traceId, string spanId, Key
324355

325356
private async Task ClearSelectedSpanAsync(bool causedByUserAction = false)
326357
{
327-
SelectedSpan = null;
358+
SelectedData = null;
328359

329360
if (_elementIdBeforeDetailsViewOpened is not null && causedByUserAction)
330361
{
@@ -336,6 +367,21 @@ private async Task ClearSelectedSpanAsync(bool causedByUserAction = false)
336367

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

370+
private async Task ToggleSpanLogsAsync(OtlpLogEntry logEntry)
371+
{
372+
if (SelectedData?.LogEntryViewModel?.LogEntry.InternalId == logEntry.InternalId)
373+
{
374+
await ClearSelectedSpanAsync();
375+
}
376+
else
377+
{
378+
SelectedData = new TraceDetailSelectedDataViewModel
379+
{
380+
LogEntryViewModel = new StructureLogsDetailsViewModel { LogEntry = logEntry }
381+
};
382+
}
383+
}
384+
339385
public void Dispose()
340386
{
341387
foreach (var subscription in _peerChangesSubscriptions)

src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.css

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,75 @@
193193
display: flex;
194194
column-gap: 8px;
195195
}
196+
197+
::deep .span-button-container {
198+
position: absolute;
199+
left: 0;
200+
right: 0;
201+
top: 0;
202+
bottom: 0;
203+
display: flex;
204+
}
205+
206+
::deep .span-log-entry-button {
207+
position: absolute;
208+
width: 15px;
209+
height: 15px;
210+
overflow: hidden;
211+
border-radius: 50%;
212+
align-self: center;
213+
opacity: 0.8;
214+
transform: translateX(-50%);
215+
background: color-mix(in srgb, var(--span-color), white 30%);
216+
border-color: color-mix(in srgb, var(--span-color), black 50%);
217+
}
218+
219+
::deep .span-log-entry-button:hover {
220+
opacity: 1;
221+
background: color-mix(in srgb, var(--span-color), white 50%);
222+
}
223+
224+
::deep .span-log-entry-selected {
225+
opacity: 1;
226+
background: color-mix(in srgb, var(--span-color), white 50%);
227+
width: 20px;
228+
height: 20px;
229+
}
230+
231+
::deep .log-tooltip-title {
232+
font-weight: bold;
233+
font-size: 16px;
234+
}
235+
236+
::deep .log-tooltip-subtitle {
237+
color: var(--foreground-subtext-rest);
238+
font-size: 12px;
239+
padding-left: 0.5rem;
240+
}
241+
242+
::deep.log-tooltip-title-container {
243+
text-overflow: ellipsis;
244+
white-space: nowrap;
245+
overflow: hidden;
246+
margin-bottom: 8px;
247+
margin-top: 4px;
248+
}
249+
250+
::deep.log-tooltip-table {
251+
margin-bottom: 0;
252+
width: 100%;
253+
table-layout: fixed;
254+
}
255+
256+
::deep.log-tooltip-table td:nth-child(1) {
257+
width: 25%;
258+
}
259+
260+
::deep.log-tooltip-table td {
261+
padding: calc((var(--design-unit) + var(--focus-stroke-width) - var(--stroke-width))* 1px) 0;
262+
overflow: hidden;
263+
text-overflow: ellipsis;
264+
white-space: nowrap;
265+
align-content: center;
266+
border-bottom: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-divider-rest);
267+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Dashboard.Otlp.Model;
5+
6+
namespace Aspire.Dashboard.Model.Otlp;
7+
8+
public sealed class SpanLogEntryViewModel
9+
{
10+
public required int Index { get; init; }
11+
public required OtlpLogEntry LogEntry { get; init; }
12+
public required double LeftOffset { get; init; }
13+
}

0 commit comments

Comments
 (0)