Skip to content

Commit f31a50f

Browse files
authored
Add metrics overflow warning to the dashboard (#7784)
1 parent fe81722 commit f31a50f

25 files changed

+421
-39
lines changed

playground/Stress/Stress.ApiService/Program.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Globalization;
56
using System.Text;
67
using System.Text.Json.Nodes;
78
using System.Threading.Channels;
@@ -55,6 +56,21 @@
5556
return "Counter incremented";
5657
});
5758

59+
app.MapGet("/overflow-counter", (TestMetrics metrics) =>
60+
{
61+
// Emit measurements to ensure at least 2000 unique tag values are emitted,
62+
// matching the default cardinality limit in OpenTelemetry.
63+
for (var i = 0; i < 250; i++)
64+
{
65+
for (int j = 0; j < 10; j++)
66+
{
67+
metrics.IncrementCounter(1, new TagList([new KeyValuePair<string, object?>($"add-tag-{i}", j.ToString(CultureInfo.InvariantCulture))]));
68+
}
69+
}
70+
71+
return "Counter overflowed";
72+
});
73+
5874
app.MapGet("/big-trace", async () =>
5975
{
6076
var traceCreator = new TraceCreator

playground/Stress/Stress.ApiService/TestMetrics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public TestMetrics()
2020
new KeyValuePair<string, object?>("meter-tag", Guid.NewGuid().ToString())
2121
]);
2222

23-
_counter = _meter.CreateCounter<int>("test-counter", unit: null, description: null, tags:
23+
_counter = _meter.CreateCounter<int>("test-counter", unit: null, description: "This is a description", tags:
2424
[
2525
new KeyValuePair<string, object?>("instrument-tag", Guid.NewGuid().ToString())
2626
]);

playground/Stress/Stress.AppHost/Program.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
}
2727
}
2828

29-
var serviceBuilder = builder.AddProject<Projects.Stress_ApiService>("stress-apiservice", launchProfileName: null);
29+
// TODO: OTEL env var can be removed when OTEL libraries are updated to 1.9.0
30+
// See https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/RELEASENOTES.md#1100
31+
var serviceBuilder = builder.AddProject<Projects.Stress_ApiService>("stress-apiservice", launchProfileName: null)
32+
.WithEnvironment("OTEL_DOTNET_EXPERIMENTAL_METRICS_EMIT_OVERFLOW_ATTRIBUTE", "true");
3033
serviceBuilder.WithCommand(
3134
name: "icon-test",
3235
displayName: "Icon test",
@@ -59,6 +62,7 @@
5962
serviceBuilder.WithHttpCommand("/log-message", "Log message", method: HttpMethod.Get, iconName: "ContentViewGalleryLightning");
6063
serviceBuilder.WithHttpCommand("/log-message-limit", "Log message limit", method: HttpMethod.Get, iconName: "ContentViewGalleryLightning");
6164
serviceBuilder.WithHttpCommand("/multiple-traces-linked", "Multiple traces linked", method: HttpMethod.Get, iconName: "ContentViewGalleryLightning");
65+
serviceBuilder.WithHttpCommand("/overflow-counter", "Overflow counter", method: HttpMethod.Get, iconName: "ContentViewGalleryLightning");
6266

6367
builder.AddProject<Projects.Stress_TelemetryService>("stress-telemetryservice");
6468

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ else
1515
<div class="metrics-chart-header">
1616
<h3>@_instrument.Summary.Name</h3>
1717
<p>@_instrument.Summary.Description</p>
18+
@if (_instrument.HasOverflow)
19+
{
20+
<div class="overflow-warning">
21+
<div class="overflow-warning-icon">
22+
<FluentIcon Value="new Icons.Filled.Size16.ErrorCircle()" Color="Color.Error" />
23+
</div>
24+
25+
<div class="overflow-warning-message">
26+
<span class="title">@Loc[nameof(ControlsStrings.ChartContainerOverflowTitle)]</span>
27+
@Loc[nameof(ControlsStrings.ChartContainerOverflowDescription)]<br/>
28+
@((MarkupString)string.Format(ControlsStrings.ChartContainerOverflowLink, "https://aka.ms/dotnet/aspire/cardinality-limits"))
29+
</div>
30+
</div>
31+
}
1832
</div>
1933
<FluentTabs ActiveTabId="@($"tab-{ActiveView}")" OnTabChange="@OnTabChangeAsync">
2034
<FluentTab LabelClass="tab-label"

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

Lines changed: 5 additions & 5 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.Immutable;
5+
using System.Runtime.InteropServices;
46
using Aspire.Dashboard.Model;
57
using Aspire.Dashboard.Otlp.Model;
68
using Aspire.Dashboard.Otlp.Model.MetricValues;
@@ -41,7 +43,7 @@ public partial class ChartContainer : ComponentBase, IAsyncDisposable
4143
[Inject]
4244
public required ThemeManager ThemeManager { get; init; }
4345

44-
public List<DimensionFilterViewModel> DimensionFilters { get; } = [];
46+
public ImmutableList<DimensionFilterViewModel> DimensionFilters { get; set; } = [];
4547
public string? PreviousMeterName { get; set; }
4648
public string? PreviousInstrumentName { get; set; }
4749

@@ -158,10 +160,8 @@ protected override async Task OnParametersSetAsync()
158160
PreviousMeterName = MeterName;
159161
PreviousInstrumentName = InstrumentName;
160162

161-
var filters = CreateUpdatedFilters(hasInstrumentChanged);
162-
163-
DimensionFilters.Clear();
164-
DimensionFilters.AddRange(filters);
163+
// Replace filters collection on change. Filters can be accessed from a background task so it is immutable for thread safety.
164+
DimensionFilters = ImmutableList.Create(CollectionsMarshal.AsSpan(CreateUpdatedFilters(hasInstrumentChanged)));
165165

166166
await UpdateInstrumentDataAsync(_instrument);
167167
}
Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,42 @@
1-
::deep .tab-label > svg {
1+
::deep .tab-label > svg {
22
margin-right: 2px;
33
}
44

55
::deep .metric-tab {
66
margin-top: 10px;
77
}
8+
9+
.overflow-warning {
10+
font-family: var(--body-font);
11+
border: 1px solid var(--messagebar-warning-border-color);
12+
background-color: var(--messagebar-warning-background-color);
13+
color: var(--neutral-foreground-rest);
14+
display: grid;
15+
grid-template-columns: 24px auto;
16+
width: fit-content;
17+
align-items: center;
18+
min-height: 36px;
19+
border-radius: calc(var(--control-corner-radius)* 1px);
20+
padding: 0 12px;
21+
column-gap: 8px;
22+
}
23+
24+
.overflow-warning-icon {
25+
grid-column: 1;
26+
display: flex;
27+
justify-content: center;
28+
}
29+
30+
.overflow-warning-message {
31+
grid-column: 2;
32+
padding: 10px 0;
33+
align-self: center;
34+
font-size: 12px;
35+
font-weight: 400;
36+
line-height: 16px;
37+
}
38+
39+
.overflow-warning-message .title {
40+
font-weight: 600;
41+
padding: 0 4px 0 0;
42+
}

src/Aspire.Dashboard/Components/Controls/Chart/ChartFilters.razor.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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.Immutable;
45
using Aspire.Dashboard.Model;
56
using Aspire.Dashboard.Otlp.Model;
67
using Microsoft.AspNetCore.Components;
@@ -16,7 +17,7 @@ public partial class ChartFilters
1617
public required InstrumentViewModel InstrumentViewModel { get; set; }
1718

1819
[Parameter, EditorRequired]
19-
public required List<DimensionFilterViewModel> DimensionFilters { get; set; }
20+
public required ImmutableList<DimensionFilterViewModel> DimensionFilters { get; set; }
2021

2122
public bool ShowCounts { get; set; }
2223

src/Aspire.Dashboard/Otlp/Model/OtlpInstrument.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class OtlpInstrumentData
2727
public required OtlpInstrumentSummary Summary { get; init; }
2828
public required List<DimensionScope> Dimensions { get; init; }
2929
public required Dictionary<string, List<string?>> KnownAttributeValues { get; init; }
30+
public required bool HasOverflow { get; init; }
3031
}
3132

