Skip to content

Commit d9708f3

Browse files
committed
Allow users to create native histograms
Initial support for allowing users to create native histograms. The initial implementation directly exposes the initial schema version rather than a factor, and we can provide guidance for choosing a good initial schema. If too many buckets will be created then resolution can be reduced, but in the current implementation the other reduction techniques (increasing zero bucket width, resets, etc...) are not yet implemented.
1 parent e3902ea commit d9708f3

File tree

4 files changed

+670
-30
lines changed

4 files changed

+670
-30
lines changed

prometheus_client/metrics.py

+85-30
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import math
12
import os
23
from threading import Lock
34
import time
@@ -557,10 +558,16 @@ def create_response(request):
557558
558559
The default buckets are intended to cover a typical web/rpc request from milliseconds to seconds.
559560
They can be overridden by passing `buckets` keyword argument to `Histogram`.
561+
562+
In addition, native histograms are experimentally supported, but may change at any time. In order
563+
to use native histograms, one must set `native_histogram_bucket_factor` to a value greater than 1.0.
564+
When native histograms are enabled the classic histogram buckets are only collected if they are
565+
explicitly set.
560566
"""
561567
_type = 'histogram'
562568
_reserved_labelnames = ['le']
563569
DEFAULT_BUCKETS = (.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, INF)
570+
DEFAULT_NATIVE_HISTOGRAM_ZERO_THRESHOLD = 2.938735877055719e-39
564571

565572
def __init__(self,
566573
name: str,
@@ -571,9 +578,26 @@ def __init__(self,
571578
unit: str = '',
572579
registry: Optional[CollectorRegistry] = REGISTRY,
573580
_labelvalues: Optional[Sequence[str]] = None,
574-
buckets: Sequence[Union[float, str]] = DEFAULT_BUCKETS,
581+
buckets: Optional[Sequence[Union[float, str]]] = None,
582+
native_histogram_initial_schema: Optional[int] = None,
583+
native_histogram_max_buckets: int = 160,
584+
native_histogram_zero_threshold: float = DEFAULT_NATIVE_HISTOGRAM_ZERO_THRESHOLD,
585+
native_histogram_max_exemplars: int = 10,
575586
):
587+
if native_histogram_initial_schema and (native_histogram_initial_schema > 8 or native_histogram_initial_schema < -4):
588+
raise ValueError("native_histogram_initial_schema must be between -4 and 8 inclusive")
589+
590+
# Use the default buckets iff we are not using a native histogram.
591+
if buckets is None and native_histogram_initial_schema is None:
592+
buckets = self.DEFAULT_BUCKETS
593+
576594
self._prepare_buckets(buckets)
595+
596+
self._schema = native_histogram_initial_schema
597+
self._max_nh_buckets = native_histogram_max_buckets
598+
self._zero_threshold = native_histogram_zero_threshold
599+
self._max_nh_exemplars = native_histogram_max_exemplars,
600+
577601
super().__init__(
578602
name=name,
579603
documentation=documentation,
@@ -586,7 +610,12 @@ def __init__(self,
586610
)
587611
self._kwargs['buckets'] = buckets
588612

589-
def _prepare_buckets(self, source_buckets: Sequence[Union[float, str]]) -> None:
613+
def _prepare_buckets(self, source_buckets: Optional[Sequence[Union[float, str]]]) -> None:
614+
# Only native histograms are supported for this case.
615+
if source_buckets is None:
616+
self._upper_bounds = None
617+
return
618+
590619
buckets = [float(b) for b in source_buckets]
591620
if buckets != sorted(buckets):
592621
# This is probably an error on the part of the user,
@@ -601,17 +630,35 @@ def _prepare_buckets(self, source_buckets: Sequence[Union[float, str]]) -> None:
601630
def _metric_init(self) -> None:
602631
self._buckets: List[values.ValueClass] = []
603632
self._created = time.time()
604-
bucket_labelnames = self._labelnames + ('le',)
605-
self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues, self._documentation)
606-
for b in self._upper_bounds:
607-
self._buckets.append(values.ValueClass(
608-
self._type,
609-
self._name,
610-
self._name + '_bucket',
611-
bucket_labelnames,
612-
self._labelvalues + (floatToGoString(b),),
613-
self._documentation)
614-
)
633+
634+
if self._schema is not None:
635+
self._native_histogram = values.NativeHistogramMutexValue(
636+
self._type,
637+
self._name,
638+
self._name,
639+
self._labelnames,
640+
self._labelvalues,
641+
self._documentation,
642+
self._schema,
643+
self._zero_threshold,
644+
self._max_nh_buckets,
645+
self._max_nh_exemplars,
646+
)
647+
648+
if self._upper_bounds is not None:
649+
bucket_labelnames = self._labelnames + ('le',)
650+
self._sum = values.ValueClass(self._type, self._name, self._name + '_sum', self._labelnames, self._labelvalues, self._documentation)
651+
for b in self._upper_bounds:
652+
self._buckets.append(values.ValueClass(
653+
self._type,
654+
self._name,
655+
self._name + '_bucket',
656+
bucket_labelnames,
657+
self._labelvalues + (floatToGoString(b),),
658+
self._documentation)
659+
)
660+
661+
615662

616663
def observe(self, amount: float, exemplar: Optional[Dict[str, str]] = None) -> None:
617664
"""Observe the given amount.
@@ -624,14 +671,18 @@ def observe(self, amount: float, exemplar: Optional[Dict[str, str]] = None) -> N
624671
for details.
625672
"""
626673
self._raise_if_not_observable()
627-
self._sum.inc(amount)
628-
for i, bound in enumerate(self._upper_bounds):
629-
if amount <= bound:
630-
self._buckets[i].inc(1)
631-
if exemplar:
632-
_validate_exemplar(exemplar)
633-
self._buckets[i].set_exemplar(Exemplar(exemplar, amount, time.time()))
634-
break
674+
if self._upper_bounds is not None:
675+
self._sum.inc(amount)
676+
for i, bound in enumerate(self._upper_bounds):
677+
if amount <= bound:
678+
self._buckets[i].inc(1)
679+
if exemplar:
680+
_validate_exemplar(exemplar)
681+
self._buckets[i].set_exemplar(Exemplar(exemplar, amount, time.time()))
682+
break
683+
684+
if self._schema and not math.isnan(amount):
685+
self._native_histogram.observe(amount)
635686

636687
def time(self) -> Timer:
637688
"""Time a block of code or function, and observe the duration in seconds.
@@ -642,15 +693,19 @@ def time(self) -> Timer:
642693

