| File: Otlp\Model\MetricValues\HistogramValue.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.Globalization; using System.Text; using Aspire.Dashboard.Extensions; using OpenTelemetry.Proto.Metrics.V1; namespace Aspire.Dashboard.Otlp.Model.MetricValues; public class HistogramValue : MetricValueBase { public ulong[] Values { get; init; } public double Sum { get; init; } public double[] ExplicitBounds { get; init; } /// <summary> /// Gets the producer's aggregation start in nanoseconds since the Unix epoch, independently of the chart interval. /// </summary> public ulong AggregationStartUnixNano { get; init; } internal long AggregationId { get; init; } internal OtlpAggregationTemporality AggregationTemporality { get; init; } /// <summary> /// Initializes a histogram snapshot with its chart interval and aggregation identity. /// </summary> /// <param name="values">The counts in each histogram bucket.</param> /// <param name="sum">The sum of recorded measurements.</param> /// <param name="count">The number of recorded measurements.</param> /// <param name="startTimeUnixNano">The normalized interval start in nanoseconds since the Unix epoch.</param> /// <param name="endTimeUnixNano">The point end in nanoseconds since the Unix epoch.</param> /// <param name="explicitBounds">The explicit upper bucket boundaries.</param> /// <param name="aggregationStartUnixNano">The aggregation start reported by the producer, in nanoseconds since the Unix epoch.</param> /// <param name="aggregationId">The identity separating cumulative aggregations or delta intervals.</param> /// <param name="aggregationTemporality">The producer's aggregation temporality.</param> public HistogramValue(ulong[] values, double sum, ulong count, ulong startTimeUnixNano, ulong endTimeUnixNano, double[] explicitBounds, ulong aggregationStartUnixNano, long aggregationId, OtlpAggregationTemporality aggregationTemporality) : base(startTimeUnixNano, endTimeUnixNano) { Values = values; Sum = sum; Count = count; ExplicitBounds = explicitBounds; AggregationStartUnixNano = aggregationStartUnixNano; AggregationId = aggregationId; AggregationTemporality = aggregationTemporality; } /// <summary> /// Creates a snapshot from a validated point, extending and returning the previous instance when cumulative values are unchanged. /// </summary> internal static HistogramValue Create(HistogramDataPoint point, OtlpAggregationTemporality temporality, HistogramValue? previous) { var aggregationStart = point.StartTimeUnixNano; var end = point.TimeUnixNano; // Out-of-order delivery is not a reset. Reject before changing the aggregation // identity or extending the previous snapshot, which would move its end backwards. if (temporality != OtlpAggregationTemporality.Delta && previous is not null && point.TimeUnixNano < previous.EndTimeUnixNano) { throw new InvalidOperationException("Cumulative histogram point timestamp is earlier than the previous point."); } var sameBounds = previous is not null && HasSameBounds(previous.ExplicitBounds, point); // StartTimeUnixNano is optional. With StartTimeUnixNano = 0, use the end timestamp // for delta chart placement, but retain the original aggregation start for export. // See https://opentelemetry.io/docs/specs/otel/metrics/data-model/#timestamps. var start = temporality == OtlpAggregationTemporality.Delta && point.StartTimeUnixNano == 0 ? end : aggregationStart; // Delta ends 40 ns and 80 ns into the same tick must have distinct identities. // The unchecked cast preserves all timestamp bits in an opaque signed database key. var aggregationId = temporality == OtlpAggregationTemporality.Delta ? unchecked((long)point.TimeUnixNano) : OtlpHelpers.UnixNanoSecondsToDateTime(end).Ticks; if (temporality != OtlpAggregationTemporality.Delta && previous is not null) { // Starts 20 ns apart can identify different aggregations even when their chart timestamps share a tick. var reset = point.StartTimeUnixNano != previous.AggregationStartUnixNano || point.Count < previous.Count; if (!reset) { if (!sameBounds || previous.Values.Length != point.BucketCounts.Count) { throw new InvalidOperationException("Histogram bucket layout changed within a cumulative aggregation."); } // A producer that omits start timestamps can still reset. Detect decreasing bucket // counts before unsigned subtraction, even if the total count has already recovered. var sameCounts = true; for (var i = 0; i < point.BucketCounts.Count; i++) { var count = point.BucketCounts[i]; reset |= count < previous.Values[i]; sameCounts &= count == previous.Values[i]; } // Unchanged cumulative points extend the existing snapshot. Compare protobuf fields // before allocating a new bucket array or a snapshot that would immediately be discarded. if (!reset && sameCounts && point.Count == previous.Count && point.Sum.Equals(previous.Sum) && previous.AggregationTemporality != OtlpAggregationTemporality.Delta) { previous.EndTimeUnixNano = end; return previous; } } start = reset && aggregationStart > previous.EndTimeUnixNano ? aggregationStart : previous.EndTimeUnixNano; // Resets can share an end tick or even an exact timestamp. Advance past the persisted // epoch identity so those resets remain distinct, including after database reopening. aggregationId = reset ? Math.Max(aggregationId, checked(previous.AggregationId + 1)) : previous.AggregationId; } var bounds = previous is not null && sameBounds ? previous.ExplicitBounds : point.ExplicitBounds.ToArray(); return new HistogramValue(point.BucketCounts.ToArray(), point.Sum, point.Count, start, end, bounds, aggregationStart, aggregationId, temporality); } private static bool HasSameBounds(double[] bounds, HistogramDataPoint point) { if (bounds.Length != point.ExplicitBounds.Count) { return false; } for (var i = 0; i < bounds.Length; i++) { if (!bounds[i].Equals(point.ExplicitBounds[i])) { return false; } } return true; } public override string ToString() { var sb = new StringBuilder(); var first = true; sb.Append(CultureInfo.InvariantCulture, $"Count:{Count} Sum:{Sum} Values:"); foreach (var v in Values) { if (!first) { sb.Append(' '); } first = false; sb.Append(CultureInfo.InvariantCulture, $"{v}"); } return sb.ToString(); } internal override bool TryCompare(MetricValueBase other, out int comparisonResult) { comparisonResult = default; return false; } protected override MetricValueBase Clone() { var value = new HistogramValue(Values, Sum, Count, StartTimeUnixNano, EndTimeUnixNano, ExplicitBounds, AggregationStartUnixNano, AggregationId, AggregationTemporality); if (HasExemplars) { value.Exemplars.AddRange(Exemplars); } return value; } public override bool Equals(object? obj) { return obj is HistogramValue other && Values.Equivalent(other.Values) && Sum.Equals(other.Sum) && Count.Equals(other.Count) && StartTimeUnixNano == other.StartTimeUnixNano && AggregationStartUnixNano == other.AggregationStartUnixNano && AggregationId == other.AggregationId && AggregationTemporality == other.AggregationTemporality && ExplicitBounds.Equivalent(other.ExplicitBounds); } public override int GetHashCode() { return HashCode.Combine(StartTimeUnixNano, Count, Values, Sum, ExplicitBounds, AggregationStartUnixNano, AggregationId, AggregationTemporality); } }