Skip to content

Commit 291b0b0

Browse files
authored
Merge pull request #1654 from shivanthzen/constNativeHistogram
Feat: Add ConstNativeHistogram
2 parents 13851e9 + ae84979 commit 291b0b0

File tree

2 files changed

+744
-4
lines changed

2 files changed

+744
-4
lines changed

prometheus/histogram.go

Lines changed: 201 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package prometheus
1515

1616
import (
17+
"errors"
1718
"fmt"
1819
"math"
1920
"runtime"
@@ -28,6 +29,11 @@ import (
2829
"google.golang.org/protobuf/types/known/timestamppb"
2930
)
3031

32+
const (
33+
nativeHistogramSchemaMaximum = 8
34+
nativeHistogramSchemaMinimum = -4
35+
)
36+
3137
// nativeHistogramBounds for the frac of observed values. Only relevant for
3238
// schema > 0. The position in the slice is the schema. (0 is never used, just
3339
// here for convenience of using the schema directly as the index.)
@@ -1460,9 +1466,9 @@ func pickSchema(bucketFactor float64) int32 {
14601466
floor := math.Floor(math.Log2(math.Log2(bucketFactor)))
14611467
switch {
14621468
case floor <= -8:
1463-
return 8
1469+
return nativeHistogramSchemaMaximum
14641470
case floor >= 4:
1465-
return -4
1471+
return nativeHistogramSchemaMinimum
14661472
default:
14671473
return -int32(floor)
14681474
}
@@ -1851,3 +1857,196 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
18511857
n.exemplars = append(n.exemplars[:nIdx], append([]*dto.Exemplar{e}, append(n.exemplars[nIdx:rIdx], n.exemplars[rIdx+1:]...)...)...)
18521858
}
18531859
}
1860+
1861+
type constNativeHistogram struct {
1862+
desc *Desc
1863+
dto.Histogram
1864+
labelPairs []*dto.LabelPair
1865+
}
1866+
1867+
func validateCount(sum float64, count uint64, negativeBuckets, positiveBuckets map[int]int64, zeroBucket uint64) error {
1868+
var bucketPopulationSum int64
1869+
for _, v := range positiveBuckets {
1870+
bucketPopulationSum += v
1871+
}
1872+
for _, v := range negativeBuckets {
1873+
bucketPopulationSum += v
1874+
}
1875+
bucketPopulationSum += int64(zeroBucket)
1876+
1877+
// If the sum of observations is NaN, the number of observations must be greater or equal to the sum of all bucket counts.
1878+
// Otherwise, the number of observations must be equal to the sum of all bucket counts .
1879+
1880+
if math.IsNaN(sum) && bucketPopulationSum > int64(count) ||
1881+
!math.IsNaN(sum) && bucketPopulationSum != int64(count) {
1882+
return errors.New("the sum of all bucket populations exceeds the count of observations")
1883+
}
1884+
return nil
1885+
}
1886+
1887+
// NewConstNativeHistogram returns a metric representing a Prometheus native histogram with
1888+
// fixed values for the count, sum, and positive/negative/zero bucket counts. As those parameters
1889+
// cannot be changed, the returned value does not implement the Histogram
1890+
// interface (but only the Metric interface). Users of this package will not
1891+
// have much use for it in regular operations. However, when implementing custom
1892+
// OpenTelemetry Collectors, it is useful as a throw-away metric that is generated on the fly
1893+
// to send it to Prometheus in the Collect method.
1894+
//
1895+
// zeroBucket counts all (positive and negative)
1896+
// observations in the zero bucket (with an absolute value less or equal
1897+
// the current threshold).
1898+
// positiveBuckets and negativeBuckets are separate maps for negative and positive
1899+
// observations. The map's value is an int64, counting observations in
1900+
// that bucket. The map's key is the
1901+
// index of the bucket according to the used
1902+
// Schema. Index 0 is for an upper bound of 1 in positive buckets and for a lower bound of -1 in negative buckets.
1903+
// NewConstNativeHistogram returns an error if
1904+
// - the length of labelValues is not consistent with the variable labels in Desc or if Desc is invalid.
1905+
// - the schema passed is not between 8 and -4
1906+
// - the sum of counts in all buckets including the zero bucket does not equal the count if sum is not NaN (or exceeds the count if sum is NaN)
1907+
//
1908+
// See https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/#exponential-histograms for more details about the conversion from OTel to Prometheus.
1909+
func NewConstNativeHistogram(
1910+
desc *Desc,
1911+
count uint64,
1912+
sum float64,
1913+
positiveBuckets, negativeBuckets map[int]int64,
1914+
zeroBucket uint64,
1915+
schema int32,
1916+
zeroThreshold float64,
1917+
createdTimestamp time.Time,
1918+
labelValues ...string,
1919+
) (Metric, error) {
1920+
if desc.err != nil {
1921+
return nil, desc.err
1922+
}
1923+
if err := validateLabelValues(labelValues, len(desc.variableLabels.names)); err != nil {
1924+
return nil, err
1925+
}
1926+
if schema > nativeHistogramSchemaMaximum || schema < nativeHistogramSchemaMinimum {
1927+
return nil, errors.New("invalid native histogram schema")
1928+
}
1929+
if err := validateCount(sum, count, negativeBuckets, positiveBuckets, zeroBucket); err != nil {
1930+
return nil, err
1931+
}
1932+
1933+
NegativeSpan, NegativeDelta := makeBucketsFromMap(negativeBuckets)
1934+
PositiveSpan, PositiveDelta := makeBucketsFromMap(positiveBuckets)
1935+
ret := &constNativeHistogram{
1936+
desc: desc,
1937+
Histogram: dto.Histogram{
1938+
CreatedTimestamp: timestamppb.New(createdTimestamp),
1939+
Schema: &schema,
1940+
ZeroThreshold: &zeroThreshold,
1941+
SampleCount: &count,
1942+
SampleSum: &sum,
1943+
1944+
NegativeSpan: NegativeSpan,
1945+
NegativeDelta: NegativeDelta,
1946+
1947+
PositiveSpan: PositiveSpan,
1948+
PositiveDelta: PositiveDelta,
1949+
1950+
ZeroCount: proto.Uint64(zeroBucket),
1951+
},
1952+
labelPairs: MakeLabelPairs(desc, labelValues),
1953+
}
1954+
if *ret.ZeroThreshold == 0 && *ret.ZeroCount == 0 && len(ret.PositiveSpan) == 0 && len(ret.NegativeSpan) == 0 {
1955+
ret.PositiveSpan = []*dto.BucketSpan{{
1956+
Offset: proto.Int32(0),
1957+
Length: proto.Uint32(0),
1958+
}}
1959+
}
1960+
return ret, nil
1961+
}
1962+
1963+
// MustNewConstNativeHistogram is a version of NewConstNativeHistogram that panics where
1964+
// NewConstNativeHistogram would have returned an error.
1965+
func MustNewConstNativeHistogram(
1966+
desc *Desc,
1967+
count uint64,
1968+
sum float64,
1969+
positiveBuckets, negativeBuckets map[int]int64,
1970+
zeroBucket uint64,
1971+
nativeHistogramSchema int32,
1972+
nativeHistogramZeroThreshold float64,
1973+
createdTimestamp time.Time,
1974+
labelValues ...string,
1975+
) Metric {
1976+
nativehistogram, err := NewConstNativeHistogram(desc,
1977+
count,
1978+
sum,
1979+
positiveBuckets,
1980+
negativeBuckets,
1981+
zeroBucket,
1982+
nativeHistogramSchema,
1983+
nativeHistogramZeroThreshold,
1984+
createdTimestamp,
1985+
labelValues...)
1986+
if err != nil {
1987+
panic(err)
1988+
}
1989+
return nativehistogram
1990+
}
1991+
1992+
func (h *constNativeHistogram) Desc() *Desc {
1993+
return h.desc
1994+
}
1995+
1996+
func (h *constNativeHistogram) Write(out *dto.Metric) error {
1997+
out.Histogram = &h.Histogram
1998+
out.Label = h.labelPairs
1999+
return nil
2000+
}
2001+
2002+
func makeBucketsFromMap(buckets map[int]int64) ([]*dto.BucketSpan, []int64) {
2003+
if len(buckets) == 0 {
2004+
return nil, nil
2005+
}
2006+
var ii []int
2007+
for k := range buckets {
2008+
ii = append(ii, k)
2009+
}
2010+
sort.Ints(ii)
2011+
2012+
var (
2013+
spans []*dto.BucketSpan
2014+
deltas []int64
2015+
prevCount int64
2016+
nextI int
2017+
)
2018+
2019+
appendDelta := func(count int64) {
2020+
*spans[len(spans)-1].Length++
2021+
deltas = append(deltas, count-prevCount)
2022+
prevCount = count
2023+
}
2024+
2025+
for n, i := range ii {
2026+
count := buckets[i]
2027+
// Multiple spans with only small gaps in between are probably
2028+
// encoded more efficiently as one larger span with a few empty
2029+
// buckets. Needs some research to find the sweet spot. For now,
2030+
// we assume that gaps of one or two buckets should not create
2031+
// a new span.
2032+
iDelta := int32(i - nextI)
2033+
if n == 0 || iDelta > 2 {
2034+
// We have to create a new span, either because we are
2035+
// at the very beginning, or because we have found a gap
2036+
// of more than two buckets.
2037+
spans = append(spans, &dto.BucketSpan{
2038+
Offset: proto.Int32(iDelta),
2039+
Length: proto.Uint32(0),
2040+
})
2041+
} else {
2042+
// We have found a small gap (or no gap at all).
2043+
// Insert empty buckets as needed.
2044+
for j := int32(0); j < iDelta; j++ {
2045+
appendDelta(0)
2046+
}
2047+
}
2048+
appendDelta(count)
2049+
nextI = i + 1
2050+
}
2051+
return spans, deltas
2052+
}

0 commit comments

Comments
 (0)