File: src\Shared\Otlp\OtlpHelpers.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// 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;
using System.Globalization;
 
namespace Aspire.Dashboard.Otlp.Model;
 
/// <summary>
/// Shared helper methods for working with OTLP data.
/// Used by both Dashboard and CLI.
/// </summary>
public static partial class OtlpHelpers
{
    /// <summary>
    /// The attribute name for Aspire's log entry ID.
    /// </summary>
    public const string AspireLogIdAttribute = "aspire.log_id";
 
    /// <summary>
    /// The attribute name for the resolved destination name of a span.
    /// </summary>
    public const string AspireDestinationNameAttribute = "aspire.destination";
 
    /// <summary>
    /// The standard length for shortened trace/span IDs.
    /// </summary>
    public const int ShortenedIdLength = 7;
 
    /// <summary>
    /// Shortens a trace or span ID to the standard display length.
    /// </summary>
    public static string ToShortenedId(string id) => TruncateString(id, maxLength: ShortenedIdLength);
 
    /// <summary>
    /// Truncates a string to the specified maximum length.
    /// </summary>
    public static string TruncateString(string value, int maxLength)
    {
        return value.Length > maxLength ? value[..maxLength] : value;
    }
 
    /// <summary>
    /// Converts Unix nanoseconds to a DateTime (UTC).
    /// </summary>
    public static DateTime UnixNanoSecondsToDateTime(ulong unixTimeNanoseconds)
    {
        var ticks = NanosecondsToTicks(unixTimeNanoseconds);
        return DateTime.UnixEpoch.AddTicks(ticks);
    }
 
    /// <summary>
    /// Converts a DateTime to Unix nanoseconds.
    /// </summary>
    public static ulong DateTimeToUnixNanoseconds(DateTime dateTime)
    {
        var timeSinceEpoch = dateTime.ToUniversalTime() - DateTime.UnixEpoch;
        return (ulong)timeSinceEpoch.Ticks * TimeSpan.NanosecondsPerTick;
    }
 
    /// <summary>
    /// Converts nanoseconds to ticks.
    /// </summary>
    public static long NanosecondsToTicks(ulong nanoseconds)
    {
        return (long)(nanoseconds / TimeSpan.NanosecondsPerTick);
    }
 
    /// <summary>
    /// Converts nanoseconds to a TimeSpan.
    /// </summary>
    public static TimeSpan NanosecondsToTimeSpan(ulong nanoseconds)
    {
        return TimeSpan.FromTicks(NanosecondsToTicks(nanoseconds));
    }
 
    /// <summary>
    /// Calculates duration as a TimeSpan from start and end nanosecond timestamps.
    /// </summary>
    public static TimeSpan CalculateDuration(ulong? startNano, ulong? endNano)
    {
        if (startNano.HasValue && endNano.HasValue && endNano.Value >= startNano.Value)
        {
            return NanosecondsToTimeSpan(endNano.Value - startNano.Value);
        }
        return TimeSpan.Zero;
    }
 
    public static string GetResourceName(IOtlpResource resource, IReadOnlyList<IOtlpResource> allResources)
    {
        var count = 0;
        foreach (var item in allResources)
        {
            if (string.Equals(item.ResourceName, resource.ResourceName, StringComparisons.ResourceName))
            {
                count++;
                if (count >= 2)
                {
                    var instanceId = resource.InstanceId;
 
                    // Convert long GUID into a shorter, more human friendly format.
                    // The last characters are used because version 7 GUIDs created close
                    // in time share the same leading characters, e.g. Guid.CreateVersion7().
                    // Before: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
                    // After:  eeeeeeee
                    if (instanceId != null && Guid.TryParse(instanceId, out var guid))
                    {
                        Span<char> chars = stackalloc char[32];
                        var result = guid.TryFormat(chars, out var charsWritten, format: "N");
                        Debug.Assert(result, "Guid.TryFormat not successful.");
 
                        instanceId = chars.Slice(charsWritten - 8, 8).ToString();
                    }
 
                    if (instanceId == null)
                    {
                        return item.ResourceName;
                    }
 
                    return $"{item.ResourceName}-{instanceId}";
                }
            }
        }
 
        return resource.ResourceName;
    }
 
    /// <summary>
    /// Finds a resource by composite name first, then falls back to all resources with the base name.
    /// Returns no matches when a composite name identifies multiple resources or also matches a resource's
    /// base name because joining the resource name and instance ID with a dash is not guaranteed to produce
    /// a unique value.
    /// </summary>
    internal static IReadOnlyList<T> ResolveResourceNameMatches<T>(string resourceName, IEnumerable<T> resources)
        where T : IOtlpResource
    {
        var compositeNameMatches = new List<T>();
        var resourceNameMatches = new List<T>();
 
        foreach (var resource in resources)
        {
            if (resource.InstanceId is { } instanceId && EqualsCompositeName(resource.ResourceName, instanceId, resourceName))
            {
                compositeNameMatches.Add(resource);
            }
 
            if (string.Equals(resource.ResourceName, resourceName, StringComparisons.ResourceName))
            {
                resourceNameMatches.Add(resource);
            }
        }
 
        return compositeNameMatches.Count switch
        {
            0 => resourceNameMatches,
            1 when resourceNameMatches.Count == 0 => compositeNameMatches,
            _ => []
        };
    }
 
    private static bool EqualsCompositeName(string resourceName, string instanceId, string value)
    {
        // Composite names have the format "{resourceName}-{instanceId}". Compare each
        // segment without allocating the composite string because this runs for every resource.
        return value.Length == resourceName.Length + instanceId.Length + 1
            && value.AsSpan(0, resourceName.Length).Equals(resourceName, StringComparisons.ResourceName)
            && value[resourceName.Length] == '-'
            && value.AsSpan(resourceName.Length + 1, instanceId.Length).Equals(instanceId, StringComparisons.ResourceName);
    }
}