Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 0 additions & 2 deletions src/Aspire.Dashboard/Components/Controls/Chart/ChartBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@ public abstract class ChartBase : ComponentBase, IAsyncDisposable
private Dictionary<SpanKey, OtlpSpan> _currentCache = new Dictionary<SpanKey, OtlpSpan>();
private Dictionary<SpanKey, OtlpSpan> _newCache = new Dictionary<SpanKey, OtlpSpan>();

private readonly record struct SpanKey(string TraceId, string SpanId);

protected override void OnInitialized()
{
// Copy the token so there is no chance it is accessed on CTS after it is disposed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ private void UpdateSpanActionsMenu()
}
});

if (GenAIHelpers.IsGenAISpan(ViewModel.Span.Attributes))
if (GenAIHelpers.HasGenAIAttribute(ViewModel.Span.Attributes))
{
_spanActionsMenuItems.Add(new MenuButtonItem
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
</AspireTemplateColumn>
<AspireTemplateColumn ColumnId="@MessageColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsMessageColumnHeader)]">
@* Tooltip is displayed by the message GridValue instance *@
<LogMessageColumnDisplay FilterText="@(ViewModel.FilterText)" LogEntry="@context" LaunchGenAIVisualizerCallback="@LaunchGenAIVisualizerAsync" />
<LogMessageColumnDisplay FilterText="@(ViewModel.FilterText)" LogEntry="@context" IsGenAILogCallback="@IsGenAILogEntry" LaunchGenAIVisualizerCallback="@LaunchGenAIVisualizerAsync" />
</AspireTemplateColumn>
<AspireTemplateColumn ColumnId="@TraceColumn" ColumnManager="@_manager" Title="@Loc[nameof(Dashboard.Resources.StructuredLogs.StructuredLogsTraceColumnHeader)]">
@if (!string.IsNullOrEmpty(context.TraceId))
Expand Down
44 changes: 39 additions & 5 deletions src/Aspire.Dashboard/Components/Pages/StructuredLogs.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,22 @@ public async Task UpdateViewModelFromQueryAsync(StructuredLogsPageViewModel view
await InvokeAsync(_dataGrid.SafeRefreshDataAsync);
}

private bool IsGenAILogEntry(OtlpLogEntry logEntry)
{
if (string.IsNullOrEmpty(logEntry.SpanId) || string.IsNullOrEmpty(logEntry.TraceId))
{
return false;
}

if (GenAIHelpers.HasGenAIAttribute(logEntry.Attributes))
{
// GenAI telemetry is on the log entry.
return true;
}

return ViewModel.HasGenAISpan(logEntry.TraceId, logEntry.SpanId);
}

private async Task LaunchGenAIVisualizerAsync(OtlpLogEntry logEntry)
{
var available = await TraceLinkHelpers.WaitForSpanToBeAvailableAsync(
Expand Down Expand Up @@ -571,7 +587,13 @@ await GenAIVisualizerDialog.OpenDialogAsync(
var filters = ViewModel.GetFilters();
filters.Add(new FieldTelemetryFilter
{
Field = GenAIHelpers.GenAISystem,
Field = KnownStructuredLogFields.SpanIdField,
Condition = FilterCondition.NotEqual,
Value = string.Empty
});
filters.Add(new FieldTelemetryFilter
{
Field = KnownStructuredLogFields.TraceIdField,
Condition = FilterCondition.NotEqual,
Value = string.Empty
});
Expand All @@ -584,10 +606,22 @@ await GenAIVisualizerDialog.OpenDialogAsync(
Filters = filters
});

return logs.Items
.DistinctBy(l => (l.SpanId, l.TraceId))
.Select(l => TelemetryRepository.GetSpan(l.TraceId, l.SpanId)!)
.ToList();
var genAISpans = new List<OtlpSpan>();
foreach (var l in logs.Items.DistinctBy(l => (l.SpanId, l.TraceId)))
{
var span = TelemetryRepository.GetSpan(l.TraceId, l.SpanId);
if (span == null)
{
continue;
}

if (GenAIHelpers.HasGenAIAttribute(l.Attributes) || GenAIHelpers.HasGenAIAttribute(span.Attributes))
{
genAISpans.Add(span);
}
}

return genAISpans;
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ private async Task ToggleSpanLogsAsync(OtlpLogEntry logEntry)

private static bool IsGenAISpan(SpanWaterfallViewModel spanViewModel)
{
return GenAIHelpers.IsGenAISpan(spanViewModel.Span.Attributes);
return GenAIHelpers.HasGenAIAttribute(spanViewModel.Span.Attributes);
}

private async Task OnGenAIClickedAsync(OtlpSpan span)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
HighlightText="@FilterText"
StopClickPropagation="true">
<ContentInButtonArea>
@if (!string.IsNullOrEmpty(LogEntry.TraceId) &&
!string.IsNullOrEmpty(LogEntry.SpanId) &&
GenAIHelpers.IsGenAISpan(LogEntry.Attributes))
@if (IsGenAILogCallback(LogEntry))
{
<FluentButton Appearance="Appearance.Lightweight"
Title="@ControlStringsLoc[nameof(ControlsStrings.GenAIDetailsTitle)]"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public partial class LogMessageColumnDisplay
[Parameter, EditorRequired]
public required EventCallback<OtlpLogEntry> LaunchGenAIVisualizerCallback { get; set; }

[Parameter, EditorRequired]
public required Func<OtlpLogEntry, bool> IsGenAILogCallback { get; set; }

private string? _exceptionText;

protected override void OnInitialized()
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Dashboard/Model/GenAI/GenAIHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static class GenAIHelpers

public const string ErrorType = "error.type";

public static bool IsGenAISpan(KeyValuePair<string, string>[] attributes)
public static bool HasGenAIAttribute(KeyValuePair<string, string>[] attributes)
{
return attributes.GetValueWithFallback(GenAISystem, GenAIProviderName) is { Length: > 0 };
}
Expand Down
42 changes: 38 additions & 4 deletions src/Aspire.Dashboard/Model/StructuredLogsViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using Aspire.Dashboard.Model.GenAI;
using Aspire.Dashboard.Model.Otlp;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
Expand All @@ -11,6 +13,8 @@ public class StructuredLogsViewModel
{
private readonly TelemetryRepository _telemetryRepository;
private readonly List<FieldTelemetryFilter> _filters = new();
// Cache span lookups for GenAI attributes to avoid repeated lookups.
private readonly ConcurrentDictionary<SpanKey, bool> _spanGenAICache = new();

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

public bool HasGenAISpan(string traceId, string spanId)
{
// Get a flag indicating whether the span has GenAI telemetry on it.
// This is cached to avoid repeated lookups. The cache is cleared when logs change.
// It's ok that this isn't completely thread safe, i.e. get and a clear happen at the same time.

var spanKey = new SpanKey(traceId, spanId);

if (_spanGenAICache.TryGetValue(spanKey, out var value))
{
return value;
}

var span = _telemetryRepository.GetSpan(spanKey.TraceId, spanKey.SpanId);
var hasGenAISpan = false;

if (span != null)
{
// Only cache a value if a span is present.
// We don't want to cache false if there is no span because the span may be added later.
hasGenAISpan = GenAIHelpers.HasGenAIAttribute(span.Attributes);
_spanGenAICache.TryAdd(spanKey, hasGenAISpan);
}

return hasGenAISpan;
}

public void ClearFilters()
{
_filters.Clear();
_logs = null;
ClearData();
}

public void AddFilter(FieldTelemetryFilter filter)
Expand All @@ -47,14 +78,14 @@ public void AddFilter(FieldTelemetryFilter filter)
}

_filters.Add(filter);
_logs = null;
ClearData();
}

public bool RemoveFilter(FieldTelemetryFilter filter)
{
if (_filters.Remove(filter))
{
_logs = null;
ClearData();
return true;
}
return false;
Expand All @@ -72,7 +103,7 @@ private void SetValue<T>(ref T field, T value)
}

field = value;
_logs = null;
ClearData();
}

public PagedResult<OtlpLogEntry> GetLogs()
Expand Down Expand Up @@ -135,5 +166,8 @@ public PagedResult<OtlpLogEntry> GetErrorLogs(int count)
public void ClearData()
{
_logs = null;

// Clear cache whenever log data changes to prevent it growing forever.
_spanGenAICache.Clear();
}
}
6 changes: 6 additions & 0 deletions src/Aspire.Dashboard/Otlp/Model/SpanKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// 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.Otlp.Model;

public readonly record struct SpanKey(string TraceId, string SpanId);