3233
[DebuggerDisplay("Name = {Summary.Name}, Unit = {Summary.Unit}, Type = {Summary.Type}")]
@@ -37,9 +38,17 @@ public class OtlpInstrument
3738

3839
public Dictionary<ReadOnlyMemory<KeyValuePair<string, string>>, DimensionScope> Dimensions { get; } = new(ScopeAttributesComparer.Instance);
3940
public Dictionary<string, List<string?>> KnownAttributeValues { get; } = new();
41+
public bool HasOverflow { get; set; }
4042

4143
public DimensionScope FindScope(RepeatedField<KeyValue> attributes, ref KeyValuePair<string, string>[]? tempAttributes)
4244
{
45+
// See https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#overflow-attribute
46+
// Inspect attributes before they're merged with parent attributes. "otel.metric.overflow" should be the only attribute.
47+
if (!HasOverflow && attributes.Count == 1 && attributes[0].Key == "otel.metric.overflow" && attributes[0].Value.GetString() == "true")
48+
{
49+
HasOverflow = true;
50+
}
51+
4352
// We want to find the dimension scope that matches the attributes, but we don't want to allocate.
4453
// Copy values to a temporary reusable array.
4554
//
@@ -101,7 +110,8 @@ public static OtlpInstrument Clone(OtlpInstrument instrument, bool cloneData, Da
101110
var newInstrument = new OtlpInstrument
102111
{
103112
Summary = instrument.Summary,
104-
Context = instrument.Context
113+
Context = instrument.Context,
114+
HasOverflow = instrument.HasOverflow
105115
};
106116

107117
if (cloneData)

src/Aspire.Dashboard/Otlp/Storage/TelemetryRepository.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,13 +1174,15 @@ public List<OtlpInstrumentSummary> GetInstrumentsSummaries(ApplicationKey key)
11741174
{
11751175
Summary = instrument.Summary,
11761176
KnownAttributeValues = instrument.KnownAttributeValues,
1177-
Dimensions = instrument.Dimensions.Values.ToList()
1177+
Dimensions = instrument.Dimensions.Values.ToList(),
1178+
HasOverflow = instrument.HasOverflow
11781179
};
11791180
}
11801181
else
11811182
{
11821183
var allDimensions = new List<DimensionScope>();
11831184
var allKnownAttributes = new Dictionary<string, List<string?>>();
1185+
var hasOverflow = false;
11841186

11851187
foreach (var instrument in instruments)
11861188
{
@@ -1199,13 +1201,16 @@ public List<OtlpInstrumentSummary> GetInstrumentsSummaries(ApplicationKey key)
11991201
values = knownAttributeValues.Value.ToList();
12001202
}
12011203
}
1204+
1205+
hasOverflow = hasOverflow || instrument.HasOverflow;
12021206
}
12031207

12041208
return new OtlpInstrumentData
12051209
{
12061210
Summary = instruments[0].Summary,
12071211
Dimensions = allDimensions,
1208-
KnownAttributeValues = allKnownAttributes
1212+
KnownAttributeValues = allKnownAttributes,
1213+
HasOverflow = hasOverflow
12091214
};
12101215
}
12111216
}

src/Aspire.Dashboard/Resources/ControlsStrings.Designer.cs

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)