File Coverage

blib/lib/Net/Prometheus/Histogram.pm
Criterion Covered Total %
statement 101 101 100.0
branch 7 8 87.5
condition 19 22 86.3
subroutine 17 17 100.0
pod 4 4 100.0
total 148 152 97.3


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2016-2026 -- leonerd@leonerd.org.uk
5              
6             package Net::Prometheus::Histogram 0.16;
7              
8 11     11   235475 use v5.20;
  11         43  
9 11     11   86 use warnings;
  11         21  
  11         727  
10 11     11   71 use base qw( Net::Prometheus::Metric );
  11         22  
  11         1597  
11              
12 11     11   67 use feature qw( postderef signatures );
  11         21  
  11         1546  
13 11     11   70 no warnings qw( experimental::postderef experimental::signatures );
  11         30  
  11         495  
14              
15 11     11   108 use Carp;
  11         26  
  11         2395  
16 11     11   71 use List::Util 1.33 qw( any );
  11         336  
  11         747  
17              
18 11     11   61 use constant _type => "histogram";
  11         19  
  11         1080  
19              
20 11         15594 use constant DEFAULT_BUCKETS => [
21             0.005,
22             0.01, 0.025, 0.05, 0.075,
23             0.1, 0.25, 0.5, 0.75,
24             1.0, 2.5, 5.0, 7.5,
25             10
26 11     11   67 ];
  11         21  
