Skip to content

Commit da382ff

Browse files
committed
Merge remote-tracking branch 'origin/main' into tests-out-of-repo
2 parents 18b92c3 + de35684 commit da382ff

66 files changed

Lines changed: 2065 additions & 144 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Updating the test container registry
2+
3+
Docker Hub has a rate limit on how often requests can be made from an IP. Because of this, we can't use Docker Hub in our build and automation. Instead, we have created a mirror container registry (netaspireci.azurecr.io) to mirror the public images used in our tests.
4+
5+
Use the following guide to add a new image, or update an existing image with a new version.
6+
7+
1. Install az cli - https://learn.microsoft.com/cli/azure/install-azure-cli
8+
2. Log in to the container registry (Assumes you are permitted to login to the registry. Contact an admin if you need permissions.)
9+
1. `az login`
10+
2. `az acr login --name netaspireci --expose-token --output tsv --query accessToken | docker login netaspireci.azurecr.io -u 00000000-0000-0000-0000-000000000000 --password-stdin`
11+
3. See the following docs for more information
12+
1. https://learn.microsoft.com/azure/container-registry/container-registry-get-started-docker-cli#log-in-to-a-registry
13+
2. https://github.com/dotnet/dotnet-docker/blob/main/samples/push-image-to-acr.md#login-to-acr
14+
3. Pull the image locally, tag it, and push it
15+
1. `docker pull docker.io/library/redis:7.2`
16+
2. `docker tag library/redis:7.2 netaspireci.azurecr.io/library/redis:7.2`
17+
3. `docker push netaspireci.azurecr.io/library/redis:7.2`
18+
4. Alternatively, you can try the import command, but unless you have Docker Hub credentials it can fail due to rate limits.
19+
1. `az acr import --name netaspireci --source docker.io/library/redis:7.2 --image library/redis:7.2 --username <Docker Hub user name> --password <Docker Hub token>`
20+
2. See https://learn.microsoft.com/azure/container-registry/container-registry-import-images?tabs=azure-cli#import-from-docker-hub
21+
22+
> [!IMPORTANT]
23+
> Note that the image name in netaspireci.azurecr.io needs to match exactly the name in docker.io or else the test won't be able to simply override the container registry.

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

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Aspire.Dashboard.Model;
99
using Aspire.Dashboard.Otlp.Model;
1010
using Aspire.Dashboard.Otlp.Model.MetricValues;
11+
using Aspire.Dashboard.Otlp.Storage;
1112
using Aspire.Dashboard.Resources;
1213
using Aspire.Dashboard.Utils;
1314
using 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
}

src/Aspire.Dashboard/Components/Controls/Chart/ChartContainer.razor

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ else
2525
Label="@Loc[nameof(ControlsStrings.ChartContainerGraphTab)]"
2626
Icon="@(new Icons.Regular.Size24.DataArea())">
2727
<div class="metrics-chart-container metric-tab">
28-
<PlotlyChart InstrumentViewModel="_instrumentViewModel" Duration="Duration"/>
28+
<PlotlyChart InstrumentViewModel="_instrumentViewModel" Duration="Duration" Applications="Applications"/>
2929
<ChartFilters InstrumentViewModel="_instrumentViewModel" Instrument="_instrument" ViewModel="_viewModel"/>
3030
</div>
3131
</FluentTab>
@@ -34,7 +34,7 @@ else
3434
Label="@Loc[nameof(ControlsStrings.ChartContainerTableTab)]"
3535
Icon="@(new Icons.Regular.Size24.Table())">
3636
<div class="metric-tab">
37-
<MetricTable InstrumentViewModel="_instrumentViewModel" Duration="Duration"/>
37+
<MetricTable InstrumentViewModel="_instrumentViewModel" Duration="Duration" Applications="Applications" />
3838
<ChartFilters InstrumentViewModel="_instrumentViewModel" Instrument="_instrument" ViewModel="_viewModel"/>
3939
</div>
4040
</FluentTab>
@@ -48,4 +48,7 @@ else
4848

4949
[Parameter, EditorRequired]
5050
public required Func<Metrics.MetricViewKind, Task> OnViewChangedAsync { get; set; }
51+
52+
[Parameter]
53+
public required List<OtlpApplication> Applications { get; set; }
5154
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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 System.Diagnostics;
5+
using Aspire.Dashboard.Otlp.Model;
6+
7+
namespace Aspire.Dashboard.Components.Controls.Chart;
8+
9+
[DebuggerDisplay("Start = {Start}, Value = {Value}, TraceId = {TraceId}, SpanId = {SpanId}")]
10+
public class ChartExemplar
11+
{
12+
public required DateTimeOffset Start { get; init; }
13+
public required double Value { get; init; }
14+
public required string TraceId { get; init; }
15+
public required string SpanId { get; init; }
16+
public required OtlpSpan? Span { get; init; }
17+
}

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

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

44
namespace Aspire.Dashboard.Components.Controls.Chart;

0 commit comments

Comments
 (0)