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

Exponential Histogram mapping functions for public use #2502

Merged
merged 10 commits into from Mar 22, 2022
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -19,6 +19,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
If the provided environment variables are invalid (negative), the default values would be used.
- Rename the `gc` runtime name to `go` (#2560)
- Log the Exporters configuration in the TracerProviders message. (#2578)
- Metrics Exponential Histogram support: Mapping functions have been made available
in `sdk/metric/aggregator/exponential/mapping` for other OpenTelemetry projects to take
dependencies on. (#2502)

### Changed

Expand All @@ -41,6 +44,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Added

- Support `OTEL_EXPORTER_ZIPKIN_ENDPOINT` env to specify zipkin collector endpoint (#2490)
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
- Use `OTEL_EXPORTER_ZIPKIN_ENDPOINT` environment variable to specify zipkin collector endpoint. (#2490)
- Log the configuration of `TracerProvider`s, and `Tracer`s for debugging.
To enable use a logger with Verbosity (V level) `>=1`. (#2500)
Expand Down
27 changes: 27 additions & 0 deletions sdk/metric/aggregator/exponential/README.md
@@ -0,0 +1,27 @@
# Base-2 Exponential Histogram

## Design

This document is a placeholder for future Aggregator, once seen in [PR
2393](https://github.com/open-telemetry/opentelemetry-go/pull/2393).

Only the mapping functions have been made available at this time. The
equations tested here are specified in the [data model for Exponential
Histogram data points](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/datamodel.md#exponentialhistogram).

### Mapping function

There are two mapping functions used, depending on the sign of the
scale. Negative and zero scales use the `mapping/exponent` mapping
function, which computes the bucket index directly from the bits of
the `float64` exponent. This mapping function is used with scale `-10
<= scale <= 0`. Scales smaller than -10 map the entire normal
`float64` number range into a single bucket, thus are not considered
useful.

The `mapping/logarithm` mapping function uses `math.Log(value)` times
the scaling factor `math.Ldexp(math.Log2E, scale)`. This mapping
function is used with `0 < scale <= 20`. The maximum scale is
selected because at scale 21, simply, it becomes difficult to test
correctness--at this point `math.MaxFloat64` maps to index
`math.MaxInt32` and the `math/big` logic used in testing breaks down.
117 changes: 117 additions & 0 deletions sdk/metric/aggregator/exponential/mapping/exponent/exponent.go
@@ -0,0 +1,117 @@
// 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.

package exponent // import "go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping/exponent"

import (
"fmt"
"math"

"go.opentelemetry.io/otel/sdk/metric/aggregator/exponential/mapping"
)

const (
// MinScale defines the point at which the exponential mapping
// function becomes useless for float64. With scale -10, ignoring
// subnormal values, bucket indices range from -1 to 1.
MinScale int32 = -10

// MaxScale is the largest scale supported in this code. Use
// ../logarithm for larger scales.
MaxScale int32 = 0
)

type exponentMapping struct {
shift uint8 // equals negative scale
}

// exponentMapping is used for negative scales, effectively a
// mapping of the base-2 logarithm of the exponent.
var prebuiltMappings = [-MinScale + 1]exponentMapping{
{10},
{9},
{8},
{7},
{6},
{5},
{4},
{3},
{2},
{1},
{0},
}

// NewMapping constructs an exponential mapping function, used for scales <= 0.
func NewMapping(scale int32) (mapping.Mapping, error) {
if scale > MaxScale {
return nil, fmt.Errorf("exponent mapping requires scale <= 0")
}
if scale < MinScale {
return nil, fmt.Errorf("scale too low")
}
return &prebuiltMappings[scale-MinScale], nil
}

// MapToIndex implements mapping.Mapping.
func (e *exponentMapping) MapToIndex(value float64) int32 {
// Note: we can assume not a 0, Inf, or NaN; positive sign bit.

// Note: bit-shifting does the right thing for negative
// exponents, e.g., -1 >> 1 == -1.
return getBase2(value) >> e.shift
}

func (e *exponentMapping) minIndex() int32 {
return int32(MinNormalExponent) >> e.shift
}

func (e *exponentMapping) maxIndex() int32 {
return int32(MaxNormalExponent) >> e.shift
}

// LowerBoundary implements mapping.Mapping.
func (e *exponentMapping) LowerBoundary(index int32) (float64, error) {
if min := e.minIndex(); index < min {
return 0, mapping.ErrUnderflow
}

if max := e.maxIndex(); index > max {
return 0, mapping.ErrOverflow
}

unbiased := int64(index << e.shift)

// Note: although the mapping function rounds subnormal values
// up to the smallest normal value, there are still buckets
// that may be filled that start at subnormal values. The
// following code handles this correctly. It's equivalent to and
// faster than math.Ldexp(1, int(unbiased)).
if unbiased < int64(MinNormalExponent) {
subnormal := uint64(1 << SignificandWidth)
for unbiased < int64(MinNormalExponent) {
unbiased++
subnormal >>= 1
}
return math.Float64frombits(subnormal), nil
}
exponent := unbiased + ExponentBias

bits := uint64(exponent << SignificandWidth)
return math.Float64frombits(bits), nil
}

// Scale implements mapping.Mapping.
func (e *exponentMapping) Scale() int32 {
return -int32(e.shift)
}