Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exponent and logarithm mappings #2960

Merged
merged 24 commits into from
Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

- Add logarithm and exponent mappings
([#2960](https://github.com/open-telemetry/opentelemetry-python/pull/2960))
- Add and use missing metrics environment variables
([#2968](https://github.com/open-telemetry/opentelemetry-python/pull/2968))
- Enabled custom samplers via entry points
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# 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):
"""
Parent class for `LogarithmMapping` and `ExponentialMapping`.
"""

# pylint: disable=no-member
def __new__(cls, scale: int):
ocelotl marked this conversation as resolved.
Show resolved Hide resolved

with cls._mappings_lock:
ocelotl marked this conversation as resolved.
Show resolved Hide resolved
# cls._mappings and cls._mappings_lock are implemented in each of
# the child classes as a dictionary and a lock, respectively. They
# are not instantiated here because that would lead to both child
# classes having the same instance of cls._mappings and
# cls._mappings_lock.
if scale not in cls._mappings:
cls._mappings[scale] = super().__new__(cls)
cls._mappings[scale]._init(scale)

return cls._mappings[scale]

@abstractmethod
def _init(self, scale: int) -> None:
# pylint: disable=attribute-defined-outside-init

if scale > self._get_max_scale():
raise Exception(f"scale is larger than {self._max_scale}")

if scale < self._get_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))
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/data-model.md#all-scales-use-the-logarithm-function
self._scale = scale

@abstractmethod
def _get_min_scale(self) -> int:
"""
Return the smallest possible value for the mapping scale
"""

@abstractmethod
def _get_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:
ocelotl marked this conversation as resolved.
Show resolved Hide resolved
"""
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 normal floating point values. If the corresponding
bucket's upper boundary is less than or equal to 2 ** -1022,
:class:`~opentelemetry.sdk.metrics.MappingUnderflowError`
will be raised. If the corresponding bucket's lower boundary is greater
than ``sys.float_info.max``,
:class:`~opentelemetry.sdk.metrics.MappingOverflowError`
will be raised.
"""

@property
def scale(self) -> int:
ocelotl marked this conversation as resolved.
Show resolved Hide resolved
"""
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
"""
return self._scale
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
denormal floating point value.
"""


class MappingOverflowError(Exception):
"""
Raised when computing the lower boundary of an index that maps into +inf.
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# 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 (
MANTISSA_WIDTH,
MAX_NORMAL_EXPONENT,
ocelotl marked this conversation as resolved.
Show resolved Hide resolved
MIN_NORMAL_EXPONENT,
MIN_NORMAL_VALUE,
get_ieee_754_exponent,
get_ieee_754_mantissa,
)


class ExponentMapping(Mapping):
ocelotl marked this conversation as resolved.
Show resolved Hide resolved
# Reference implementation here:
# https://github.com/open-telemetry/opentelemetry-go/blob/0e6f9c29c10d6078e8131418e1d1d166c7195d61/sdk/metric/aggregator/exponential/mapping/exponent/exponent.go

_mappings = {}
_mappings_lock = Lock()

_min_scale = -10
_max_scale = 0

def _get_min_scale(self):
# _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.
return -10

def _get_max_scale(self):
# _max_scale is the largest scale supported by exponential mapping. Use
# a logarithm mapping for larger scales.
return 0

def _init(self, scale: int):
# pylint: disable=attribute-defined-outside-init

super()._init(scale)

# self._min_normal_lower_boundary_index is the largest index such that
# base ** index < MIN_NORMAL_VALUE and
# base ** (index + 1) >= MIN_NORMAL_VALUE. An exponential histogram
# bucket with this index covers the range
# (base ** index, base (index + 1)], including MIN_NORMAL_VALUE. This
# is the smallest valid index that contains at least one normal value.
index = MIN_NORMAL_EXPONENT >> -self._scale

if -self._scale < 2:
# For scales -1 and 0, the maximum value 2 ** -1022 is a
# power-of-two multiple, meaning base ** index == MIN_NORMAL_VALUE.
# Subtracting 1 so that base ** (index + 1) == MIN_NORMAL_VALUE.
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_mantissa(value) gets the positive integer made up
# from the rightmost MANTISSA_WIDTH bits (the mantissa) of the IEEE
# 754 representation of value. If value is an exact power of 2, all
# these MANTISSA_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 MANTISSA_WIDTH
# places, the resulting value for correction is -1. If value is not an
# exact power of 2, at least one of the rightmost MANTISSA_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
# MANTISSA_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 MANTISSA_WIDTH ones that have an infinite
# amount of leading zeroes. When those MANTISSA_WIDTH bits are
# shifted to the right MANTISSA_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_mantissa(value) - 1) >> MANTISSA_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
Loading