File: Otlp\Model\OtlpInstrument.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;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Aspire.Dashboard.Otlp.Model.MetricValues;
using Google.Protobuf.Collections;
using OpenTelemetry.Proto.Common.V1;
using OpenTelemetry.Proto.Metrics.V1;
 
namespace Aspire.Dashboard.Otlp.Model;
 
[DebuggerDisplay("Name = {Name}, Unit = {Unit}, Type = {Type}")]
public class OtlpInstrumentSummary
{
    public required string Name { get; init; }
    public required string Description { get; init; }
    public required string Unit { get; init; }
    public required OtlpInstrumentType Type { get; init; }
    public required OtlpMeter Parent { get; init; }
 
    public OtlpInstrumentKey GetKey() => new(Parent.MeterName, Name);
}
 
public class OtlpInstrumentData
{
    public required OtlpInstrumentSummary Summary { get; init; }
    public required List<DimensionScope> Dimensions { get; init; }
    public required Dictionary<string, List<string?>> KnownAttributeValues { get; init; }
}
 
[DebuggerDisplay("Name = {Summary.Name}, Unit = {Summary.Unit}, Type = {Summary.Type}")]
public class OtlpInstrument
{
    public required OtlpInstrumentSummary Summary { get; init; }
    public required OtlpContext Context { get; init; }
 
    public Dictionary<ReadOnlyMemory<KeyValuePair<string, string>>, DimensionScope> Dimensions { get; } = new(ScopeAttributesComparer.Instance);
    public Dictionary<string, List<string?>> KnownAttributeValues { get; } = new();
 
    public void AddMetrics(Metric metric, ref KeyValuePair<string, string>[]? tempAttributes)
    {
        switch (metric.DataCase)
        {
            case Metric.DataOneofCase.Gauge:
                foreach (var d in metric.Gauge.DataPoints)
                {
                    FindScope(d.Attributes, ref tempAttributes).AddPointValue(d, Context);
                }
                break;
            case Metric.DataOneofCase.Sum:
                foreach (var d in metric.Sum.DataPoints)
                {
                    FindScope(d.Attributes, ref tempAttributes).AddPointValue(d, Context);
                }
                break;
            case Metric.DataOneofCase.Histogram:
                foreach (var d in metric.Histogram.DataPoints)
                {
                    FindScope(d.Attributes, ref tempAttributes).AddHistogramValue(d, Context);
                }
                break;
        }
    }
 
    private DimensionScope FindScope(RepeatedField<KeyValue> attributes, ref KeyValuePair<string, string>[]? tempAttributes)
    {
        // We want to find the dimension scope that matches the attributes, but we don't want to allocate.
        // Copy values to a temporary reusable array.
        //
        // A meter can have attributes. Merge these with the data point attributes when creating a dimension.
        OtlpHelpers.CopyKeyValuePairs(attributes, Summary.Parent.Attributes, Context, out var copyCount, ref tempAttributes);
        Array.Sort(tempAttributes, 0, copyCount, KeyValuePairComparer.Instance);
 
        var comparableAttributes = tempAttributes.AsMemory(0, copyCount);
 
        // Can't use CollectionsMarshal.GetValueRefOrAddDefault here because comparableAttributes is a view over mutable data.
        // Need to add dimensions using durable attributes instance after scope is created.
        if (!Dimensions.TryGetValue(comparableAttributes, out var dimension))
        {
            dimension = CreateDimensionScope(comparableAttributes);
            Dimensions.Add(dimension.Attributes, dimension);
        }
        return dimension;
    }
 
    private DimensionScope CreateDimensionScope(Memory<KeyValuePair<string, string>> comparableAttributes)
    {
        var isFirst = Dimensions.Count == 0;
        var durableAttributes = comparableAttributes.ToArray();
        var dimension = new DimensionScope(Context.Options.MaxMetricsCount, durableAttributes);
 
        var keys = KnownAttributeValues.Keys.Union(durableAttributes.Select(a => a.Key)).Distinct();
        foreach (var key in keys)
        {
            ref var values = ref CollectionsMarshal.GetValueRefOrAddDefault(KnownAttributeValues, key, out _);
            // Adds to dictionary if not present.
            if (values == null)
            {
                values = new List<string?>();
 
                // If the key is new and there are already dimensions, add an empty value because there are dimensions without this key.
                if (!isFirst)
                {
                    TryAddValue(values, null);
                }
            }
 
            var currentDimensionValue = OtlpHelpers.GetValue(durableAttributes, key);
            TryAddValue(values, currentDimensionValue);
        }
 
        return dimension;
 
        static void TryAddValue(List<string?> values, string? value)
        {
            if (!values.Contains(value))
            {
                values.Add(value);
            }
        }
    }
 
    public static OtlpInstrument Clone(OtlpInstrument instrument, bool cloneData, DateTime? valuesStart, DateTime? valuesEnd)
    {
        var newInstrument = new OtlpInstrument
        {
            Summary = instrument.Summary,
            Context = instrument.Context
        };
 
        if (cloneData)
        {
            foreach (var item in instrument.KnownAttributeValues)
            {
                newInstrument.KnownAttributeValues.Add(item.Key, item.Value.ToList());
            }
            foreach (var item in instrument.Dimensions)
            {
                newInstrument.Dimensions.Add(item.Key, DimensionScope.Clone(item.Value, valuesStart, valuesEnd));
            }
        }
 
        return newInstrument;
    }
 
    private sealed class ScopeAttributesComparer : IEqualityComparer<ReadOnlyMemory<KeyValuePair<string, string>>>
    {
        public static readonly ScopeAttributesComparer Instance = new();
 
        public bool Equals(ReadOnlyMemory<KeyValuePair<string, string>> x, ReadOnlyMemory<KeyValuePair<string, string>> y)
        {
            return x.Span.SequenceEqual(y.Span);
        }
 
        public int GetHashCode([DisallowNull] ReadOnlyMemory<KeyValuePair<string, string>> obj)
        {
            var hashcode = new HashCode();
            foreach (KeyValuePair<string, string> pair in obj.Span)
            {
                hashcode.Add(pair.Key);
                hashcode.Add(pair.Value);
            }
            return hashcode.ToHashCode();
        }
    }
 
    private sealed class KeyValuePairComparer : IComparer<KeyValuePair<string, string>>
    {
        public static readonly KeyValuePairComparer Instance = new();
 
        public int Compare(KeyValuePair<string, string> x, KeyValuePair<string, string> y)
        {
            return string.Compare(x.Key, y.Key, StringComparison.Ordinal);
        }
    }
}