File: Otlp\Model\OtlpApplication.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.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Aspire.Dashboard.Otlp.Storage;
using Google.Protobuf.Collections;
using OpenTelemetry.Proto.Common.V1;
using OpenTelemetry.Proto.Metrics.V1;
 
namespace Aspire.Dashboard.Otlp.Model;
 
[DebuggerDisplay("ApplicationName = {ApplicationName}, InstanceId = {InstanceId}")]
public class OtlpApplication
{
    public const string SERVICE_NAME = "service.name";
    public const string SERVICE_INSTANCE_ID = "service.instance.id";
    public const string PROCESS_EXECUTABLE_NAME = "process.executable.name";
 
    public string ApplicationName { get; }
    public string InstanceId { get; }
    public OtlpContext Context { get; }
 
    public ApplicationKey ApplicationKey => new ApplicationKey(ApplicationName, InstanceId);
 
    private readonly ReaderWriterLockSlim _metricsLock = new();
    private readonly Dictionary<string, OtlpMeter> _meters = new();
    private readonly Dictionary<OtlpInstrumentKey, OtlpInstrument> _instruments = new();
    private readonly ConcurrentDictionary<KeyValuePair<string, string>[], OtlpApplicationView> _applicationViews = new(ApplicationViewKeyComparer.Instance);
 
    public OtlpApplication(string name, string instanceId, OtlpContext context)
    {
        ApplicationName = name;
        InstanceId = instanceId;
        Context = context;
    }
 
    public void AddMetrics(AddContext context, RepeatedField<ScopeMetrics> scopeMetrics)
    {
        _metricsLock.EnterWriteLock();
 
        try
        {
            // Temporary attributes array to use when adding metrics to the instruments.
            KeyValuePair<string, string>[]? tempAttributes = null;
 
            foreach (var sm in scopeMetrics)
            {
                foreach (var metric in sm.Metrics)
                {
                    try
                    {
                        var instrumentKey = new OtlpInstrumentKey(sm.Scope.Name, metric.Name);
                        ref var instrument = ref CollectionsMarshal.GetValueRefOrAddDefault(_instruments, instrumentKey, out _);
                        // Adds to dictionary if not present.
                        instrument ??= new OtlpInstrument
                        {
                            Summary = new OtlpInstrumentSummary
                            {
                                Name = metric.Name,
                                Description = metric.Description,
                                Unit = metric.Unit,
                                Type = MapMetricType(metric.DataCase),
                                Parent = GetMeter(sm.Scope)
                            },
                            Context = Context
                        };
 
                        instrument.AddMetrics(metric, ref tempAttributes);
                    }
                    catch (Exception ex)
                    {
                        context.FailureCount++;
                        Context.Logger.LogInformation(ex, "Error adding metric.");
                    }
                }
            }
        }
        finally
        {
            _metricsLock.ExitWriteLock();
        }
    }
 
    private static OtlpInstrumentType MapMetricType(Metric.DataOneofCase data)
    {
        return data switch
        {
            Metric.DataOneofCase.Gauge => OtlpInstrumentType.Gauge,
            Metric.DataOneofCase.Sum => OtlpInstrumentType.Sum,
            Metric.DataOneofCase.Histogram => OtlpInstrumentType.Histogram,
            _ => OtlpInstrumentType.Unsupported
        };
    }
 
    private OtlpMeter GetMeter(InstrumentationScope scope)
    {
        ref var meter = ref CollectionsMarshal.GetValueRefOrAddDefault(_meters, scope.Name, out _);
        // Adds to dictionary if not present.
        meter ??= new OtlpMeter(scope, Context);
 
        return meter;
    }
 
    public OtlpInstrument? GetInstrument(string meterName, string instrumentName, DateTime? valuesStart, DateTime? valuesEnd)
    {
        _metricsLock.EnterReadLock();
 
        try
        {
            if (!_instruments.TryGetValue(new OtlpInstrumentKey(meterName, instrumentName), out var instrument))
            {
                return null;
            }
 
            return OtlpInstrument.Clone(instrument, cloneData: true, valuesStart: valuesStart, valuesEnd: valuesEnd);
        }
        finally
        {
            _metricsLock.ExitReadLock();
        }
    }
 
