88using Aspire . Dashboard . Model ;
99using Aspire . Dashboard . Otlp . Model ;
1010using Aspire . Dashboard . Otlp . Model . MetricValues ;
11+ using Aspire . Dashboard . Otlp . Storage ;
1112using Aspire . Dashboard . Resources ;
1213using Aspire . Dashboard . Utils ;
1314using Microsoft . AspNetCore . Components ;
@@ -30,18 +31,34 @@ public abstract class ChartBase : ComponentBase
3031 [ Inject ]
3132 public required IStringLocalizer < ControlsStrings > Loc { get ; init ; }
3233
34+ [ Inject ]
35+ public required IStringLocalizer < Resources . Dialogs > DialogsLoc { get ; init ; }
36+
3337 [ Inject ]
3438 public required IInstrumentUnitResolver InstrumentUnitResolver { get ; init ; }
3539
3640 [ Inject ]
3741 public required BrowserTimeProvider TimeProvider { get ; init ; }
3842
43+ [ Inject ]
44+ public required TelemetryRepository TelemetryRepository { get ; init ; }
45+
3946 [ Parameter , EditorRequired ]
4047 public required InstrumentViewModel InstrumentViewModel { get ; set ; }
4148
4249 [ Parameter , EditorRequired ]
4350 public required TimeSpan Duration { get ; set ; }
4451
52+ [ Parameter ]
53+ public required List < OtlpApplication > Applications { get ; set ; }
54+
55+ // Stores a cache of the last set of spans returned as exemplars.
56+ // This dictionary is replaced each time the chart is updated.
57+ private Dictionary < SpanKey , OtlpSpan > _currentCache = new Dictionary < SpanKey , OtlpSpan > ( ) ;
58+ private Dictionary < SpanKey , OtlpSpan > _newCache = new Dictionary < SpanKey , OtlpSpan > ( ) ;
59+
60+ private readonly record struct SpanKey ( string TraceId , string SpanId ) ;
61+
4562 protected override void OnInitialized ( )
4663 {
4764 _currentDataStartTime = GetCurrentDataTime ( ) ;
@@ -93,7 +110,7 @@ private Task OnInstrumentDataUpdate()
93110 return InvokeAsync ( StateHasChanged ) ;
94111 }
95112
96- private ( List < ChartTrace > Y , List < DateTimeOffset > X ) CalculateHistogramValues ( List < DimensionScope > dimensions , int pointCount , bool tickUpdate , DateTimeOffset inProgressDataTime , string yLabel )
113+ private ( List < ChartTrace > Y , List < DateTimeOffset > X , List < ChartExemplar > Exemplars ) CalculateHistogramValues ( List < DimensionScope > dimensions , int pointCount , bool tickUpdate , DateTimeOffset inProgressDataTime , string yLabel )
97114 {
98115 var pointDuration = Duration / pointCount ;
99116 var traces = new Dictionary < int , ChartTrace >
@@ -103,8 +120,10 @@ private Task OnInstrumentDataUpdate()
103120 [ 99 ] = new ( ) { Name = $ "P99 { yLabel } ", Percentile = 99 }
104121 } ;
105122 var xValues = new List < DateTimeOffset > ( ) ;
123+ var exemplars = new List < ChartExemplar > ( ) ;
106124 var startDate = _currentDataStartTime ;
107125 DateTimeOffset ? firstPointEndTime = null ;
126+ DateTimeOffset ? lastPointStartTime = null ;
108127
109128 // Generate the points in reverse order so that the chart is drawn from right to left.
110129 // Add a couple of extra points to the end so that the chart is drawn all the way to the right edge.
@@ -113,10 +132,11 @@ private Task OnInstrumentDataUpdate()
113132 var start = CalcOffset ( pointIndex , startDate , pointDuration ) ;
114133 var end = CalcOffset ( pointIndex - 1 , startDate , pointDuration ) ;
115134 firstPointEndTime ??= end ;
135+ lastPointStartTime = start ;
116136
117137 xValues . Add ( TimeProvider . ToLocalDateTimeOffset ( end ) ) ;
118138
119- if ( ! TryCalculateHistogramPoints ( dimensions , start , end , traces ) )
139+ if ( ! TryCalculateHistogramPoints ( dimensions , start , end , traces , exemplars ) )
120140 {
121141 foreach ( var trace in traces )
122142 {
@@ -131,7 +151,7 @@ private Task OnInstrumentDataUpdate()
131151 }
132152 xValues . Reverse ( ) ;
133153
134- if ( tickUpdate && TryCalculateHistogramPoints ( dimensions , firstPointEndTime ! . Value , inProgressDataTime , traces ) )
154+ if ( tickUpdate && TryCalculateHistogramPoints ( dimensions , firstPointEndTime ! . Value , inProgressDataTime , traces , exemplars ) )
135155 {
136156 xValues . Add ( TimeProvider . ToLocalDateTimeOffset ( inProgressDataTime ) ) ;
137157 }
@@ -161,12 +181,15 @@ private Task OnInstrumentDataUpdate()
161181
162182 previousValues = currentTrace ;
163183 }
164- return ( traces . Select ( kvp => kvp . Value ) . ToList ( ) , xValues ) ;
184+
185+ exemplars = exemplars . Where ( p => p . Start <= startDate && p . Start >= lastPointStartTime ! . Value ) . OrderBy ( p => p . Start ) . ToList ( ) ;
186+
187+ return ( traces . Select ( kvp => kvp . Value ) . ToList ( ) , xValues , exemplars ) ;
165188 }
166189
167190 private string FormatTooltip ( string name , double yValue , DateTimeOffset xValue )
168191 {
169- return $ "<b>{ HttpUtility . HtmlEncode ( InstrumentViewModel . Instrument ? . Name ) } </b><br />{ HttpUtility . HtmlEncode ( name ) } : { FormatHelpers . FormatNumberWithOptionalDecimalPlaces ( yValue , CultureInfo . CurrentCulture ) } <br />Time: { FormatHelpers . FormatTime ( TimeProvider , TimeProvider . ToLocal ( xValue ) ) } ";
192+ return $ "<b>{ HttpUtility . HtmlEncode ( InstrumentViewModel . Instrument ? . Name ) } </b><br />{ HttpUtility . HtmlEncode ( name ) } : { FormatHelpers . FormatNumberWithOptionalDecimalPlaces ( yValue , maxDecimalPlaces : 6 , CultureInfo . CurrentCulture ) } <br />Time: { FormatHelpers . FormatTime ( TimeProvider , TimeProvider . ToLocal ( xValue ) ) } ";
170193 }
171194
172195 private static HistogramValue GetHistogramValue ( MetricValueBase metric )
@@ -179,7 +202,7 @@ private static HistogramValue GetHistogramValue(MetricValueBase metric)
179202 throw new InvalidOperationException ( "Unexpected metric type: " + metric . GetType ( ) ) ;
180203 }
181204
182- internal static bool TryCalculateHistogramPoints ( List < DimensionScope > dimensions , DateTimeOffset start , DateTimeOffset end , Dictionary < int , ChartTrace > traces )
205+ internal bool TryCalculateHistogramPoints ( List < DimensionScope > dimensions , DateTimeOffset start , DateTimeOffset end , Dictionary < int , ChartTrace > traces , List < ChartExemplar > exemplars )
183206 {
184207 var hasValue = false ;
185208
@@ -199,6 +222,8 @@ internal static bool TryCalculateHistogramPoints(List<DimensionScope> dimensions
199222 {
200223 var histogramValue = GetHistogramValue ( metric ) ;
201224
225+ AddExemplars ( exemplars , metric ) ;
226+
202227 // Only use the first recorded entry if it is the beginning of data.
203228 // We can verify the first entry is the beginning of data by checking if the number of buckets equals the total count.
204229 if ( i == 0 && CountBuckets ( histogramValue ) != histogramValue . Count )
@@ -247,6 +272,57 @@ internal static bool TryCalculateHistogramPoints(List<DimensionScope> dimensions
247272 return hasValue ;
248273 }
249274
275+ private void AddExemplars ( List < ChartExemplar > exemplars , MetricValueBase metric )
276+ {
277+ if ( metric . HasExemplars )
278+ {
279+ foreach ( var exemplar in metric . Exemplars )
280+ {
281+ // TODO: Exemplars are duplicated on metrics in some scenarios.
282+ // This is a quick fix to ensure a distinct collection of metrics are displayed in the UI.
283+ // Investigation is needed into why there are duplicates.
284+ var exists = false ;
285+ foreach ( var existingExemplar in exemplars )
286+ {
287+ if ( exemplar . Start == existingExemplar . Start &&
288+ exemplar . Value == existingExemplar . Value &&
289+ exemplar . SpanId == existingExemplar . SpanId &&
290+ exemplar . TraceId == existingExemplar . TraceId )
291+ {
292+ exists = true ;
293+ break ;
294+ }
295+ }
296+ if ( exists )
297+ {
298+ continue ;
299+ }
300+
301+ // Try to find span the the local cache first.
302+ // This is done to avoid scanning a potentially large trace collection in repository.
303+ var key = new SpanKey ( exemplar . TraceId , exemplar . SpanId ) ;
304+ if ( ! _currentCache . TryGetValue ( key , out var span ) )
305+ {
306+ span = GetSpan ( exemplar . TraceId , exemplar . SpanId ) ;
307+ }
308+ if ( span != null )
309+ {
310+ _newCache [ key ] = span ;
311+ }
312+
313+ var exemplarStart = TimeProvider . ToLocalDateTimeOffset ( exemplar . Start ) ;
314+ exemplars . Add ( new ChartExemplar
315+ {
316+ Start = exemplarStart ,
317+ Value = exemplar . Value ,
318+ TraceId = exemplar . TraceId ,
319+ SpanId = exemplar . SpanId ,
320+ Span = span
321+ } ) ;
322+ }
323+ }
324+ }
325+
250326 private static ulong CountBuckets ( HistogramValue histogramValue )
251327 {
252328 ulong value = 0ul ;
@@ -287,11 +363,12 @@ private static ulong CountBuckets(HistogramValue histogramValue)
287363 return explicitBounds [ explicitBounds . Length - 1 ] ;
288364 }
289365
290- private ( List < ChartTrace > Y , List < DateTimeOffset > X ) CalculateChartValues ( List < DimensionScope > dimensions , int pointCount , bool tickUpdate , DateTimeOffset inProgressDataTime , string yLabel )
366+ private ( List < ChartTrace > Y , List < DateTimeOffset > X , List < ChartExemplar > Exemplars ) CalculateChartValues ( List < DimensionScope > dimensions , int pointCount , bool tickUpdate , DateTimeOffset inProgressDataTime , string yLabel )
291367 {
292368 var pointDuration = Duration / pointCount ;
293369 var yValues = new List < double ? > ( ) ;
294370 var xValues = new List < DateTimeOffset > ( ) ;
371+ var exemplars = new List < ChartExemplar > ( ) ;
295372 var startDate = _currentDataStartTime ;
296373 DateTimeOffset ? firstPointEndTime = null ;
297374
@@ -305,7 +382,7 @@ private static ulong CountBuckets(HistogramValue histogramValue)
305382
306383 xValues . Add ( TimeProvider . ToLocalDateTimeOffset ( end ) ) ;
307384
308- if ( TryCalculatePoint ( dimensions , start , end , out var tickPointValue ) )
385+ if ( TryCalculatePoint ( dimensions , start , end , exemplars , out var tickPointValue ) )
309386 {
310387 yValues . Add ( tickPointValue ) ;
311388 }
@@ -318,7 +395,7 @@ private static ulong CountBuckets(HistogramValue histogramValue)
318395 yValues . Reverse ( ) ;
319396 xValues . Reverse ( ) ;
320397
321- if ( tickUpdate && TryCalculatePoint ( dimensions , firstPointEndTime ! . Value , inProgressDataTime , out var inProgressPointValue ) )
398+ if ( tickUpdate && TryCalculatePoint ( dimensions , firstPointEndTime ! . Value , inProgressDataTime , exemplars , out var inProgressPointValue ) )
322399 {
323400 yValues . Add ( inProgressPointValue ) ;
324401 xValues . Add ( TimeProvider . ToLocalDateTimeOffset ( inProgressDataTime ) ) ;
@@ -343,10 +420,10 @@ private static ulong CountBuckets(HistogramValue histogramValue)
343420 }
344421 }
345422
346- return ( [ trace ] , xValues ) ;
423+ return ( [ trace ] , xValues , exemplars ) ;
347424 }
348425
349- private static bool TryCalculatePoint ( List < DimensionScope > dimensions , DateTimeOffset start , DateTimeOffset end , out double pointValue )
426+ private bool TryCalculatePoint ( List < DimensionScope > dimensions , DateTimeOffset start , DateTimeOffset end , List < ChartExemplar > exemplars , out double pointValue )
350427 {
351428 var hasValue = false ;
352429 pointValue = 0d ;
@@ -371,6 +448,8 @@ private static bool TryCalculatePoint(List<DimensionScope> dimensions, DateTimeO
371448 dimensionValue = Math . Max ( value , dimensionValue ) ;
372449 hasValue = true ;
373450 }
451+
452+ AddExemplars ( exemplars , metric ) ;
374453 }
375454
376455 pointValue += dimensionValue ;
@@ -406,16 +485,29 @@ private async Task UpdateChart(bool tickUpdate, DateTimeOffset inProgressDataTim
406485
407486 List < ChartTrace > traces ;
408487 List < DateTimeOffset > xValues ;
488+ List < ChartExemplar > exemplars ;
409489 if ( InstrumentViewModel . Instrument ? . Type != OtlpInstrumentType . Histogram || InstrumentViewModel . ShowCount )
410490 {
411- ( traces , xValues ) = CalculateChartValues ( InstrumentViewModel . MatchedDimensions , GraphPointCount , tickUpdate , inProgressDataTime , unit ) ;
491+ ( traces , xValues , exemplars ) = CalculateChartValues ( InstrumentViewModel . MatchedDimensions , GraphPointCount , tickUpdate , inProgressDataTime , unit ) ;
492+
493+ // TODO: Exemplars on non-histogram charts doesn't work well. Don't display for now.
494+ exemplars . Clear ( ) ;
412495 }
413496 else
414497 {
415- ( traces , xValues ) = CalculateHistogramValues ( InstrumentViewModel . MatchedDimensions , GraphPointCount , tickUpdate , inProgressDataTime , unit ) ;
498+ ( traces , xValues , exemplars ) = CalculateHistogramValues ( InstrumentViewModel . MatchedDimensions , GraphPointCount , tickUpdate , inProgressDataTime , unit ) ;
416499 }
417500
418- await OnChartUpdated ( traces , xValues , tickUpdate , inProgressDataTime ) ;
501+ // Replace cache for next update.
502+ _currentCache = _newCache ;
503+ _newCache = new Dictionary < SpanKey , OtlpSpan > ( ) ;
504+
505+ await OnChartUpdated ( traces , xValues , exemplars , tickUpdate , inProgressDataTime ) ;
506+ }
507+
508+ protected OtlpSpan ? GetSpan ( string traceId , string spanId )
509+ {
510+ return MetricsHelpers . GetSpan ( TelemetryRepository , traceId , spanId ) ;
419511 }
420512
421513 private DateTimeOffset GetCurrentDataTime ( )
@@ -425,8 +517,8 @@ private DateTimeOffset GetCurrentDataTime()
425517
426518 private string GetDisplayedUnit ( OtlpInstrument instrument )
427519 {
428- return InstrumentUnitResolver . ResolveDisplayedUnit ( instrument ) ;
520+ return InstrumentUnitResolver . ResolveDisplayedUnit ( instrument , titleCase : true , pluralize : true ) ;
429521 }
430522
431- protected abstract Task OnChartUpdated ( List < ChartTrace > traces , List < DateTimeOffset > xValues , bool tickUpdate , DateTimeOffset inProgressDataTime ) ;
523+ protected abstract Task OnChartUpdated ( List < ChartTrace > traces , List < DateTimeOffset > xValues , List < ChartExemplar > exemplars , bool tickUpdate , DateTimeOffset inProgressDataTime ) ;
432524}
0 commit comments