Skip to content

Commit b590865

Browse files
JamesNKadamint
andauthored
Ensure visible trace spans stay sorted by start time (#8909)
Co-authored-by: Adam Ratzman <adam@adamratzman.com>
1 parent 2b55ce1 commit b590865

File tree

4 files changed

+233
-6
lines changed

4 files changed

+233
-6
lines changed

playground/Stress/Stress.ApiService/Program.cs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
builder.AddServiceDefaults();
1616

1717
builder.Services.AddOpenTelemetry()
18-
.WithTracing(tracing => tracing.AddSource(TraceCreator.ActivitySourceName, ProducerConsumer.ActivitySourceName))
18+
.WithTracing(tracing => tracing
19+
.AddSource(TraceCreator.ActivitySourceName, ProducerConsumer.ActivitySourceName)
20+
.AddSource("Services.Api"))
1921
.WithMetrics(metrics => metrics.AddMeter(TestMetrics.MeterName));
2022
builder.Services.AddSingleton<TestMetrics>();
2123

@@ -285,4 +287,68 @@ async IAsyncEnumerable<string> WriteOutput()
285287
return $"Created {TraceCount} traces.";
286288
});
287289

290+
app.MapGet("/nested-trace-spans", async () =>
291+
{
292+
var forecast = Enumerable.Range(1, 5).Select(index =>
293+
new WeatherForecast
294+
(
295+
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
296+
Random.Shared.Next(-20, 55),
297+
"Sample Text"
298+
))
299+
.ToArray();
300+
ActivitySource source = new("Services.Api", "1.0.0");
301+
ActivitySource.AddActivityListener(new ActivityListener
302+
{
303+
ShouldListenTo = _ => true,
304+
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
305+
});
306+
using var activity = source.StartActivity("ValidateAndUpdateCacheService.ExecuteAsync");
307+
await Task.Delay(100);
308+
Debug.Assert(activity is not null);
309+
using var innerActivity = source.StartActivity("ValidateAndUpdateCacheService.activeUser",
310+
ActivityKind.Internal, parentContext: activity.Context);
311+
await Task.Delay(100);
312+
Debug.Assert(innerActivity is not null);
313+
using (source.StartActivity("Perform1", ActivityKind.Internal, parentContext: innerActivity.Context))
314+
{
315+
await Task.Delay(10);
316+
}
317+
318+
using (source.StartActivity("Perform2", ActivityKind.Internal, parentContext: innerActivity.Context))
319+
{
320+
await Task.Delay(20);
321+
}
322+
323+
using (source.StartActivity("Perform3", ActivityKind.Internal, parentContext: innerActivity.Context))
324+
{
325+
await Task.Delay(30);
326+
}
327+
328+
using var innerActivity2 = source.StartActivity("ValidateAndUpdateCacheService.activeUser",
329+
ActivityKind.Internal, parentContext: activity.Context);
330+
await Task.Delay(100);
331+
Debug.Assert(innerActivity2 is not null);
332+
333+
using (source.StartActivity("Perform1", ActivityKind.Internal, parentContext: innerActivity2.Context))
334+
{
335+
await Task.Delay(30);
336+
}
337+
338+
using (source.StartActivity("Perform2", ActivityKind.Internal, parentContext: innerActivity2.Context))
339+
{
340+
await Task.Delay(20);
341+
}
342+
343+
using (source.StartActivity("Perform3", ActivityKind.Internal, parentContext: innerActivity2.Context))
344+
{
345+
await Task.Delay(10);
346+
}
347+
348+
return forecast;
349+
})
350+
.WithName("GetWeatherForecast");
351+
288352
app.Run();
353+
354+
public record WeatherForecast(DateOnly Date, int TemperatureC, string Summary);

