1
+ from math import floor , log2
1
2
import os
2
3
from threading import Lock
3
4
import time
@@ -555,12 +556,32 @@ def create_response(request):
555
556
with REQUEST_TIME.time():
556
557
pass # Logic to be timed
557
558
559
+ There are two kinds of histograms: classic and native. A Histogram object can be both.
560
+
561
+ For classic histograms you can configure buckets.
562
+
558
563
The default buckets are intended to cover a typical web/rpc request from milliseconds to seconds.
559
564
They can be overridden by passing `buckets` keyword argument to `Histogram`.
565
+
566
+ For native histograms you can set a schema, the maximum count of populated
567
+ buckets, a reset timer and a zero threshold.
568
+
569
+ The schema is set indirectly by the `nh_bucket_factor` which determines how much larger the
570
+ next higher bucket is compared to a chosen one. It must be a float greater than one.
571
+
572
+ If one more than the maximum number of populated buckets is filled excluding the zero bucket
573
+ the histogram is reset except if the duration since the last reset is less than the reset time.
574
+ In this case the resolution of the histogram is reduced.
575
+ In case the resultion was reduced as soon as the reset time is reached since the last reset
576
+ the histogram is reset to its initial schema.
560
577
"""
561
578
_type = 'histogram'
562
579
_reserved_labelnames = ['le' ]
563
580
DEFAULT_BUCKETS = (.005 , .01 , .025 , .05 , .075 , .1 , .25 , .5 , .75 , 1.0 , 2.5 , 5.0 , 7.5 , 10.0 , INF )
581
+ DEFAULT_BUCKET_FACTOR = 1.1
582
+ DEFAULT_MIN_RESET_DURATION_SECONDS = 10 * 60
583
+ DEFAULT_MAX_POPULATED_BUCKETS = 100
584
+ DEFAULT_ZERO_THRESHOLD = 1 / 2 ** 2 ** 4
564
585
565
586
def __init__ (self ,
566
587
name : str ,
@@ -572,8 +593,38 @@ def __init__(self,
572
593
registry : Optional [CollectorRegistry ] = REGISTRY ,
573
594
_labelvalues : Optional [Sequence [str ]] = None ,
574
595
buckets : Sequence [Union [float , str ]] = DEFAULT_BUCKETS ,
596
+ * ,
597
+ classic : bool = True ,
598
+ native : bool = False ,
599
+ nh_bucket_factor : float = DEFAULT_BUCKET_FACTOR ,
600
+ nh_max_populated_buckets : int = DEFAULT_MAX_POPULATED_BUCKETS ,
601
+ nh_min_reset_duration_seconds : int = DEFAULT_MIN_RESET_DURATION_SECONDS ,
602
+ nh_zero_threshold : float = DEFAULT_ZERO_THRESHOLD ,
575
603
):
576
- self ._prepare_buckets (buckets )
604
+ if not (classic or native ):
605
+ raise ValueError ('Histogram must be classic or native or both' )
606
+
607
+ if classic :
608
+ self ._upper_bounds = self ._prepare_buckets (buckets )
609
+
610
+ if native :
611
+ if nh_bucket_factor <= 1 :
612
+ raise ValueError ('native_histogram_bucket_factor must be greater than one' )
613
+ if nh_min_reset_duration_seconds <= 0 :
614
+ raise ValueError ('min_reset_duration_seconds must be positive' )
615
+ if nh_zero_threshold is not None and nh_zero_threshold < 0 :
616
+ raise ValueError ('zero_threshold must be non-negative or None' )
617
+ if values .ValueClass ._multiprocess :
618
+ raise ValueError ('native histograms are only supported in threaded mode' )
619
+
620
+ self ._nh_bucket_factor = nh_bucket_factor
621
+ self ._nh_max_populated_buckets = nh_max_populated_buckets
622
+ self ._nh_min_reset_duration_seconds = nh_min_reset_duration_seconds
623
+ self ._nh_zero_threshold = nh_zero_threshold
624
+
625
+ self ._is_classic_histogram = classic
626
+ self ._is_native_histogram = native
627
+
577
628
super ().__init__ (
578
629
name = name ,
579
630
documentation = documentation ,
@@ -584,9 +635,17 @@ def __init__(self,
584
635
registry = registry ,
585
636
_labelvalues = _labelvalues ,
586
637
)
587
- self ._kwargs ['buckets' ] = buckets
588
638
589
- def _prepare_buckets (self , source_buckets : Sequence [Union [float , str ]]) -> None :
639
+ self ._kwargs ['buckets' ] = buckets
640
+ self ._kwargs ['classic' ] = classic
641
+ self ._kwargs ['native' ] = native
642
+ self ._kwargs ['nh_bucket_factor' ] = nh_bucket_factor
643
+ self ._kwargs ['nh_max_populated_buckets' ] = nh_max_populated_buckets
644
+ self ._kwargs ['nh_min_reset_duration_seconds' ] = nh_min_reset_duration_seconds
645
+ self ._kwargs ['nh_zero_threshold' ] = nh_zero_threshold
646
+
647
+ @staticmethod
648
+ def _prepare_buckets (source_buckets : Sequence [Union [float , str ]]) -> Sequence [float ]:
590
649
buckets = [float (b ) for b in source_buckets ]
591
650
if buckets != sorted (buckets ):
592
651
# This is probably an error on the part of the user,
@@ -596,21 +655,37 @@ def _prepare_buckets(self, source_buckets: Sequence[Union[float, str]]) -> None:
596
655
buckets .append (INF )
597
656
if len (buckets ) < 2 :
598
657
raise ValueError ('Must have at least two buckets' )
599
- self ._upper_bounds = buckets
658
+ return buckets
659
+
660
+ @staticmethod
661
+ def _choose_schema_from_bucket_factor (bucket_factor : float ) -> int :
662
+ schema = - floor (log2 (log2 (bucket_factor )))
663
+ return max (min (schema , 8 ), - 4 )
600
664
601
665
def _metric_init (self ) -> None :
602
- self ._buckets : List [values .ValueClass ] = []
603
666
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 )
667
+
668
+ if self ._is_classic_histogram :
669
+ self ._buckets : List [values .ValueClass ] = []
670
+ bucket_labelnames = self ._labelnames + ('le' ,)
671
+ self ._sum = values .ValueClass (self ._type , self ._name , self ._name + '_sum' , self ._labelnames , self ._labelvalues , self ._documentation )
672
+ for b in self ._upper_bounds :
673
+ self ._buckets .append (values .ValueClass (
674
+ self ._type ,
675
+ self ._name ,
676
+ self ._name + '_bucket' ,
677
+ bucket_labelnames ,
678
+ self ._labelvalues + (floatToGoString (b ),),
679
+ self ._documentation )
680
+ )
681
+
682
+ if self ._is_native_histogram :
683
+ schema = self ._choose_schema_from_bucket_factor (self ._nh_bucket_factor )
684
+ self ._native_histogram = values .ThreadSafeNativeHistogram (
685
+ schema = schema ,
686
+ zero_threshold = self ._nh_zero_threshold ,
687
+ max_populated_buckets = self ._nh_max_populated_buckets ,
688
+ min_reset_duration_seconds = self ._nh_min_reset_duration_seconds ,
614
689
)
615
690
616
691
def observe (self , amount : float , exemplar : Optional [Dict [str , str ]] = None ) -> None :
@@ -624,14 +699,19 @@ def observe(self, amount: float, exemplar: Optional[Dict[str, str]] = None) -> N
624
699
for details.
625
700
"""
626
701
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
702
+
703
+ if self ._is_classic_histogram :
704
+ self ._sum .inc (amount )
705
+ for i , bound in enumerate (self ._upper_bounds ):
706
+ if amount <= bound :
707
+ self ._buckets [i ].inc (1 )
708
+ if exemplar :
709
+ _validate_exemplar (exemplar )
710
+ self ._buckets [i ].set_exemplar (Exemplar (exemplar , amount , time .time ()))
711
+ break
712
+
713
+ if self ._is_native_histogram :
714
+ self ._native_histogram .add_observation (amount )
635
715
636
716
def time (self ) -> Timer :
637
717
"""Time a block of code or function, and observe the duration in seconds.
@@ -642,15 +722,23 @@ def time(self) -> Timer:
642
722
643
723
def _child_samples (self ) -> Iterable [Sample ]:
644
724
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 ))
725
+
726
+ # must come first
727
+ if self ._is_native_histogram :
728
+ samples .append (Sample ('' , {}, native_histogram = self ._native_histogram .extract ()))
729
+
730
+ if self ._is_classic_histogram :
731
+ acc = 0.0
732
+ for i , bound in enumerate (self ._upper_bounds ):
733
+ acc += self ._buckets [i ].get ()
734
+ samples .append (Sample ('_bucket' , {'le' : floatToGoString (bound )}, acc , None , self ._buckets [i ].get_exemplar ()))
735
+ samples .append (Sample ('_count' , {}, acc , None , None ))
736
+ if self ._upper_bounds [0 ] >= 0 :
737
+ samples .append (Sample ('_sum' , {}, self ._sum .get (), None , None ))
738
+
652
739
if _use_created :
653
740
samples .append (Sample ('_created' , {}, self ._created , None , None ))
741
+
654
742
return tuple (samples )
655
743
656
744
0 commit comments