File: Otlp\Model\OtlpTrace.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 System.Diagnostics;
 
namespace Aspire.Dashboard.Otlp.Model;
 
[DebuggerDisplay("{DebuggerToString(),nq}")]
public class OtlpTrace
{
    private OtlpSpan? _rootSpan;
 
    public ReadOnlyMemory<byte> Key { get; }
    public string TraceId { get; }
 
    public string FullName { get; private set; }
    public OtlpSpan FirstSpan => Spans[0]; // There should always be at least one span in a trace.
    public DateTime TimeStamp => FirstSpan.StartTime;
    public OtlpSpan? RootSpan => _rootSpan;
    public TimeSpan Duration
    {
        get
        {
            var start = FirstSpan.StartTime;
            DateTime end = default;
            foreach (var span in Spans)
            {
                if (span.EndTime > end)
                {
                    end = span.EndTime;
                }
            }
            return end - start;
        }
    }
 
    public OtlpSpanCollection Spans { get; } = new OtlpSpanCollection();
 
    public int CalculateDepth(OtlpSpan span)
    {
        var depth = 0;
        var currentSpan = span;
        while (currentSpan != null)
        {
            depth++;
            currentSpan = currentSpan.GetParentSpan();
        }
        return depth;
    }
 
    public int CalculateMaxDepth() => Spans.Max(CalculateDepth);
 
    public void AddSpan(OtlpSpan span)
    {
        if (Spans.Contains(span.SpanId))
        {
            throw new InvalidOperationException($"Duplicate span id '{span.SpanId}' detected.");
        }
 
        var added = false;
        for (var i = Spans.Count - 1; i >= 0; i--)
        {
            if (span.StartTime > Spans[i].StartTime)
            {
                Spans.Insert(i + 1, span);
                added = true;
                break;
            }
        }
        if (!added)
        {
            Spans.Insert(0, span);
        }
 
        if (HasCircularReference(span))
        {
            Spans.Remove(span);
            throw new InvalidOperationException($"Circular loop detected for span '{span.SpanId}' with parent '{span.ParentSpanId}'.");
        }
 
        if (string.IsNullOrEmpty(span.ParentSpanId))
        {
            // There should only be one span with no parent span ID.
            // Incase there isn't, the first span with no parent span ID is considered to be the root.
            foreach (var existingSpan in Spans)
            {
                if (string.IsNullOrEmpty(existingSpan.ParentSpanId))
                {
                    _rootSpan = existingSpan;
                    FullName = BuildFullName(existingSpan);
                    break;
                }
            }
        }
        else if (_rootSpan == null && span == Spans[0])
        {
            // If there isn't a root span then the first span is used as the trace name.
            FullName = BuildFullName(span);
        }
 
        AssertSpanOrder();
 
        static string BuildFullName(OtlpSpan existingSpan)
        {
            return $"{existingSpan.Source.Application.ApplicationName}: {existingSpan.Name}";
        }
    }
 
    private static bool HasCircularReference(OtlpSpan span)
    {
        // Can't have a circular reference if the span has no parent.
        if (string.IsNullOrEmpty(span.ParentSpanId))
        {
            return false;
        }
 
        // Walk up span ancestors to check there is no loop.
        var stack = new OtlpSpanCollection { span };
        var currentSpan = span;
        while (currentSpan.GetParentSpan() is { } parentSpan)
        {
            if (stack.Contains(parentSpan))
            {
                return true;
            }
 
            stack.Add(parentSpan);
            currentSpan = parentSpan;
        }
 
        return false;
    }
 
    [Conditional("DEBUG")]
    private void AssertSpanOrder()
    {
        DateTime current = default;
        for (var i = 0; i < Spans.Count; i++)
        {
            var span = Spans[i];
            if (span.StartTime < current)
            {
                throw new InvalidOperationException($"Trace {TraceId} spans not in order at index {i}.");
            }
 
            current = span.StartTime;
        }
    }
 
    public OtlpTrace(ReadOnlyMemory<byte> traceId)
    {
        Key = traceId;
        TraceId = OtlpHelpers.ToHexString(traceId);
        FullName = string.Empty;
    }
 
    public static OtlpTrace Clone(OtlpTrace trace)
    {
        var newTrace = new OtlpTrace(trace.Key);
        foreach (var item in trace.Spans)
        {
            newTrace.AddSpan(OtlpSpan.Clone(item, newTrace));
        }
 
        return newTrace;
    }
 
    private string DebuggerToString()
    {
        return $@"TraceId = ""{TraceId}"", Spans = {Spans.Count}, StartDate = {FirstSpan.StartTime.ToLocalTime():yyyy:MM:dd}, StartTime = {FirstSpan.StartTime.ToLocalTime():h:mm:ss.fff tt}, Duration = {Duration}";
    }
 
    private sealed class SpanStartDateComparer : IComparer<OtlpSpan>
    {
        public static readonly SpanStartDateComparer Instance = new SpanStartDateComparer();
 
        public int Compare(OtlpSpan? x, OtlpSpan? y)
        {
            return x!.StartTime.CompareTo(y!.StartTime);
        }
    }
}