playground/Stress/Stress.AppHost/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
serviceBuilder.WithHttpCommand("/log-message-limit", "Log message limit", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
7070
serviceBuilder.WithHttpCommand("/multiple-traces-linked", "Multiple traces linked", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
7171
serviceBuilder.WithHttpCommand("/overflow-counter", "Overflow counter", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
72+
serviceBuilder.WithHttpCommand("/nested-trace-spans", "Out of order nested spans", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
7273

7374
builder.AddProject<Projects.Stress_TelemetryService>("stress-telemetryservice")
7475
.WithUrls(c => c.Urls.Add(new() { Url = "https://someplace.com", DisplayText = "Some place" }))

src/Aspire.Dashboard/Components/Pages/TraceDetail.razor.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,24 +76,31 @@ protected override void OnInitialized()
7676
}
7777
}
7878

79-
private ValueTask<GridItemsProviderResult<SpanWaterfallViewModel>> GetData(GridItemsProviderRequest<SpanWaterfallViewModel> request)
79+
// Internal to be used in unit tests
80+
internal ValueTask<GridItemsProviderResult<SpanWaterfallViewModel>> GetData(GridItemsProviderRequest<SpanWaterfallViewModel> request)
8081
{
8182
Debug.Assert(_spanWaterfallViewModels != null);
8283

8384
var visibleViewModels = new HashSet<SpanWaterfallViewModel>();
8485
foreach (var viewModel in _spanWaterfallViewModels)
8586
{
86-
if (!viewModel.IsHidden && viewModel.MatchesFilter(_filter, GetResourceName, out var matchedDescendents))
87+
if (viewModel.IsHidden || visibleViewModels.Contains(viewModel))
88+
{
89+
continue;
90+
}
91+
92+
if (viewModel.MatchesFilter(_filter, GetResourceName, out var matchedDescendents))
8793
{
8894
visibleViewModels.Add(viewModel);
89-
foreach (var descendent in matchedDescendents)
95+
foreach (var descendent in matchedDescendents.Where(d => !d.IsHidden))
9096
{
9197
visibleViewModels.Add(descendent);
9298
}
9399
}
94100
}
95101

96-
var page = visibleViewModels.AsEnumerable();
102+
var page = _spanWaterfallViewModels.Where(visibleViewModels.Contains).AsEnumerable();
103+
var totalItemCount = page.Count();
97104
if (request.StartIndex > 0)
98105
{
99106
page = page.Skip(request.StartIndex);
@@ -103,7 +110,7 @@ private ValueTask<GridItemsProviderResult<SpanWaterfallViewModel>> GetData(GridI
103110
return ValueTask.FromResult(new GridItemsProviderResult<SpanWaterfallViewModel>
104111
{
105112
Items = page.ToList(),
106-
TotalItemCount = visibleViewModels.Count
113+
TotalItemCount = totalItemCount
107114
});
108115
}
109116

@@ -236,6 +243,7 @@ private async Task OnToggleCollapse(SpanWaterfallViewModel viewModel)
236243
_collapsedSpanIds.Add(viewModel.Span.SpanId);
237244
}
238245

246+
UpdateDetailViewData();
239247
await _dataGrid.SafeRefreshDataAsync();
240248
}
241249

tests/Aspire.Dashboard.Components.Tests/Pages/TraceDetailsTests.cs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,158 @@ public async Task Render_ChangeTrace_RowsRendered()
137137
await AsyncTestHelpers.AssertIsTrueRetryAsync(() => rows.Count == 2, "Expected rows to be rendered.");
138138
}
139139

140+
[Fact]
141+
public async Task Render_SpansOrderedByStartTime_RowsRenderedInCorrectOrder()
142+
{
143+
// Arrange
144+
SetupTraceDetailsServices();
145+
146+
var viewport = new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false);
147+
148+
var dimensionManager = Services.GetRequiredService<DimensionManager>();
149+
dimensionManager.InvokeOnViewportInformationChanged(viewport);
150+
151+
var telemetryRepository = Services.GetRequiredService<TelemetryRepository>();
152+
telemetryRepository.AddTraces(new AddContext(),
153+
new RepeatedField<ResourceSpans>
154+
{
155+
new ResourceSpans
156+
{
157+
Resource = CreateResource(),
158+
ScopeSpans =
159+
{
160+
new ScopeSpans
161+
{
162+
Scope = CreateScope(),
163+
Spans =
164+
{
165+
CreateSpan(traceId: "1", spanId: "1-1",
166+
startTime: s_testTime.AddMinutes(1),
167+
endTime: s_testTime.AddMinutes(10)),
168+
CreateSpan(traceId: "1", spanId: "2-1",
169+
startTime: s_testTime.AddMinutes(1),
170+
endTime: s_testTime.AddMinutes(10),
171+
parentSpanId: "1-1"),
172+
CreateSpan(traceId: "1", spanId: "3-1",
173+
startTime: s_testTime.AddMinutes(1),
174+
endTime: s_testTime.AddMinutes(10),
175+
parentSpanId: "2-1"),
176+
CreateSpan(traceId: "1", spanId: "3-3",
177+
startTime: s_testTime.AddMinutes(3),
178+
endTime: s_testTime.AddMinutes(5),
179+
parentSpanId: "2-1"),
180+
CreateSpan(traceId: "1", spanId: "3-2",
181+
startTime: s_testTime.AddMinutes(2),
182+
endTime: s_testTime.AddMinutes(6),
183+
parentSpanId: "2-1")
184+
}
185+
}
186+
}
187+
}
188+
});
189+
190+
// Act
191+
var traceId = Convert.ToHexString(Encoding.UTF8.GetBytes("1"));
192+
var cut = RenderComponent<TraceDetail>(builder =>
193+
{
194+
builder.Add(p => p.TraceId, traceId);
195+
builder.AddCascadingValue(viewport);
196+
});
197+
198+
var data = await cut.Instance.GetData(new GridItemsProviderRequest<SpanWaterfallViewModel>());
199+
200+
// Assert
201+
Assert.Collection(data.Items,
202+
item => Assert.Equal("Test span. Id: 1-1", item.Span.Name),
203+
item => Assert.Equal("Test span. Id: 2-1", item.Span.Name),
204+
item => Assert.Equal("Test span. Id: 3-1", item.Span.Name),
205+
item => Assert.Equal("Test span. Id: 3-2", item.Span.Name),
206+
item => Assert.Equal("Test span. Id: 3-3", item.Span.Name));
207+
}
208+
209+
[Fact]
210+
public void ToggleCollapse_SpanStateChanges()
211+
{
212+
// Arrange
213+
SetupTraceDetailsServices();
214+
215+
var viewport = new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false);
216+
var dimensionManager = Services.GetRequiredService<DimensionManager>();
217+
dimensionManager.InvokeOnViewportInformationChanged(viewport);
218+
219+
var telemetryRepository = Services.GetRequiredService<TelemetryRepository>();
220+
telemetryRepository.AddTraces(new AddContext(),
221+
new RepeatedField<ResourceSpans>
222+
{
223+
new ResourceSpans
224+
{
225+
Resource = CreateResource(),
226+
ScopeSpans =
227+
{
228+
new ScopeSpans
229+
{
230+
Scope = CreateScope(),
231+
Spans =
232+
{
233+
CreateSpan(traceId: "1", spanId: "1-1",
234+
startTime: s_testTime.AddMinutes(1),
235+
endTime: s_testTime.AddMinutes(10)),
236+
CreateSpan(traceId: "1", spanId: "2-1",
237+
startTime: s_testTime.AddMinutes(5),
238+
endTime: s_testTime.AddMinutes(10), parentSpanId: "1-1"),
239+
CreateSpan(traceId: "1", spanId: "3-1",
240+
startTime: s_testTime.AddMinutes(6),
241+
endTime: s_testTime.AddMinutes(10), parentSpanId: "2-1")
242+
}
243+
}
244+
}
245+
}
246+
});
247+
248+
var traceId = Convert.ToHexString(Encoding.UTF8.GetBytes("1"));
249+
var cut = RenderComponent<TraceDetail>(builder =>
250+
{
251+
builder.Add(p => p.TraceId, traceId);
252+
builder.AddCascadingValue(viewport);
253+
});
254+
255+
cut.WaitForAssertion(() => Assert.Equal(2, cut.FindAll(".main-grid-expand-button").Count));
256+
// Act and assert
257+
258+
// Collapse the middle span
259+
cut.FindAll(".main-grid-expand-button")[1].Click();
260+
261+
cut.WaitForAssertion(() =>
262+
{
263+
var expandContainers = cut.FindAll(".main-grid-expand-container");
264+
// There should now be two containers since the 3rd level element should now be filtered out
265+
Assert.Collection(expandContainers,
266+
container => Assert.True(container.ClassList.Contains("main-grid-expanded")),
267+
container => Assert.True(container.ClassList.Contains("main-grid-collapsed")));
268+
});
269+
270+
// Collapse the parent span
271+
cut.FindAll(".main-grid-expand-button")[0].Click();
272+
cut.WaitForAssertion(() =>
273+
{
274+
var expandContainers = cut.FindAll(".main-grid-expand-container");
275+
// There should now be one container since the 2nd level element should now be filtered out
276+
Assert.Collection(expandContainers,
277+
container => Assert.True(container.ClassList.Contains("main-grid-collapsed")));
278+
});
279+
280+
// Expand the parent span, we should now see the same two containers as before
281+
cut.FindAll(".main-grid-expand-button")[0].Click();
282+
cut.WaitForAssertion(() =>
283+
{
284+
var expandContainers = cut.FindAll(".main-grid-expand-container");
285+
// There should now be two containers since the 3rd level element should now be filtered out
286+
Assert.Collection(expandContainers,
287+
container => Assert.True(container.ClassList.Contains("main-grid-expanded")),
288+
container => Assert.True(container.ClassList.Contains("main-grid-collapsed")));
289+
});
290+
}
291+
140292
private void SetupTraceDetailsServices()
141293
{
142294
var version = typeof(FluentMain).Assembly.GetName().Version!;

0 commit comments

Comments
 (0)