Skip to content

Commit 63233f0

Browse files
authored
Log entries for a GenAI span now link to GenAI visualizer (#13411)
1 parent 07793e3 commit 63233f0

File tree

10 files changed

+91
-18
lines changed

10 files changed

+91
-18
lines changed

src/Aspire.Dashboard/Components/Controls/Chart/ChartBase.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ public abstract class ChartBase : ComponentBase, IAsyncDisposable
6363
private Dictionary<SpanKey, OtlpSpan> _currentCache = new Dictionary<SpanKey, OtlpSpan>();
6464
private Dictionary<SpanKey, OtlpSpan> _newCache = new Dictionary<SpanKey, OtlpSpan>();
6565

66-
private readonly record struct SpanKey(string TraceId, string SpanId);
67-
6866
protected override void OnInitialized()
6967
{
7068
// Copy the token so there is no chance it is accessed on CTS after it is disposed.

src/Aspire.Dashboard/Components/Controls/SpanDetails.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ private void UpdateSpanActionsMenu()
116116
}
117117
});
118118

119-
if (GenAIHelpers.IsGenAISpan(ViewModel.Span.Attributes))
119+
if (GenAIHelpers.HasGenAIAttribute(ViewModel.Span.Attributes))
120120
{
121121
_spanActionsMenuItems.Add(new MenuButtonItem
122122
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
</AspireTemplateColumn>
145145
<AspireTemplateColumn ColumnId="@MessageColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsMessageColumnHeader)]">
146146
@* Tooltip is displayed by the message GridValue instance *@
147-
<LogMessageColumnDisplay FilterText="@(ViewModel.FilterText)" LogEntry="@context" LaunchGenAIVisualizerCallback="@LaunchGenAIVisualizerAsync" />
147+
<LogMessageColumnDisplay FilterText="@(ViewModel.FilterText)" LogEntry="@context" IsGenAILogCallback="@IsGenAILogEntry" LaunchGenAIVisualizerCallback="@LaunchGenAIVisualizerAsync" />
148148
</AspireTemplateColumn>
149149
<AspireTemplateColumn ColumnId="@TraceColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsTraceColumnHeader)]">
150150
@if (!string.IsNullOrEmpty(context.TraceId))

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

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,22 @@ public async Task UpdateViewModelFromQueryAsync(StructuredLogsPageViewModel view
541541
await InvokeAsync(_dataGrid.SafeRefreshDataAsync);
542542
}
543543

544+
private bool IsGenAILogEntry(OtlpLogEntry logEntry)
545+
{
546+
if (string.IsNullOrEmpty(logEntry.SpanId) || string.IsNullOrEmpty(logEntry.TraceId))
547+
{
548+
return false;
549+
}
550+
551+
if (GenAIHelpers.HasGenAIAttribute(logEntry.Attributes))
552+
{
553+
// GenAI telemetry is on the log entry.
554+
return true;
555+
}
556+
557+
return ViewModel.HasGenAISpan(logEntry.TraceId, logEntry.SpanId);
558+
}
559+
544560
private async Task LaunchGenAIVisualizerAsync(OtlpLogEntry logEntry)
545561
{
546562
var available = await TraceLinkHelpers.WaitForSpanToBeAvailableAsync(
@@ -571,7 +587,13 @@ await GenAIVisualizerDialog.OpenDialogAsync(
571587
var filters = ViewModel.GetFilters();
572588
filters.Add(new FieldTelemetryFilter
573589
{
574-
Field = GenAIHelpers.GenAISystem,
590+
Field = KnownStructuredLogFields.SpanIdField,
591+
Condition = FilterCondition.NotEqual,
592+
Value = string.Empty
593+
});
594+
filters.Add(new FieldTelemetryFilter
595+
{
596+
Field = KnownStructuredLogFields.TraceIdField,
575597
Condition = FilterCondition.NotEqual,
576598
Value = string.Empty
577599
});
@@ -584,10 +606,22 @@ await GenAIVisualizerDialog.OpenDialogAsync(
584606
Filters = filters
585607
});
586608

587-
return logs.Items
588-
.DistinctBy(l => (l.SpanId, l.TraceId))
589-
.Select(l => TelemetryRepository.GetSpan(l.TraceId, l.SpanId)!)
590-
.ToList();
609+
var genAISpans = new List<OtlpSpan>();
610+
foreach (var l in logs.Items.DistinctBy(l => (l.SpanId, l.TraceId)))
611+
{
612+
var span = TelemetryRepository.GetSpan(l.TraceId, l.SpanId);
613+
if (span == null)
614+
{
615+
continue;
616+
}
617+
618+
if (GenAIHelpers.HasGenAIAttribute(l.Attributes) || GenAIHelpers.HasGenAIAttribute(span.Attributes))
619+
{
620+
genAISpans.Add(span);
621+
}
622+
}
623+
624+
return genAISpans;
591625
});
592626
}
593627
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ private async Task ToggleSpanLogsAsync(OtlpLogEntry logEntry)
544544

545545
private static bool IsGenAISpan(SpanWaterfallViewModel spanViewModel)
546546
{
547-
return GenAIHelpers.IsGenAISpan(spanViewModel.Span.Attributes);
547+
return GenAIHelpers.HasGenAIAttribute(spanViewModel.Span.Attributes);
548548
}
549549

550550
private async Task OnGenAIClickedAsync(OtlpSpan span)

src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@
1414
HighlightText="@FilterText"
1515
StopClickPropagation="true">
1616
<ContentInButtonArea>
17-
@if (!string.IsNullOrEmpty(LogEntry.TraceId) &&
18-
!string.IsNullOrEmpty(LogEntry.SpanId) &&
19-
GenAIHelpers.IsGenAISpan(LogEntry.Attributes))
17+
@if (IsGenAILogCallback(LogEntry))
2018
{
2119
<FluentButton Appearance="Appearance.Lightweight"
2220
Title="@ControlStringsLoc[nameof(ControlsStrings.GenAIDetailsTitle)]"

src/Aspire.Dashboard/Components/ResourcesGridColumns/LogMessageColumnDisplay.razor.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ public partial class LogMessageColumnDisplay
1717
[Parameter, EditorRequired]
1818
public required EventCallback<OtlpLogEntry> LaunchGenAIVisualizerCallback { get; set; }
1919

20+
[Parameter, EditorRequired]
21+
public required Func<OtlpLogEntry, bool> IsGenAILogCallback { get; set; }
22+
2023
private string? _exceptionText;
2124

2225
protected override void OnInitialized()

src/Aspire.Dashboard/Model/GenAI/GenAIHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static class GenAIHelpers
3434

3535
public const string ErrorType = "error.type";
3636

37-
public static bool IsGenAISpan(KeyValuePair<string, string>[] attributes)
37+
public static bool HasGenAIAttribute(KeyValuePair<string, string>[] attributes)
3838
{
3939
return attributes.GetValueWithFallback(GenAISystem, GenAIProviderName) is { Length: > 0 };
4040
}

src/Aspire.Dashboard/Model/StructuredLogsViewModel.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Concurrent;
5+
using Aspire.Dashboard.Model.GenAI;
46
using Aspire.Dashboard.Model.Otlp;
57
using Aspire.Dashboard.Otlp.Model;
68
using Aspire.Dashboard.Otlp.Storage;
@@ -11,6 +13,8 @@ public class StructuredLogsViewModel
1113
{
1214
private readonly TelemetryRepository _telemetryRepository;
1315
private readonly List<FieldTelemetryFilter> _filters = new();
16+
// Cache span lookups for GenAI attributes to avoid repeated lookups.
17+
private readonly ConcurrentDictionary<SpanKey, bool> _spanGenAICache = new();
1418

1519
private PagedResult<OtlpLogEntry>? _logs;
1620
private ResourceKey? _resourceKey;
@@ -29,10 +33,37 @@ public StructuredLogsViewModel(TelemetryRepository telemetryRepository)
2933
public string FilterText { get => _filterText; set => SetValue(ref _filterText, value); }
3034
public IReadOnlyList<FieldTelemetryFilter> Filters => _filters;
3135

36+
public bool HasGenAISpan(string traceId, string spanId)
37+
{
38+
// Get a flag indicating whether the span has GenAI telemetry on it.
39+
// This is cached to avoid repeated lookups. The cache is cleared when logs change.
40+
// It's ok that this isn't completely thread safe, i.e. get and a clear happen at the same time.
41+
42+
var spanKey = new SpanKey(traceId, spanId);
43+
44+
if (_spanGenAICache.TryGetValue(spanKey, out var value))
45+
{
46+
return value;
47+
}
48+
49+
var span = _telemetryRepository.GetSpan(spanKey.TraceId, spanKey.SpanId);
50+
var hasGenAISpan = false;
51+
52+
if (span != null)
53+
{
54+
// Only cache a value if a span is present.
55+
// We don't want to cache false if there is no span because the span may be added later.
56+
hasGenAISpan = GenAIHelpers.HasGenAIAttribute(span.Attributes);
57+
_spanGenAICache.TryAdd(spanKey, hasGenAISpan);
58+
}
59+
60+
return hasGenAISpan;
61+
}
62+
3263
public void ClearFilters()
3364
{
3465
_filters.Clear();
35-
_logs = null;
66+
ClearData();
3667
}
3768

3869
public void AddFilter(FieldTelemetryFilter filter)
@@ -47,14 +78,14 @@ public void AddFilter(FieldTelemetryFilter filter)
4778
}
4879

4980
_filters.Add(filter);
50-
_logs = null;
81+
ClearData();
5182
}
5283

5384
public bool RemoveFilter(FieldTelemetryFilter filter)
5485
{
5586
if (_filters.Remove(filter))
5687
{
57-
_logs = null;
88+
ClearData();
5889
return true;
5990
}
6091
return false;
@@ -72,7 +103,7 @@ private void SetValue<T>(ref T field, T value)
72103
}
73104

74105
field = value;
75-
_logs = null;
106+
ClearData();
76107
}
77108

78109
public PagedResult<OtlpLogEntry> GetLogs()
@@ -135,5 +166,8 @@ public PagedResult<OtlpLogEntry> GetErrorLogs(int count)
135166
public void ClearData()
136167
{
137168
_logs = null;
169+
170+
// Clear cache whenever log data changes to prevent it growing forever.
171+
_spanGenAICache.Clear();
138172
}
139173
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
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+
namespace Aspire.Dashboard.Otlp.Model;
5+
6+
public readonly record struct SpanKey(string TraceId, string SpanId);

0 commit comments

Comments
 (0)