Skip to content

Commit 30e4054

Browse files
authored
Add exponential histogram tests and in-memory exporter support (#4303)
1 parent 5aad3e0 commit 30e4054

10 files changed

+258
-37
lines changed

src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogram.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,15 @@ internal ExponentialHistogramData GetExponentialHistogramData()
235235
{
236236
return this.SnapshotExponentialHistogramData;
237237
}
238+
239+
internal Base2ExponentialBucketHistogram Copy()
240+
{
241+
Debug.Assert(this.PositiveBuckets.Capacity == this.NegativeBuckets.Capacity, "Capacity of positive and negative buckets are not equal.");
242+
var copy = new Base2ExponentialBucketHistogram(this.PositiveBuckets.Capacity, this.SnapshotExponentialHistogramData.Scale);
243+
copy.SnapshotSum = this.SnapshotSum;
244+
copy.SnapshotMin = this.SnapshotMin;
245+
copy.SnapshotMax = this.SnapshotMax;
246+
copy.SnapshotExponentialHistogramData = this.SnapshotExponentialHistogramData.Copy();
247+
return copy;
248+
}
238249
}

src/OpenTelemetry/Metrics/Base2ExponentialBucketHistogramConfiguration.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,31 @@ namespace OpenTelemetry.Metrics;
1919
/// <summary>
2020
/// Stores configuration for a histogram metric stream with base-2 exponential bucket boundaries.
2121
/// </summary>
22-
internal sealed class Base2ExponentialBucketHistogramConfiguration : MetricStreamConfiguration
22+
internal sealed class Base2ExponentialBucketHistogramConfiguration : HistogramConfiguration
2323
{
24+
private int maxSize = 160;
25+
2426
/// <summary>
2527
/// Gets or sets the maximum number of buckets in each of the positive and negative ranges, not counting the special zero bucket.
2628
/// </summary>
2729
/// <remarks>
28-
/// The default value is 160.
30+
/// The default value is 160. The minimum size is 2.
2931
/// </remarks>
30-
public int MaxSize { get; set; } = 160;
32+
public int MaxSize
33+
{
34+
get
35+
{
36+
return this.maxSize;
37+
}
38+
39+
set
40+
{
41+
if (value < 2)
42+
{
43+
throw new ArgumentException($"Histogram max size is invalid. Minimum size is 2.", nameof(value));
44+
}
45+
46+
this.maxSize = value;
47+
}
48+
}
3149
}

src/OpenTelemetry/Metrics/ExponentialHistogramBuckets.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ namespace OpenTelemetry.Metrics;
2020

2121
public sealed class ExponentialHistogramBuckets
2222
{
23-
private int size;
2423
private long[] buckets = Array.Empty<long>();
2524

2625
internal ExponentialHistogramBuckets()
@@ -29,7 +28,7 @@ internal ExponentialHistogramBuckets()
2928

3029
public int Offset { get; private set; }
3130

32-
public Enumerator GetEnumerator() => new(this.size, this.buckets);
31+
public Enumerator GetEnumerator() => new(this.buckets);
3332

3433
internal void SnapshotBuckets(CircularBufferBuckets buckets)
3534
{
@@ -39,24 +38,30 @@ internal void SnapshotBuckets(CircularBufferBuckets buckets)
3938
}
4039

4140
this.Offset = buckets.Offset;
42-
this.size = buckets.Size;
4341
buckets.Copy(this.buckets);
4442
}
4543

