-
Notifications
You must be signed in to change notification settings - Fork 648
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes #2957
- Loading branch information
Showing
9 changed files
with
1,255 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
85 changes: 85 additions & 0 deletions
85
...try-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# 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. | ||
|
||
from abc import ABC, abstractmethod | ||
|
||
|
||
class Mapping(ABC): | ||
|
||
# pylint: disable=no-member | ||
def __new__(cls, scale: int): | ||
|
||
with cls._mappings_lock: | ||
if scale not in cls._mappings: | ||
cls._mappings[scale] = super().__new__(cls) | ||
|
||
return cls._mappings[scale] | ||
|
||
def __init__(self, scale: int) -> None: | ||
|
||
if scale > self._max_scale: | ||
raise Exception(f"scale is larger than {self._max_scale}") | ||
|
||
if scale < self._min_scale: | ||
raise Exception(f"scale is smaller than {self._min_scale}") | ||
|
||
# The size of the exponential histogram buckets is determined by a | ||
# parameter known as scale, larger values of scale will produce smaller | ||
# buckets. Bucket boundaries of the exponential histogram are located | ||
# at integer powers of the base, where: | ||
|
||
# base = 2 ** (2 ** (-scale)) | ||
self._scale = scale | ||
|
||
@property | ||
@abstractmethod | ||
def _min_scale(self) -> int: | ||
""" | ||
Return the smallest possible value for the mapping scale | ||
""" | ||
|
||
@property | ||
@abstractmethod | ||
def _max_scale(self) -> int: | ||
""" | ||
Return the largest possible value for the mapping scale | ||
""" | ||
|
||
@abstractmethod | ||
def map_to_index(self, value: float) -> int: | ||
""" | ||
Maps positive floating point values to indexes corresponding to | ||
`Mapping.scale`. Implementations are not expected to handle zeros, | ||
+inf, NaN, or negative values. | ||
""" | ||
|
||
@abstractmethod | ||
def get_lower_boundary(self, index: int) -> float: | ||
""" | ||
Returns the lower boundary of a given bucket index. The index is | ||
expected to map onto a range that is at least partially inside the | ||
range of normalized floating point values. If the corresponding | ||
bucket's upper boundary is less than or equal to 2 ** -1022, | ||
`UnderflowError` will be raised. If the corresponding bucket's lower | ||
boundary is greater than `sys.float_info.max`, `OverflowError` will be | ||
raised. | ||
""" | ||
|
||
@property | ||
@abstractmethod | ||
def scale(self) -> int: | ||
""" | ||
Returns the parameter that controls the resolution of this mapping. | ||
See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponential-scale | ||
""" |
26 changes: 26 additions & 0 deletions
26
...metry-sdk/src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/errors.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# 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. | ||
|
||
|
||
class MappingUnderflowError(Exception): | ||
""" | ||
Raised when computing the lower boundary of an index that maps into a | ||
denormalized floating point value. | ||
""" | ||
|
||
|
||
class MappingOverflowError(Exception): | ||
""" | ||
Raised when computing the lower boundary of an index that maps into +inf. | ||
""" |
125 changes: 125 additions & 0 deletions
125
...src/opentelemetry/sdk/metrics/_internal/exponential_histogram/mapping/exponent_mapping.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
# 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. | ||
|
||
from math import ldexp | ||
from threading import Lock | ||
|
||
from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping import ( | ||
Mapping, | ||
) | ||
from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.errors import ( | ||
MappingOverflowError, | ||
MappingUnderflowError, | ||
) | ||
from opentelemetry.sdk.metrics._internal.exponential_histogram.mapping.ieee_754 import ( | ||
MAX_NORMAL_EXPONENT, | ||
MIN_NORMAL_EXPONENT, | ||
MIN_NORMAL_VALUE, | ||
SIGNIFICAND_WIDTH, | ||
get_ieee_754_exponent, | ||
get_ieee_754_significand, | ||
) | ||
|
||
|
||
class ExponentMapping(Mapping): | ||
|
||
_mappings = {} | ||
_mappings_lock = Lock() | ||
# _min_scale defines the point at which the exponential mapping function | ||
# becomes useless for 64-bit floats. With scale -10, ignoring subnormal | ||
# values, bucket indices range from -1 to 1. | ||
_min_scale = -10 | ||
# _max_scale is the largest scale supported by exponential mapping. Use | ||
# a logarithm mapping for larger scales. | ||
_max_scale = 0 | ||
|
||
def __init__(self, scale: int): | ||
super().__init__(scale) | ||
|
||
# self._min_normal_lower_boundary_index is the index such that | ||
# base ** index <= MIN_NORMAL_VALUE. An exponential histogram bucket | ||
# with this index covers the range (base ** index, base (index + 1)], | ||
# including MIN_NORMAL_VALUE. | ||
index = MIN_NORMAL_EXPONENT >> -self._scale | ||
|
||
if -self._scale < 2: | ||
index -= 1 | ||
|
||
self._min_normal_lower_boundary_index = index | ||
|
||
# self._max_normal_lower_boundary_index is the index such that | ||
# base**index equals the greatest representable lower boundary. An | ||
# exponential histogram bucket with this index covers the range | ||
# ((2 ** 1024) / base, 2 ** 1024], which includes opentelemetry.sdk. | ||
# metrics._internal.exponential_histogram.ieee_754.MAX_NORMAL_VALUE. | ||
# This bucket is incomplete, since the upper boundary cannot be | ||
# represented. One greater than this index corresponds with the bucket | ||
# containing values > 2 ** 1024. | ||
self._max_normal_lower_boundary_index = ( | ||
MAX_NORMAL_EXPONENT >> -self._scale | ||
) | ||
|
||
def map_to_index(self, value: float) -> int: | ||
if value < MIN_NORMAL_VALUE: | ||
return self._min_normal_lower_boundary_index | ||
|
||
exponent = get_ieee_754_exponent(value) | ||
|
||
# Positive integers are represented in binary as having an infinite | ||
# amount of leading zeroes, for example 2 is represented as ...00010. | ||
|
||
# A negative integer -x is represented in binary as the complement of | ||
# (x - 1). For example, -4 is represented as the complement of 4 - 1 | ||
# == 3. 3 is represented as ...00011. Its compliment is ...11100, the | ||
# binary representation of -4. | ||
|
||
# get_ieee_754_significand(value) gets the positive integer made up | ||
# from the rightmost SIGNIFICAND_WIDTH bits (the mantissa) of the IEEE | ||
# 754 representation of value. If value is an exact power of 2, all | ||
# these SIGNIFICAND_WIDTH bits would be all zeroes, and when 1 is | ||
# subtracted the resulting value is -1. The binary representation of | ||
# -1 is ...111, so when these bits are right shifted SIGNIFICAND_WIDTH | ||
# places, the resulting value for correction is -1. If value is not an | ||
# exact power of 2, at least one of the rightmost SIGNIFICAND_WIDTH | ||
# bits would be 1 (even for values whose decimal part is 0, like 5.0 | ||
# since the IEEE 754 of such number is too the product of a power of 2 | ||
# (defined in the exponent part of the IEEE 754 representation) and the | ||
# value defined in the mantissa). Having at least one of the rightmost | ||
# SIGNIFICAND_WIDTH bit being 1 means that get_ieee_754(value) will | ||
# always be greater or equal to 1, and when 1 is subtracted, the | ||
# result will be greater or equal to 0, whose representation in binary | ||
# will be of at most SIGNIFICAND_WIDTH ones that have an infinite | ||
# amount of leading zeroes. When those SIGNIFICAND_WIDTH bits are | ||
# shifted to the right SIGNIFICAND_WIDTH places, the resulting value | ||
# will be 0. | ||
|
||
# In summary, correction will be -1 if value is a power of 2, 0 if not. | ||
|
||
# FIXME Document why we can assume value will not be 0, inf, or NaN. | ||
correction = (get_ieee_754_significand(value) - 1) >> SIGNIFICAND_WIDTH | ||
|
||
return (exponent + correction) >> -self._scale | ||
|
||
def get_lower_boundary(self, index: int) -> float: | ||
if index < self._min_normal_lower_boundary_index: | ||
raise MappingUnderflowError() | ||
|
||
if index > self._max_normal_lower_boundary_index: | ||
raise MappingOverflowError() | ||
|
||
return ldexp(1, index << -self._scale) | ||
|
||
@property | ||
def scale(self) -> int: | ||
return self._scale |
Oops, something went wrong.