File: Model\TraceHelpers.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Dashboard.Otlp.Model;
 
namespace Aspire.Dashboard.Model;
 
public static class TraceHelpers
{
    /// <summary>
    /// Recursively visit spans for a trace. Start visiting spans from unrooted spans.
    /// </summary>
    public static void VisitSpans<TState>(OtlpTrace trace, Func<OtlpSpan, TState, TState> spanAction, TState state)
    {
        // TODO: Investigate performance.
        // A trace's spans are stored in one collection and recursively iterated by matching the span id to its parent.
        // This behavior could cause excessive iteration over the span collection in large traces. Consider improving if this causes performance issues.
 
        var orderByFunc = static (OtlpSpan s) => s.StartTime;
 
        foreach (var unrootedSpan in trace.Spans.Where(s => s.GetParentSpan() == null).OrderBy(orderByFunc))
        {
            var newState = spanAction(unrootedSpan, state);
 
            Visit(trace.Spans, unrootedSpan, spanAction, newState, orderByFunc);
        }
 
        static void Visit(OtlpSpanCollection allSpans, OtlpSpan span, Func<OtlpSpan, TState, TState> spanAction, TState state, Func<OtlpSpan, DateTime> orderByFunc)
        {
            foreach (var childSpan in OtlpSpan.GetChildSpans(span, allSpans).OrderBy(orderByFunc))
            {
                var newState = spanAction(childSpan, state);
 
                Visit(allSpans, childSpan, spanAction, newState, orderByFunc);
            }
        }
    }
 
    private readonly record struct OrderedApplicationsState(DateTime? CurrentMinDate);
 
    /// <summary>
    /// Get applications for a trace, with grouped information, and ordered using min date.
    /// It is possible for spans to arrive with dates that are out of order (i.e. child span has earlier
    /// start date than the parent) so ensure it isn't possible for a child to appear before parent.
    /// </summary>
    public static IEnumerable<OrderedApplication> GetOrderedApplications(OtlpTrace trace)
    {
        var appFirstTimes = new Dictionary<OtlpApplication, OrderedApplication>();
 
        VisitSpans(trace, (OtlpSpan span, OrderedApplicationsState state) =>
        {
            var currentMinDate = (state.CurrentMinDate == null || state.CurrentMinDate < span.StartTime)
                ? span.StartTime
                : state.CurrentMinDate;
 
            if (appFirstTimes.TryGetValue(span.Source.Application, out var orderedApp))
            {
                if (currentMinDate < orderedApp.FirstDateTime)
                {
                    orderedApp.FirstDateTime = currentMinDate.Value;
                }
 
                if (span.Status == OtlpSpanStatusCode.Error)
                {
                    orderedApp.ErroredSpans++;
                }
 
                orderedApp.TotalSpans++;
            }
            else
            {
                appFirstTimes.Add(
                    span.Source.Application,
                    new OrderedApplication(span.Source.Application, appFirstTimes.Count, currentMinDate.Value, totalSpans: 1, erroredSpans: span.Status == OtlpSpanStatusCode.Error ? 1 : 0));
            }
 
            return new OrderedApplicationsState(currentMinDate);
        }, new OrderedApplicationsState(null));
 
        return appFirstTimes.Select(kvp => kvp.Value)
            .OrderBy(s => s.FirstDateTime)
            .ThenBy(s => s.Index);
    }
}
 
public sealed class OrderedApplication(OtlpApplication application, int index, DateTime firstDateTime, int totalSpans, int erroredSpans)
{
    public OtlpApplication Application { get; } = application;
    public int Index { get; } = index;
    public DateTime FirstDateTime { get; set; } = firstDateTime;
    public int TotalSpans { get; set; } = totalSpans;
    public int ErroredSpans { get; set; } = erroredSpans;
}