Skip to content

Commit b7f1e9e

Browse files
adamintJamesNK
authored andcommitted
Ensure visible trace spans stay sorted by start time
1 parent 0e84a68 commit b7f1e9e

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
@@ -79,24 +79,31 @@ protected override void OnInitialized()
7979
}
8080
}
8181

82-
private ValueTask<GridItemsProviderResult<SpanWaterfallViewModel>> GetData(GridItemsProviderRequest<SpanWaterfallViewModel> request)
82+
// Internal to be used in unit tests
83+
internal ValueTask<GridItemsProviderResult<SpanWaterfallViewModel>> GetData(GridItemsProviderRequest<SpanWaterfallViewModel> request)
8384
{
8485
Debug.Assert(_spanWaterfallViewModels != null);
8586

8687
var visibleViewModels = new HashSet<SpanWaterfallViewModel>();
8788
foreach (var viewModel in _spanWaterfallViewModels)
8889
{
89-
if (!viewModel.IsHidden && viewModel.MatchesFilter(_filter, GetResourceName, out var matchedDescendents))
90+
if (viewModel.IsHidden || visibleViewModels.Contains(viewModel))
91+
{
92+
continue;
93+
}
94+
95+
if (viewModel.MatchesFilter(_filter, GetResourceName, out var matchedDescendents))
9096
{
9197
visibleViewModels.Add(viewModel);
92-
foreach (var descendent in matchedDescendents)
98+
foreach (var descendent in matchedDescendents.Where(d => !d.IsHidden))
9399
{
94100
visibleViewModels.Add(descendent);
95101
}
96102
}
97103
}
98104

99-
var page = visibleViewModels.AsEnumerable();
105+
var page = _spanWaterfallViewModels.Where(visibleViewModels.Contains).AsEnumerable();
106+
var totalItemCount = page.Count();
100107
if (request.StartIndex > 0)
101108
{
102109
page = page.Skip(request.StartIndex);
@@ -106,7 +113,7 @@ private ValueTask<GridItemsProviderResult<SpanWaterfallViewModel>> GetData(GridI
106113
return ValueTask.FromResult(new GridItemsProviderResult<SpanWaterfallViewModel>
107114
{
108115
Items = page.ToList(),
109-
TotalItemCount = visibleViewModels.Count
116+
TotalItemCount = totalItemCount
110117
});
111118
}
112119

@@ -242,6 +249,7 @@ private async Task OnToggleCollapse(SpanWaterfallViewModel viewModel)
242249
_collapsedSpanIds.Add(viewModel.Span.SpanId);
243250
}
244251

252+
UpdateDetailViewData();
245253
await _dataGrid.SafeRefreshDataAsync();
246254
}
247255

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

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,158 @@ await AsyncTestHelpers.AssertIsTrueRetryAsync(() =>
156156
}, "Expected rows to be rendered.", logger);
157157
}
158158

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

0 commit comments

Comments
 (0)