Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7d51d20

Browse files
ocelotlaabmass
andauthored
Add exponent and logarithm mappings (open-telemetry#2960)
* Add exponent and logarithm mappings Fixes open-telemetry#2957 * Fix comments * Remove example function * Fix lint and spelling * Add link to spec * Fix documentation to reference the exceptions * Refactor min and max scale * Set self._scale in parent only * Add explanation for IEEE 754 * Use mantissa consistently * Refactor lock definition * Fix wrong fixed value * Fix lint * Fix test name * Update opentelemetry-sdk/tests/metrics/exponential_histogram/test_exponent_mapping.py Co-authored-by: Aaron Abbott <[email protected]> * Fix operator separator * Rename boundary functions * Add links to reference implementation * Fix lint and spelling * Revert "Refactor lock definition" This reverts commit 064bb2b. * Refactor initialization * Fix math format * Rename to normal and denormal * Remove errors from public interface Co-authored-by: Aaron Abbott <[email protected]>
1 parent fa19e1f commit 7d51d20

File tree

9 files changed

+1334
-0
lines changed

9 files changed

+1334
-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...HEAD)
99

10+
- Add logarithm and exponent mappings
11+
([#2960](https://github.com/open-telemetry/opentelemetry-python/pull/2960))
1012
- Add and use missing metrics environment variables
1113
([#2968](https://github.com/open-telemetry/opentelemetry-python/pull/2968))
1214
- Enabled custom samplers via entry points
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
Parent class for `LogarithmMapping` and `ExponentialMapping`.
21+
"""
22+
23+
# pylint: disable=no-member
24+
def __new__(cls, scale: int):
25+
26+
with cls._mappings_lock:
27+
# cls._mappings and cls._mappings_lock are implemented in each of
28+
# the child classes as a dictionary and a lock, respectively. They
29+
# are not instantiated here because that would lead to both child
30+
# classes having the same instance of cls._mappings and
31+
# cls._mappings_lock.
32+
if scale not in cls._mappings:
33+
cls._mappings[scale] = super().__new__(cls)
34+
cls._mappings[scale]._init(scale)
35+
36+
return cls._mappings[scale]
37+
38+
@abstractmethod
39+
def _init(self, scale: int) -> None:
40+
# pylint: disable=attribute-defined-outside-init
41+
42+
if scale > self._get_max_scale():
43+
raise Exception(f"scale is larger than {self._max_scale}")
44+
45+
if scale < self._get_min_scale():
46+
raise Exception(f"scale is smaller than {self._min_scale}")
47+
48+
# The size of the exponential histogram buckets is determined by a
49+
# parameter known as scale, larger values of scale will produce smaller
50+
# buckets. Bucket boundaries of the exponential histogram are located
51+
# at integer powers of the base, where:
52+
#
53+
# base = 2 ** (2 ** (-scale))
54+
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#all-scales-use-the-logarithm-function
55+
self._scale = scale
56+
57+
@abstractmethod
58+
def _get_min_scale(self) -> int:
59+
"""
60+
Return the smallest possible value for the mapping scale
61+
"""
62+
63+
@abstractmethod
64+
def _get_max_scale(self) -> int:
65+
"""
66+
Return the largest possible value for the mapping scale
67+
"""
68+
69+
@abstractmethod
70+
def map_to_index(self, value: float) -> int:
71+
"""
72+
Maps positive floating point values to indexes corresponding to
73+
`Mapping.scale`. Implementations are not expected to handle zeros,
74+
+inf, NaN, or negative values.
75+
"""
76+
77+
@abstractmethod
78+
def get_lower_boundary(self, index: int) -> float:
79+
"""
80+
Returns the lower boundary of a given bucket index. The index is
81+
expected to map onto a range that is at least partially inside the
82+
range of normal floating point values. If the corresponding
83+
bucket's upper boundary is less than or equal to 2 ** -1022,
84+
:class:`~opentelemetry.sdk.metrics.MappingUnderflowError`
85+
will be raised. If the corresponding bucket's lower boundary is greater
86+
than ``sys.float_info.max``,
87+
:class:`~opentelemetry.sdk.metrics.MappingOverflowError`
88+
will be raised.
89+
"""
90+
91+
@property
92+
def scale(self) -> int:
93+
"""
94+
Returns the parameter that controls the resolution of this mapping.
95+
See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponential-scale
96+
"""
97+
return self._scale
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+
denormal 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,141 @@
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+
MANTISSA_WIDTH,
27+
MAX_NORMAL_EXPONENT,
28+
MIN_NORMAL_EXPONENT,
29+
MIN_NORMAL_VALUE,
30+
get_ieee_754_exponent,
31+
get_ieee_754_mantissa,
32+
)
33+
34+
35+
class ExponentMapping(Mapping):
36+
# Reference implementation here:
37+
# https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/exponent/exponent.go
38+
39+
_mappings = {}
40+
_mappings_lock = Lock()
41+
42+
_min_scale = -10
43+
_max_scale = 0
44+
45+
def _get_min_scale(self):
46+
# _min_scale defines the point at which the exponential mapping
47+
# function becomes useless for 64-bit floats. With scale -10, ignoring
48+
# subnormal values, bucket indices range from -1 to 1.
49+
return -10
50+
51+
def _get_max_scale(self):
52+
# _max_scale is the largest scale supported by exponential mapping. Use
53+
# a logarithm mapping for larger scales.
54+
return 0
55+
56+
def _init(self, scale: int):
57+
# pylint: disable=attribute-defined-outside-init
58+
59+
super()._init(scale)
60+
61+
# self._min_normal_lower_boundary_index is the largest index such that
62+
# base ** index < MIN_NORMAL_VALUE and
63+
# base ** (index + 1) >= MIN_NORMAL_VALUE. An exponential histogram
64+
# bucket with this index covers the range
65+
# (base ** index, base (index + 1)], including MIN_NORMAL_VALUE. This
66+
# is the smallest valid index that contains at least one normal value.
67+
index = MIN_NORMAL_EXPONENT >> -self._scale
68+
69+
if -self._scale < 2:
70+
# For scales -1 and 0, the maximum value 2 ** -1022 is a
71+
# power-of-two multiple, meaning base ** index == MIN_NORMAL_VALUE.
72+
# Subtracting 1 so that base ** (index + 1) == MIN_NORMAL_VALUE.
73+
index -= 1
74+
75+
self._min_normal_lower_boundary_index = index
76+
77+
# self._max_normal_lower_boundary_index is the index such that
78+
# base**index equals the greatest representable lower boundary. An
79+
# exponential histogram bucket with this index covers the range
80+
# ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk.
81+
# metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE.
82+
# This bucket is incomplete, since the upper boundary cannot be
83+
# represented. One greater than this index corresponds with the bucket
84+
# containing values > 2 ** 1024.
85+
self._max_normal_lower_boundary_index = (
86+
MAX_NORMAL_EXPONENT >> -self._scale
87+
)
88+
89+
def map_to_index(self, value: float) -> int:
90+
if value < MIN_NORMAL_VALUE:
91+
return self._min_normal_lower_boundary_index
92+
93+
exponent = get_ieee_754_exponent(value)
94+
95+
# Positive integers are represented in binary as having an infinite
96+
# amount of leading zeroes, for example 2 is represented as ...00010.
97+
98+
# A negative integer -x is represented in binary as the complement of
99+
# (x - 1). For example, -4 is represented as the complement of 4 - 1
100+
# == 3. 3 is represented as ...00011. Its compliment is ...11100, the
101+
# binary representation of -4.
102+
103+
# get_ieee_754_mantissa(value) gets the positive integer made up
104+
# from the rightmost MANTISSA_WIDTH bits (the mantissa) of the IEEE
105+
# 754 representation of value. If value is an exact power of 2, all
106+
# these MANTISSA_WIDTH bits would be all zeroes, and when 1 is
107+
# subtracted the resulting value is -1. The binary representation of
108+
# -1 is ...111, so when these bits are right shifted MANTISSA_WIDTH
109+
# places, the resulting value for correction is -1. If value is not an
110+
# exact power of 2, at least one of the rightmost MANTISSA_WIDTH
111+
# bits would be 1 (even for values whose decimal part is 0, like 5.0
112+
# since the IEEE 754 of such number is too the product of a power of 2
113+
# (defined in the exponent part of the IEEE 754 representation) and the
114+
# value defined in the mantissa). Having at least one of the rightmost
115+
# MANTISSA_WIDTH bit being 1 means that get_ieee_754(value) will
116+
# always be greater or equal to 1, and when 1 is subtracted, the
117+
# result will be greater or equal to 0, whose representation in binary
118+
# will be of at most MANTISSA_WIDTH ones that have an infinite
119+
# amount of leading zeroes. When those MANTISSA_WIDTH bits are
120+
# shifted to the right MANTISSA_WIDTH places, the resulting value
121+
# will be 0.
122+
123+
# In summary, correction will be -1 if value is a power of 2, 0 if not.
124+
125+
# FIXME Document why we can assume value will not be 0, inf, or NaN.
126+
correction = (get_ieee_754_mantissa(value) - 1) >> MANTISSA_WIDTH
127+
128+
return (exponent + correction) >> -self._scale
129+
130+
def get_lower_boundary(self, index: int) -> float:
131+
if index < self._min_normal_lower_boundary_index:
132+
raise MappingUnderflowError()
133+
134+
if index > self._max_normal_lower_boundary_index:
135+
raise MappingOverflowError()
136+
137+
return ldexp(1, index << -self._scale)
138+
139+
@property
140+
def scale(self) -> int:
141+
return self._scale

0 commit comments

Comments
 (0)