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 ;
46using Aspire . Dashboard . Model . Otlp ;
57using Aspire . Dashboard . Otlp . Model ;
68using 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}
0 commit comments