643694
def _child_samples(self) -> Iterable[Sample]:
644695
samples = []
645-
acc = 0.0
646-
for i, bound in enumerate(self._upper_bounds):
647-
acc += self._buckets[i].get()
648-
samples.append(Sample('_bucket', {'le': floatToGoString(bound)}, acc, None, self._buckets[i].get_exemplar()))
649-
samples.append(Sample('_count', {}, acc, None, None))
650-
if self._upper_bounds[0] >= 0:
651-
samples.append(Sample('_sum', {}, self._sum.get(), None, None))
652-
if _use_created:
653-
samples.append(Sample('_created', {}, self._created, None, None))
696+
if self._upper_bounds is not None:
697+
acc = 0.0
698+
for i, bound in enumerate(self._upper_bounds):
699+
acc += self._buckets[i].get()
700+
samples.append(Sample('_bucket', {'le': floatToGoString(bound)}, acc, None, self._buckets[i].get_exemplar()))
701+
samples.append(Sample('_count', {}, acc, None, None))
702+
if self._upper_bounds[0] >= 0:
703+
samples.append(Sample('_sum', {}, self._sum.get(), None, None))
704+
if _use_created:
705+
samples.append(Sample('_created', {}, self._created, None, None))
706+
707+
if self._schema:
708+
samples.append(Sample('', {}, 0.0, None, None, self._native_histogram.get()))
654709
return tuple(samples)
655710

656711

prometheus_client/registry.py

+15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from threading import Lock
44
from typing import Dict, Iterable, List, Optional
55

6+
from prometheus_client.samples import NativeHistogram
7+
68
from .metrics_core import Metric
79

810

@@ -141,6 +143,19 @@ def get_sample_value(self, name: str, labels: Optional[Dict[str, str]] = None) -
141143
return s.value
142144
return None
143145

146+
def get_native_histogram_value(self, name: str, labels: Optional[Dict[str, str]] = None) -> Optional[NativeHistogram]:
147+
"""Returns the sample's native histogram value, or None if not found.
148+
149+
This is inefficient, and intended only for use in unittests.
150+
"""
151+
if labels is None:
152+
labels = {}
153+
for metric in self.collect():
154+
for s in metric.samples:
155+
if s.name == name and s.labels == labels:
156+
return s.native_histogram
157+
return None
158+
144159

145160
class RestrictedRegistry:
146161
def __init__(self, names: Iterable[str], registry: CollectorRegistry):

0 commit comments

Comments
 (0)