    public List<OtlpInstrumentSummary> GetInstrumentsSummary()
    {
        _metricsLock.EnterReadLock();
 
        try
        {
            var instruments = new List<OtlpInstrumentSummary>(_instruments.Count);
            foreach (var instrument in _instruments)
            {
                instruments.Add(instrument.Value.Summary);
            }
            return instruments;
        }
        finally
        {
            _metricsLock.ExitReadLock();
        }
    }
 
    public static Dictionary<string, List<OtlpApplication>> GetReplicasByApplicationName(IEnumerable<OtlpApplication> allApplications)
    {
        return allApplications
            .GroupBy(application => application.ApplicationName, StringComparers.ResourceName)
            .ToDictionary(grouping => grouping.Key, grouping => grouping.ToList());
    }
 
    public static string GetResourceName(OtlpApplicationView app, List<OtlpApplication> allApplications) =>
        GetResourceName(app.Application, allApplications);
 
    public static string GetResourceName(OtlpApplication app, List<OtlpApplication> allApplications)
    {
        var count = 0;
        foreach (var item in allApplications)
        {
            if (string.Equals(item.ApplicationName, app.ApplicationName, StringComparisons.ResourceName))
            {
                count++;
                if (count >= 2)
                {
                    var instanceId = app.InstanceId;
 
                    // Convert long GUID into a shorter, more human friendly format.
                    // Before: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
                    // After:  aaaaaaaa
                    if (Guid.TryParse(instanceId, out var guid))
                    {
                        Span<char> chars = stackalloc char[32];
                        var result = guid.TryFormat(chars, charsWritten: out _, format: "N");
                        Debug.Assert(result, "Guid.TryFormat not successful.");
 
                        instanceId = chars.Slice(0, 8).ToString();
                    }
 
                    return $"{item.ApplicationName}-{instanceId}";
                }
            }
        }
 
        return app.ApplicationName;
    }
 
    internal List<OtlpApplicationView> GetViews() => _applicationViews.Values.ToList();
 
    internal OtlpApplicationView GetView(RepeatedField<KeyValue> attributes)
    {
        // Inefficient to create this to possibly throw it away.
        var view = new OtlpApplicationView(this, attributes);
 
        if (_applicationViews.TryGetValue(view.Properties, out var applicationView))
        {
            return applicationView;
        }
 
        return _applicationViews.GetOrAdd(view.Properties, view);
    }
 
    /// <summary>
    /// Application views are equal when all properties are equal.
    /// </summary>
    private sealed class ApplicationViewKeyComparer : IEqualityComparer<KeyValuePair<string, string>[]>
    {
        public static readonly ApplicationViewKeyComparer Instance = new();
 
        public bool Equals(KeyValuePair<string, string>[]? x, KeyValuePair<string, string>[]? y)
        {
            if (x == y)
            {
                return true;
            }
            if (x == null || y == null)
            {
                return false;
            }
            if (x.Length != y.Length)
            {
                return false;
            }
 
            for (var i = 0; i < x.Length; i++)
            {
                if (!string.Equals(x[i].Key, y[i].Key, StringComparisons.OtlpAttribute))
                {
                    return false;
                }
                if (!string.Equals(x[i].Value, y[i].Value, StringComparisons.OtlpAttribute))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        public int GetHashCode([DisallowNull] KeyValuePair<string, string>[] obj)
        {
            var hashCode = new HashCode();
            for (var i = 0; i < obj.Length; i++)
            {
                hashCode.Add(StringComparers.OtlpAttribute.GetHashCode(obj[i].Key));
                hashCode.Add(StringComparers.OtlpAttribute.GetHashCode(obj[i].Value));
            }
 
            return hashCode.ToHashCode();
        }
    }
}