44+
internal ExponentialHistogramBuckets Copy()
45+
{
46+
var copy = new ExponentialHistogramBuckets();
47+
copy.Offset = this.Offset;
48+
copy.buckets = new long[this.buckets.Length];
49+
Array.Copy(this.buckets, copy.buckets, this.buckets.Length);
50+
return copy;
51+
}
52+
4653
/// <summary>
4754
/// Enumerates the bucket counts of an exponential histogram.
4855
/// </summary>
4956
// Note: Does not implement IEnumerator<> to prevent accidental boxing.
5057
public struct Enumerator
5158
{
52-
private readonly int size;
5359
private readonly long[] buckets;
5460
private int index;
5561

56-
internal Enumerator(int size, long[] buckets)
62+
internal Enumerator(long[] buckets)
5763
{
58-
this.index = size;
59-
this.size = size;
64+
this.index = 0;
6065
this.buckets = buckets;
6166
this.Current = default;
6267
}
@@ -76,7 +81,7 @@ internal Enumerator(int size, long[] buckets)
7681
/// collection.</returns>
7782
public bool MoveNext()
7883
{
79-
if (this.index < this.size)
84+
if (this.index < this.buckets.Length)
8085
{
8186
this.Current = this.buckets[this.index++];
8287
return true;

src/OpenTelemetry/Metrics/ExponentialHistogramData.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,17 @@ internal ExponentialHistogramData()
3030

3131
public long ZeroCount { get; internal set; }
3232

33-
public ExponentialHistogramBuckets PositiveBuckets { get; }
33+
public ExponentialHistogramBuckets PositiveBuckets { get; private set; }
3434

35-
public ExponentialHistogramBuckets NegativeBuckets { get; }
35+
public ExponentialHistogramBuckets NegativeBuckets { get; private set; }
36+
37+
internal ExponentialHistogramData Copy()
38+
{
39+
var copy = new ExponentialHistogramData();
40+
copy.Scale = this.Scale;
41+
copy.ZeroCount = this.ZeroCount;
42+
copy.PositiveBuckets = this.PositiveBuckets.Copy();
43+
copy.NegativeBuckets = this.NegativeBuckets.Copy();
44+
return copy;
45+
}
3646
}

src/OpenTelemetry/Metrics/MeterProviderSdk.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ internal MeterProviderSdk(
193193
metricStreamConfig.ViewId = i;
194194
}
195195

196-
if (metricStreamConfig is ExplicitBucketHistogramConfiguration
196+
if (metricStreamConfig is HistogramConfiguration
197197
&& instrument.GetType().GetGenericTypeDefinition() != typeof(Histogram<>))
198198
{
199199
metricStreamConfig = null;

src/OpenTelemetry/Metrics/MetricPointOptionalComponents.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ internal sealed class MetricPointOptionalComponents
3838
internal MetricPointOptionalComponents Copy()
3939
{
4040
MetricPointOptionalComponents copy = new MetricPointOptionalComponents();
41-
copy.HistogramBuckets = this.HistogramBuckets.Copy();
41+
copy.HistogramBuckets = this.HistogramBuckets?.Copy();
42+
copy.Base2ExponentialBucketHistogram = this.Base2ExponentialBucketHistogram?.Copy();
4243
if (this.Exemplars != null)
4344
{
4445
Array.Copy(this.Exemplars, copy.Exemplars, this.Exemplars.Length);
4546
}
4647

47-
// TODO: Copy Base2ExponentialBucketHistogram
4848
return copy;
4949
}
5050
}

test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,45 @@ public void HistogramWithOnlySumCount()
188188
Assert.False(enumerator.MoveNext());
189189
}
190190

191+
internal static void AssertExponentialBucketsAreCorrect(Base2ExponentialBucketHistogram expectedHistogram, ExponentialHistogramData data)
192+
{
193+
Assert.Equal(expectedHistogram.Scale, data.Scale);
194+
Assert.Equal(expectedHistogram.ZeroCount, data.ZeroCount);
195+
Assert.Equal(expectedHistogram.PositiveBuckets.Offset, data.PositiveBuckets.Offset);
196+
Assert.Equal(expectedHistogram.NegativeBuckets.Offset, data.NegativeBuckets.Offset);
197+
198+
expectedHistogram.Snapshot();
199+
var expectedData = expectedHistogram.GetExponentialHistogramData();
200+
201+
var actual = new List<long>();
202+
foreach (var bucketCount in data.PositiveBuckets)
203+
{
204+
actual.Add(bucketCount);
205+
}
206+
207+
var expected = new List<long>();
208+
foreach (var bucketCount in expectedData.PositiveBuckets)
209+
{
210+
expected.Add(bucketCount);
211+
}
212+
213+
Assert.Equal(expected, actual);
214+
215+
actual = new List<long>();
216+
foreach (var bucketCount in data.NegativeBuckets)
217+
{
218+
actual.Add(bucketCount);
219+
}
220+
221+
expected = new List<long>();
222+
foreach (var bucketCount in expectedData.NegativeBuckets)
223+
{
224+
expected.Add(bucketCount);
225+
}
226+
227+
Assert.Equal(expected, actual);
228+
}
229+
191230
[Theory]
192231
[InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Cumulative)]
193232
[InlineData(AggregationType.Base2ExponentialHistogram, AggregationTemporality.Delta)]
@@ -284,25 +323,5 @@ internal void ExponentialHistogramTests(AggregationType aggregationType, Aggrega
284323
}
285324
}
286325
}
287-
288-
private static void AssertExponentialBucketsAreCorrect(Base2ExponentialBucketHistogram expectedHistogram, ExponentialHistogramData data)
289-
{
290-
Assert.Equal(expectedHistogram.Scale, data.Scale);
291-
Assert.Equal(expectedHistogram.ZeroCount, data.ZeroCount);
292-
Assert.Equal(expectedHistogram.PositiveBuckets.Offset, data.PositiveBuckets.Offset);
293-
Assert.Equal(expectedHistogram.NegativeBuckets.Offset, data.NegativeBuckets.Offset);
294-
295-
var index = expectedHistogram.PositiveBuckets.Offset;
296-
foreach (var bucketCount in data.PositiveBuckets)
297-
{
298-
Assert.Equal(expectedHistogram.PositiveBuckets[index++], bucketCount);
299-
}
300-
301-
index = expectedHistogram.NegativeBuckets.Offset;
302-
foreach (var bucketCount in data.NegativeBuckets)
303-
{
304-
Assert.Equal(expectedHistogram.PositiveBuckets[index++], bucketCount);
305-
}
306-
}
307326
}
308327
}

