|
// 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);
}
}
}
|