diff --git a/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs b/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs new file mode 100644 index 00000000000..ca70f59f441 --- /dev/null +++ b/src/OpenTelemetry/Metrics/ExponentialBucketHistogram.cs @@ -0,0 +1,133 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NET6_0_OR_GREATER + +using System; +using System.Diagnostics; + +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + /// + /// Represents an exponential bucket histogram with base = 2 ^ (2 ^ (-scale)). + /// An exponential bucket histogram has infinite number of buckets, which are + /// identified by Bucket[i] = ( base ^ i, base ^ (i + 1) ], where i + /// is an integer. + /// + internal class ExponentialBucketHistogram + { + private static readonly double Log2E = Math.Log2(Math.E); // 1 / Math.Log(2) + + private int scale; + private double scalingFactor; // 2 ^ scale / log(2) + + public ExponentialBucketHistogram(int scale, int maxBuckets = 160) + { + Guard.ThrowIfOutOfRange(scale, min: -20, max: 20); // TODO: calculate the actual range + + this.Scale = scale; + } + + internal int Scale + { + get + { + return this.scale; + } + + private set + { + this.scale = value; + this.scalingFactor = Math.ScaleB(Log2E, value); + } + } + + internal long ZeroCount { get; private set; } + + /// + public override string ToString() + { + return nameof(ExponentialBucketHistogram) + + "{" + + nameof(this.Scale) + "=" + this.Scale + + "}"; + } + + public int MapToIndex(double value) + { + Debug.Assert(value != 0, "IEEE-754 zero values should be handled by ZeroCount."); + + // TODO: handle +Inf, -Inf, NaN + + value = Math.Abs(value); + + if (this.Scale > 0) + { + // TODO: due to precision issue, the values that are close to the bucket + // boundaries should be closely examined to avoid off-by-one. + return (int)Math.Ceiling(Math.Log(value) * this.scalingFactor) - 1; + } + else + { + var bits = BitConverter.DoubleToInt64Bits(value); + var exp = (int)((bits & IEEE754Double.EXPONENT_MASK) >> IEEE754Double.FRACTION_BITS); + var fraction = bits & IEEE754Double.FRACTION_MASK; + + if (exp == 0) + { + // TODO: benchmark and see if this should be changed to a lookup table. + fraction--; + + for (int i = IEEE754Double.FRACTION_BITS - 1; i >= 0; i--) + { + if ((fraction >> i) != 0) + { + break; + } + + exp--; + } + } + else if (fraction == 0) + { + exp--; + } + + return (exp - IEEE754Double.EXPONENT_BIAS) >> -this.Scale; + } + } + + public sealed class IEEE754Double + { +#pragma warning disable SA1310 // Field name should not contain an underscore + internal const int EXPONENT_BIAS = 1023; + internal const long EXPONENT_MASK = 0x7FF0000000000000L; + internal const int FRACTION_BITS = 52; + internal const long FRACTION_MASK = 0xFFFFFFFFFFFFFL; +#pragma warning restore SA1310 // Field name should not contain an underscore + + public static string ToString(double value) + { + var repr = Convert.ToString(BitConverter.DoubleToInt64Bits(value), 2); + return new string('0', 64 - repr.Length) + repr + ":" + "(" + value + ")"; + } + } + } +} + +#endif diff --git a/src/OpenTelemetry/Metrics/ExponentialBucketHistogramConfiguration.cs b/src/OpenTelemetry/Metrics/ExponentialBucketHistogramConfiguration.cs new file mode 100644 index 00000000000..2fcd109f4a6 --- /dev/null +++ b/src/OpenTelemetry/Metrics/ExponentialBucketHistogramConfiguration.cs @@ -0,0 +1,32 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Metrics +{ + /// + /// Stores configuration for a histogram metric stream with exponential bucket boundaries. + /// + internal class ExponentialBucketHistogramConfiguration : MetricStreamConfiguration + { + /// + /// Gets or sets the maximum number of buckets in each of the positive and negative ranges, not counting the special zero bucket. + /// + /// + /// The default value is 160. + /// + public int MaxSize { get; set; } = 160; + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/ExponentialBucketHistogramTest.cs b/test/OpenTelemetry.Tests/Metrics/ExponentialBucketHistogramTest.cs new file mode 100644 index 00000000000..ce13bfdde44 --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/ExponentialBucketHistogramTest.cs @@ -0,0 +1,114 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NET6_0_OR_GREATER + +using System; +using Xunit; + +namespace OpenTelemetry.Metrics.Tests +{ + public class ExponentialBucketHistogramTest + { + [Fact] + public void IndexLookup() + { + // An exponential bucket histogram with scale = 0. + // The base is 2 ^ (2 ^ -0) = 2. + // The buckets are: + // + // ... + // bucket[-3]: (1/8, 1/4] + // bucket[-2]: (1/4, 1/2] + // bucket[-1]: (1/2, 1] + // bucket[0]: (1, 2] + // bucket[1]: (2, 4] + // bucket[2]: (4, 8] + // bucket[3]: (8, 16] + // ... + + var histogram_scale0 = new ExponentialBucketHistogram(0); + + Assert.Equal(-1075, histogram_scale0.MapToIndex(double.Epsilon)); + + Assert.Equal(-1074, histogram_scale0.MapToIndex(double.Epsilon * 2)); + + Assert.Equal(-1073, histogram_scale0.MapToIndex(double.Epsilon * 3)); + Assert.Equal(-1073, histogram_scale0.MapToIndex(double.Epsilon * 4)); + + Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 5)); + Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 6)); + Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 7)); + Assert.Equal(-1072, histogram_scale0.MapToIndex(double.Epsilon * 8)); + + Assert.Equal(-1023, histogram_scale0.MapToIndex(2.2250738585072009E-308)); + Assert.Equal(-1023, histogram_scale0.MapToIndex(2.2250738585072014E-308)); + + Assert.Equal(-3, histogram_scale0.MapToIndex(0.25)); + + Assert.Equal(-2, histogram_scale0.MapToIndex(0.375)); + Assert.Equal(-2, histogram_scale0.MapToIndex(0.5)); + + Assert.Equal(-1, histogram_scale0.MapToIndex(0.75)); + Assert.Equal(-1, histogram_scale0.MapToIndex(1)); + + Assert.Equal(0, histogram_scale0.MapToIndex(1.5)); + Assert.Equal(0, histogram_scale0.MapToIndex(2)); + + Assert.Equal(1, histogram_scale0.MapToIndex(3)); + Assert.Equal(1, histogram_scale0.MapToIndex(4)); + + Assert.Equal(2, histogram_scale0.MapToIndex(5)); + Assert.Equal(2, histogram_scale0.MapToIndex(6)); + Assert.Equal(2, histogram_scale0.MapToIndex(7)); + Assert.Equal(2, histogram_scale0.MapToIndex(8)); + + Assert.Equal(3, histogram_scale0.MapToIndex(9)); + Assert.Equal(3, histogram_scale0.MapToIndex(16)); + + Assert.Equal(4, histogram_scale0.MapToIndex(17)); + Assert.Equal(4, histogram_scale0.MapToIndex(32)); + + // An exponential bucket histogram with scale = 1. + // The base is 2 ^ (2 ^ -1) = sqrt(2) = 1.41421356237. + // The buckets are: + // + // ... + // bucket[-3]: (0.35355339059, 1/2] + // bucket[-2]: (1/2, 0.70710678118] + // bucket[-1]: (0.70710678118, 1] + // bucket[0]: (1, 1.41421356237] + // bucket[1]: (1.41421356237, 2] + // bucket[2]: (2, 2.82842712474] + // bucket[3]: (2.82842712474, 4] + // ... + + var histogram_scale1 = new ExponentialBucketHistogram(1); + + Assert.Equal(-3, histogram_scale1.MapToIndex(0.5)); + + Assert.Equal(-2, histogram_scale1.MapToIndex(0.6)); + + Assert.Equal(-1, histogram_scale1.MapToIndex(1)); + + Assert.Equal(1, histogram_scale1.MapToIndex(2)); + + Assert.Equal(3, histogram_scale1.MapToIndex(4)); + } + } +} + +#endif