test/OpenTelemetry.Tests/Metrics/MetricSnapshotTests.cs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,5 +163,104 @@ public void VerifySnapshot_Histogram()
163163
Assert.Equal(2, snapshot2.MetricPoints[0].GetHistogramCount());
164164
Assert.Equal(15, snapshot2.MetricPoints[0].GetHistogramSum());
165165
}
166+
167+
[Fact]
168+
public void VerifySnapshot_ExponentialHistogram()
169+
{
170+
var expectedHistogram = new Base2ExponentialBucketHistogram();
171+
var exportedMetrics = new List<Metric>();
172+
var exportedSnapshots = new List<MetricSnapshot>();
173+
174+
using var meter = new Meter(Utils.GetCurrentMethodName());
175+
var histogram = meter.CreateHistogram<int>("histogram");
176+
using var meterProvider = Sdk.CreateMeterProviderBuilder()
177+
.AddMeter(meter.Name)
178+
.AddView("histogram", new Base2ExponentialBucketHistogramConfiguration())
179+
.AddInMemoryExporter(exportedMetrics)
180+
.AddInMemoryExporter(exportedSnapshots)
181+
.Build();
182+
183+
// FIRST EXPORT
184+
expectedHistogram.Record(10);
185+
histogram.Record(10);
186+
meterProvider.ForceFlush();
187+
188+
// Verify Metric 1
189+
Assert.Single(exportedMetrics);
190+
var metric1 = exportedMetrics[0];
191+
var metricPoints1Enumerator = metric1.GetMetricPoints().GetEnumerator();
192+
Assert.True(metricPoints1Enumerator.MoveNext());
193+
ref readonly var metricPoint1 = ref metricPoints1Enumerator.Current;
194+
Assert.Equal(1, metricPoint1.GetHistogramCount());
195+
Assert.Equal(10, metricPoint1.GetHistogramSum());
196+
metricPoint1.TryGetHistogramMinMaxValues(out var min, out var max);
197+
Assert.Equal(10, min);
198+
Assert.Equal(10, max);
199+
AggregatorTest.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint1.GetExponentialHistogramData());
200+
201+
// Verify Snapshot 1
202+
Assert.Single(exportedSnapshots);
203+
var snapshot1 = exportedSnapshots[0];
204+
Assert.Single(snapshot1.MetricPoints);
205+
Assert.Equal(1, snapshot1.MetricPoints[0].GetHistogramCount());
206+
Assert.Equal(10, snapshot1.MetricPoints[0].GetHistogramSum());
207+
snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max);
208+
Assert.Equal(10, min);
209+
Assert.Equal(10, max);
210+
AggregatorTest.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot1.MetricPoints[0].GetExponentialHistogramData());
211+
212+
// Verify Metric == Snapshot
213+
Assert.Equal(metric1.Name, snapshot1.Name);
214+
Assert.Equal(metric1.Description, snapshot1.Description);
215+
Assert.Equal(metric1.Unit, snapshot1.Unit);
216+
Assert.Equal(metric1.MeterName, snapshot1.MeterName);
217+
Assert.Equal(metric1.MetricType, snapshot1.MetricType);
218+
Assert.Equal(metric1.MeterVersion, snapshot1.MeterVersion);
219+
220+
// SECOND EXPORT
221+
expectedHistogram.Record(5);
222+
histogram.Record(5);
223+
meterProvider.ForceFlush();
224+
225+
// Verify Metric 1 after second export
226+
// This value is expected to be updated.
227+
Assert.Equal(2, metricPoint1.GetHistogramCount());
228+
Assert.Equal(15, metricPoint1.GetHistogramSum());
229+
metricPoint1.TryGetHistogramMinMaxValues(out min, out max);
230+
Assert.Equal(5, min);
231+
Assert.Equal(10, max);
232+
233+
// Verify Metric 2
234+
Assert.Equal(2, exportedMetrics.Count);
235+
var metric2 = exportedMetrics[1];
236+
var metricPoints2Enumerator = metric2.GetMetricPoints().GetEnumerator();
237+
Assert.True(metricPoints2Enumerator.MoveNext());
238+
ref readonly var metricPoint2 = ref metricPoints2Enumerator.Current;
239+
Assert.Equal(2, metricPoint2.GetHistogramCount());
240+
Assert.Equal(15, metricPoint2.GetHistogramSum());
241+
metricPoint1.TryGetHistogramMinMaxValues(out min, out max);
242+
Assert.Equal(5, min);
243+
Assert.Equal(10, max);
244+
AggregatorTest.AssertExponentialBucketsAreCorrect(expectedHistogram, metricPoint2.GetExponentialHistogramData());
245+
246+
// Verify Snapshot 1 after second export
247+
// This value is expected to be unchanged.
248+
Assert.Equal(1, snapshot1.MetricPoints[0].GetHistogramCount());
249+
Assert.Equal(10, snapshot1.MetricPoints[0].GetHistogramSum());
250+
snapshot1.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max);
251+
Assert.Equal(10, min);
252+
Assert.Equal(10, max);
253+
254+
// Verify Snapshot 2
255+
Assert.Equal(2, exportedSnapshots.Count);
256+
var snapshot2 = exportedSnapshots[1];
257+
Assert.Single(snapshot2.MetricPoints);
258+
Assert.Equal(2, snapshot2.MetricPoints[0].GetHistogramCount());
259+
Assert.Equal(15, snapshot2.MetricPoints[0].GetHistogramSum());
260+
snapshot2.MetricPoints[0].TryGetHistogramMinMaxValues(out min, out max);
261+
Assert.Equal(5, min);
262+
Assert.Equal(10, max);
263+
AggregatorTest.AssertExponentialBucketsAreCorrect(expectedHistogram, snapshot2.MetricPoints[0].GetExponentialHistogramData());
264+
}
166265
}
167266
}

test/OpenTelemetry.Tests/Metrics/MetricTestData.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,15 @@ public static IEnumerable<object[]> ValidHistogramMinMax
5858
new object[] { new double[] { double.NegativeInfinity, 0, double.PositiveInfinity }, new HistogramConfiguration(), double.NegativeInfinity, double.PositiveInfinity },
5959
new object[] { new double[] { 1 }, new HistogramConfiguration(), 1, 1 },
6060
new object[] { new double[] { 5, 100, 4, 101, -2, 97 }, new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 } }, -2, 101 },
61+
new object[] { new double[] { 5, 100, 4, 101, -2, 97 }, new Base2ExponentialBucketHistogramConfiguration(), -2, 101 },
6162
};
6263

6364
public static IEnumerable<object[]> InvalidHistogramMinMax
6465
=> new List<object[]>
6566
{
6667
new object[] { new double[] { 1 }, new HistogramConfiguration() { RecordMinMax = false } },
6768
new object[] { new double[] { 1 }, new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 }, RecordMinMax = false } },
69+
new object[] { new double[] { 1 }, new Base2ExponentialBucketHistogramConfiguration() { RecordMinMax = false } },
6870
};
6971
}
7072
}

0 commit comments

Comments
 (0)