File Coverage

blib/lib/Net/Prometheus/Histogram.pm
Criterion Covered Total %
statement 79 79 100.0
branch 6 8 75.0
condition 17 22 77.2
subroutine 14 14 100.0
pod 3 3 100.0
total 119 126 94.4


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