27              
28             __PACKAGE__->MAKE_child_class;
29              
30             =head1 NAME
31              
32             C - count the distribution of numeric observations
33              
34             =head1 SYNOPSIS
35              
36             =for highlighter language=perl
37              
38             use Net::Prometheus;
39             use Time::HiRes qw( time );
40              
41             my $client = Net::Prometheus->new;
42              
43             my $histogram = $client->new_histogram(
44             name => "request_seconds",
45             help => "Summary request processing time",
46             );
47              
48             sub handle_request
49             {
50             my $start = time();
51              
52             ...
53              
54             $summary->observe( time() - $start );
55             }
56              
57             =head1 DESCRIPTION
58              
59             This class provides a histogram metric - a count of the distribution of
60             individual numerical observations into distinct buckets. These are usually
61             reports of times. It is a subclass of L.
62              
63             =cut
64              
65             =head1 CONSTRUCTOR
66              
67             Instances of this class are not usually constructed directly, but instead via
68             the L object that will serve it:
69              
70             $histogram = $prometheus->new_histogram( %args );
71              
72             This takes the same constructor arguments as documented in
73             L, and additionally the following:
74              
75             =over
76              
77             =item buckets => ARRAY
78              
79             A reference to an ARRAY containing numerical upper bounds for the buckets.
80              
81             =item bucket_min => NUM
82              
83             =item bucket_max => NUM
84              
85             =item buckets_per_decade => ARRAY[ NUM ]
86              
87             I
88              
89             A more flexible alternative to specifying literal bucket sizes. The values
90             given in C are repeated, multiplied by various powers of
91             10 to generate values between C (or a default of 0.001 if not
92             supplied) and C (or a default of 1000 if not supplied).
93              
94             =back
95              
96             =cut
97              
98 7         13 sub new ( $class, %opts )
99 7     7 1 168190 {
  7         23  
  7         10  
100 7 100 100     32 if( !$opts{buckets} and grep { m/^bucket/ } keys %opts ) {
  18         43  
101 3         8 _gen_buckets( \%opts );
102             }
103              
104 7   100     20 my $buckets = $opts{buckets} || DEFAULT_BUCKETS;
105              
106             $buckets->[$_] > $buckets->[$_-1] or
107 7   66     187 croak "Histogram bucket limits must be monotonically-increasing" for 1 .. $#$buckets;
108              
109 2     2   258 $opts{labels} and any { $_ eq "le" } $opts{labels}->@* and
110 6 100 100     35 croak "A Histogram may not have a label called 'le'";
111              
112 5         36 my $self = $class->SUPER::new( %opts );
113              
114 5         20 $self->{bounds} = [ @$buckets ]; # clone it
115 5         15 $self->{bucketcounts} = {};
116 5         9 $self->{sums} = {};
117              
118 5 100       17 if( !$self->labelcount ) {
119 4         18 $self->{bucketcounts}{""} = [ ( 0 ) x ( @$buckets + 1 ) ];
120 4         39 $self->{sums}{""} = 0;
121             }
122              
123 5         17 return $self;
124             }
125              
126             sub _gen_buckets ( $opts )
127 3     3   4 {
  3         5  
  3         2  
128 3   50     10 my $min = $opts->{bucket_min} // 1E-3;
129 3   100     10 my $max = $opts->{bucket_max} // 1E3;
130              
131 3   100     14 my @values_per_decade = ( $opts->{buckets_per_decade} // [ 1 ] )->@*;
132              
133 3         4 my $power = 0;
134 3         5 my $value;
135             my @buckets;
136              
137 3         9 while( ( $value = 10 ** $power ) >= $min ) {
138 9         10 unshift @buckets, map { $_ * $value } @values_per_decade;
  14         21  
139              
140 9         19 $power--;
141             }
142              
143 3         5 $power = 1;
144 3         6 while( ( $value = 10 ** $power ) <= $max ) {
145 9         10 push @buckets, map { $_ * $value } @values_per_decade;
  24         26  
146              
147 9         14 $power++;
148             }
149              
150             # Trim overgenerated ends
151 3 50       6 @buckets = grep { $min <= $_ and $_ <= $max } @buckets;
  38         69  
152              
153 3         7 $opts->{buckets} = \@buckets;
154             }
155              
156             =head2 bucket_bounds
157              
158             @bounds = $histogram->bucket_bounds;
159              
160             Returns the bounding values for each of the buckets, excluding the final
161             C<+Inf> bucket.
162              
163             =cut
164              
165             sub bucket_bounds ( $self )
166 4     4 1 5734 {
  4         4  
  4         5  
167 4         32 return $self->{bounds}->@*;
168             }
169              
170             =head2 observe
171              
172             $histogram->observe( @label_values, $value );
173             $histogram->observe( \%labels, $value );
174              
175             $child->observe( $value );
176              
177             Increment the histogram sum by the given value, and each bucket count by 1
178             where the value is less than or equal to the bucket upper bound.
179              
180             =cut
181              
182             __PACKAGE__->MAKE_child_method( 'observe' );
183 5         6 sub _observe_child ( $self, $labelkey, $value )
  5         6  
184 5     5   19 {
  5         5  
  5         5  
185 5         8 my $bounds = $self->{bounds};
186 5   100     27 my $buckets = $self->{bucketcounts}{$labelkey} ||= [ ( 0 ) x ( @$bounds + 1 ) ];
187              
188 5   66     36 $value <= $bounds->[$_] and $buckets->[$_]++ for 0 .. $#$bounds;
189 5         6 $buckets->[scalar @$bounds]++;
190              
191 5         16 $self->{sums}{$labelkey} += $value;
192             }
193              
194             # remove is generated automatically
195 1         2 sub _remove_child ( $self, $labelkey )
196 1     1   2 {
  1         2  
  1         2  
197 1         2 delete $self->{bucketcounts}{$labelkey};
198 1         4 delete $self->{sums}{$labelkey};
199             }
200              
201             sub clear ( $self )
202 1     1 1 1805 {
  1         2  
  1         2  
203 1         4 undef $self->{bucketcounts}->%*;
204 1         3 undef $self->{sums}->%*;
205             }
206              
207             sub samples ( $self )
208 6     6 1 30 {
  6         9  
  6         8  
209 6         12 my $bounds = $self->{bounds};
210 6         10 my $bucketcounts = $self->{bucketcounts};
211 6         10 my $sums = $self->{sums};
212              
213             return map {
214 6         24 my $labelkey = $_;
  8         69  
215 8         13 my $buckets = $bucketcounts->{$labelkey};
216              
217             $self->make_sample( count => $labelkey, $buckets->[-1] ),
218             $self->make_sample( sum => $labelkey, $sums->{$labelkey} ),
219             ( map {
220 8         56 $self->make_sample( bucket => $labelkey, $buckets->[$_], [ le => $bounds->[$_] ] )
  79         1770  
221             } 0 .. $#$bounds ),
222             $self->make_sample( bucket => $labelkey, $buckets->[-1], [ le => "+Inf" ] );
223             } sort keys %$sums;
224             }
225              
226             =head1 AUTHOR
227              
228             Paul Evans
229              
230             =cut
231              
232             0x55AA;