Skip to content

Commit d10bacf

Browse files
committed
Add exponent and logarithm mappings
Fixes #2957
1 parent cd4ccab commit d10bacf

File tree

9 files changed

+1255
-0
lines changed

9 files changed

+1255
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0-0.34b0...HEAD)
99

10+
- Add logarithm and exponent mappings
11+
([#2960](https://github.com/open-telemetry/opentelemetry-python/pull/2960))
1012
- Update explicit histogram bucket boundaries
1113
([#2947](https://github.com/open-telemetry/opentelemetry-python/pull/2947))
1214

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from abc import ABC, abstractmethod
16+
17+
18+
class Mapping(ABC):
19+
20+
# pylint: disable=no-member
21+
def __new__(cls, scale: int):
22+
23+
with cls._mappings_lock:
24+
if scale not in cls._mappings:
25+
cls._mappings[scale] = super().__new__(cls)
26+
27+
return cls._mappings[scale]
28+
29+
def __init__(self, scale: int) -> None:
30+
31+
if scale > self._max_scale:
32+
raise Exception(f"scale is larger than {self._max_scale}")
33+
34+
if scale < self._min_scale:
35+
raise Exception(f"scale is smaller than {self._min_scale}")
36+
37+
# The size of the exponential histogram buckets is determined by a
38+
# parameter known as scale, larger values of scale will produce smaller
39+
# buckets. Bucket boundaries of the exponential histogram are located
40+
# at integer powers of the base, where:
41+
42+
# base = 2 ** (2 ** (-scale))
43+
self._scale = scale
44+
45+
@property
46+
@abstractmethod
47+
def _min_scale(self) -> int:
48+
"""
49+
Return the smallest possible value for the mapping scale
50+
"""
51+
52+
@property
53+
@abstractmethod
54+
def _max_scale(self) -> int:
55+
"""
56+
Return the largest possible value for the mapping scale
57+
"""
58+
59+
@abstractmethod
60+
def map_to_index(self, value: float) -> int:
61+
"""
62+
Maps positive floating point values to indexes corresponding to
63+
`Mapping.scale`. Implementations are not expected to handle zeros,
64+
+inf, NaN, or negative values.
65+
"""
66+
67+
@abstractmethod
68+
def get_lower_boundary(self, index: int) -> float:
69+
"""
70+
Returns the lower boundary of a given bucket index. The index is
71+
expected to map onto a range that is at least partially inside the
72+
range of normalized floating point values. If the corresponding
73+
bucket's upper boundary is less than or equal to 2 ** -1022,
74+
`UnderflowError` will be raised. If the corresponding bucket's lower
75+
boundary is greater than `sys.float_info.max`, `OverflowError` will be
76+
raised.
77+
"""
78+
79+
@property
80+
@abstractmethod
81+
def scale(self) -> int:
82+
"""
83+
Returns the parameter that controls the resolution of this mapping.
84+
See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponential-scale
85+
"""
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
class MappingUnderflowError(Exception):
17+
"""
18+
Raised when computing the lower boundary of an index that maps into a
19+
denormalized floating point value.
20+
"""
21+
22+
23+
class MappingOverflowError(Exception):
24+
"""
25+
Raised when computing the lower boundary of an index that maps into +inf.
26+
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from math import ldexp
16+
from threading import Lock
17+
18+
from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import (
19+
Mapping,
20+
)
21+
from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import (
22+
MappingOverflowError,
23+
MappingUnderflowError,
24+
)
25+
from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import (
26+
MAX_NORMAL_EXPONENT,
27+
MIN_NORMAL_EXPONENT,
28+
MIN_NORMAL_VALUE,
29+
SIGNIFICAND_WIDTH,
30+
get_ieee_754_exponent,
31+
get_ieee_754_significand,
32+
)
33+
34+
35+
class ExponentMapping(Mapping):
36+
37+
_mappings = {}
38+
_mappings_lock = Lock()
39+
# _min_scale defines the point at which the exponential mapping function
40+
# becomes useless for 64-bit floats. With scale -10, ignoring subnormal
41+
# values, bucket indices range from -1 to 1.
42+
_min_scale = -10
43+
# _max_scale is the largest scale supported by exponential mapping. Use
44+
# a logarithm mapping for larger scales.
45+
_max_scale = 0
46+
47+
def __init__(self, scale: int):
48+
super().__init__(scale)
49+
50+
# self._min_normal_lower_boundary_index is the index such that
51+
# base ** index <= MIN_NORMAL_VALUE. An exponential histogram bucket
52+
# with this index covers the range (base ** index, base (index + 1)],
53+
# including MIN_NORMAL_VALUE.
54+
index = MIN_NORMAL_EXPONENT >> -self._scale
55+
56+
if -self._scale < 2:
57+
index -= 1
58+
59+
self._min_normal_lower_boundary_index = index
60+
61+
# self._max_normal_lower_boundary_index is the index such that
62+
# base**index equals the greatest representable lower boundary. An
63+
# exponential histogram bucket with this index covers the range
64+
# ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk.
65+
# metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE.
66+
# This bucket is incomplete, since the upper boundary cannot be
67+
# represented. One greater than this index corresponds with the bucket
68+
# containing values > 2 ** 1024.
69+
self._max_normal_lower_boundary_index = (
70+
MAX_NORMAL_EXPONENT >> -self._scale
71+
)
72+
73+
def map_to_index(self, value: float) -> int:
74+
if value < MIN_NORMAL_VALUE:
75+
return self._min_normal_lower_boundary_index
76+
77+
exponent = get_ieee_754_exponent(value)
78+
79+
# Positive integers are represented in binary as having an infinite
80+
# amount of leading zeroes, for example 2 is represented as ...00010.
81+
82+
# A negative integer -x is represented in binary as the complement of
83+
# (x - 1). For example, -4 is represented as the complement of 4 - 1
84+
# == 3. 3 is represented as ...00011. Its compliment is ...11100, the
85+
# binary representation of -4.
86+
87+
# get_ieee_754_significand(value) gets the positive integer made up
88+
# from the rightmost SIGNIFICAND_WIDTH bits (the mantissa) of the IEEE
89+
# 754 representation of value. If value is an exact power of 2, all
90+
# these SIGNIFICAND_WIDTH bits would be all zeroes, and when 1 is
91+
# subtracted the resulting value is -1. The binary representation of
92+
# -1 is ...111, so when these bits are right shifted SIGNIFICAND_WIDTH
93+
# places, the resulting value for correction is -1. If value is not an
94+
# exact power of 2, at least one of the rightmost SIGNIFICAND_WIDTH
95+
# bits would be 1 (even for values whose decimal part is 0, like 5.0
96+
# since the IEEE 754 of such number is too the product of a power of 2
97+
# (defined in the exponent part of the IEEE 754 representation) and the
98+
# value defined in the mantissa). Having at least one of the rightmost
99+
# SIGNIFICAND_WIDTH bit being 1 means that get_ieee_754(value) will
100+
# always be greater or equal to 1, and when 1 is subtracted, the
101+
# result will be greater or equal to 0, whose representation in binary
102+
# will be of at most SIGNIFICAND_WIDTH ones that have an infinite
103+
# amount of leading zeroes. When those SIGNIFICAND_WIDTH bits are
104+
# shifted to the right SIGNIFICAND_WIDTH places, the resulting value
105+
# will be 0.
106+
107+
# In summary, correction will be -1 if value is a power of 2, 0 if not.
108+
109+
# FIXME Document why we can assume value will not be 0, inf, or NaN.
110+
correction = (get_ieee_754_significand(value) - 1) >> SIGNIFICAND_WIDTH
111+
112+
return (exponent + correction) >> -self._scale
113+
114+
def get_lower_boundary(self, index: int) -> float:
115+
if index < self._min_normal_lower_boundary_index:
116+
raise MappingUnderflowError()
117+
118+
if index > self._max_normal_lower_boundary_index:
119+
raise MappingOverflowError()
120+
121+
return ldexp(1, index << -self._scale)
122+
123+
@property
124+
def scale(self) -> int:
125+
return self._scale

0 commit comments

Comments
 (0)