File Coverage

blib/lib/Metrics/Any/Adapter/Prometheus.pm
Criterion Covered Total %
statement 80 84 95.2
branch 12 18 66.6
condition 16 41 39.0
subroutine 16 16 100.0
pod 1 11 9.0
total 125 170 73.5


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, 2020 -- leonerd@leonerd.org.uk
5              
6             package Metrics::Any::Adapter::Prometheus;
7              
8 2     2   3130 use strict;
  2         5  
  2         59  
9 2     2   10 use warnings;
  2         5  
  2         77  
10              
11             our $VERSION = '0.04';
12              
13 2     2   11 use Carp;
  2         4  
  2         131  
14              
15 2     2   530 use Net::Prometheus::Registry;
  2         410  
  2         54  
16              
17 2     2   599 use Net::Prometheus::Counter;
  2         9046  
  2         2390  
18              
19             =head1 NAME
20              
21             C - a metrics reporting adapter for Prometheus
22              
23             =head1 SYNOPSIS
24              
25             use Metrics::Any::Adapter 'Prometheus';
26              
27             =head1 DESCRIPTION
28              
29             This L adapter type reports metrics to Prometheus by using
30             L. Each metric added to the adapter will be registered with
31             the global L instance.
32              
33             It becomes the calling program's responsibility to arrange for these to be
34             HTTP accessible by using the C API.
35              
36             Distribution metrics are exported as Histograms by default. They may
37             alternatively be exported as Summaries in order to generate smaller amounts
38             of export data, by setting the C import argument to false:
39              
40             use Metrics::Any::Adapter 'Prometheus', use_histograms => 0;
41              
42             Timer metrics are implemented as distribution metrics with the units set to
43             C.
44              
45             =cut
46              
47             sub new
48             {
49 1     1 0 10 my $class = shift;
50 1         3 my ( %args ) = @_;
51              
52             return bless {
53             metrics => {},
54 1   50     14 use_histograms => $args{use_histograms} // 1,
55             }, $class;
56             }
57              
58             =head1 METHODS
59              
60             =cut
61              
62             sub mangle_name
63             {
64 4     4 0 8 my $self = shift;
65 4         8 my ( $name ) = @_;
66              
67 4 100       19 $name = join "_", @$name if ref $name eq "ARRAY";
68              
69             # TODO: Consider lowercase, squashing unallowed chars to _,...
70              
71 4         9 return $name;
72             }
73              
74             sub make_counter
75             {
76 1     1 0 35 my $self = shift;
77 1         3 my ( $handle, %args ) = @_;
78              
79 1   33     5 my $name = $self->mangle_name( delete $args{name} // $handle );
80 1   33     6 my $help = delete $args{description} // "Metrics::Any counter $handle";
81              
82 1 50       4 if( my $units = delete $args{units} ) {
83             # Append _bytes et.al. if required
84 0 0       0 $name .= "_$units" unless $name =~ m/_\Q$units\E$/;
85             }
86             else {
87             # Prometheus policy says unitless counters take _total suffix
88 1         3 $name .= "_total";
89             }
90              
91 1         6 $self->{metrics}{$handle} = Net::Prometheus::Registry->register(
92             Net::Prometheus::Counter->new(
93             name => $name,
94             help => $help,
95             %args,
96             )
97             );
98             }
99              
100             sub inc_counter_by
101             {
102 1     1 0 183 my $self = shift;
103 1         3 my ( $handle, $amount, @labelvalues ) = @_;
104              
105 1   33     7 ( $self->{metrics}{$handle} or croak "No such counter named '$handle'" )
106             ->inc( @labelvalues, $amount );
107             }
108              
109             =head2 make_distribution
110              
111             $adapter->make_distribution( $name, %args )
112              
113             In addition to the standard arguments, the following are recognised:
114              
115             =over 4
116              
117             =item buckets => ARRAY[ NUM ]
118              
119             If present, overrides the default Histogram bucket sizes.
120              
121             =item bucket_min => NUM
122              
123             =item bucket_max => NUM
124              
125             =item buckets_per_decade => ARRAY[ NUM ]
126              
127             I
128              
129             A more flexible alternative to specifying literal bucket sizes. The values
130             given in C are repeated, multiplied by various powers of
131             10 to generate values between C (or a default of 0.001 if not
132             supplied) and C (or a default of 1000 if not supplied).
133              
134             =back
135              
136             =cut
137              
138             my %BUCKETS_FOR_UNITS = (
139             bytes => { bucket_min => 100, bucket_max => 1E8 },
140             seconds => undef, # Prometheus defaults are fine
141             );
142              
143             # TODO: This probably ought to live in Net::Prometheus::Histogram
144             sub gen_buckets
145             {
146 1     1 0 2 shift;
147 1         2 my ( $args ) = @_;
148              
149 1   50     4 my $min = $args->{bucket_min} // 1E-3;
150 1   50     3 my $max = $args->{bucket_max} // 1E3;
151              
152 1   50     2 my @values_per_decade = @{ $args->{buckets_per_decade} // [ 1 ] };
  1         5  
153              
154 1         3 my $value;
155             my @buckets;
156              
157 1         2 $value = 1;
158 1         3 while( $value >= $min ) {
159 0         0 unshift @buckets, map { $_ * $value } @values_per_decade;
  0         0  
160              
161 0         0 $value /= 10;
162             }
163              
164 1         2 $value = 10;
165 1         3 while( $value <= $max ) {
166 8         12 push @buckets, map { $_ * $value } @values_per_decade;
  8         16  
167              
168 8         16 $value *= 10;
169             }
170              
171             # Trim overgenerated ends
172 1 100       11 @buckets = grep { $min <= $_ and $_ <= $max } @buckets;
  8         28  
173              
174 1         3 $args->{buckets} = \@buckets;
175             }
176              
177             sub make_distribution
178             {
179 2     2 1 2052 my $self = shift;
180 2         27 my ( $handle, %args ) = @_;
181              
182 2   33     12 my $name = $self->mangle_name( delete $args{name} // $handle );
183 2         5 my $units = delete $args{units};
184 2   66     10 my $help = delete $args{description} // "Metrics::Any $units distribution $handle";
185              
186             # Append _bytes et.al. if required
187 2 50       34 $name .= "_$units" unless $name =~ m/_\Q$units\E$/;
188              
189 2 50       6 unless( $args{buckets} ) {
190 2 100       6 %args = ( %{ $BUCKETS_FOR_UNITS{$units} }, %args ) if $BUCKETS_FOR_UNITS{$units};
  1         4  
191              
192 2 100       6 $self->gen_buckets( \%args ) if grep { m/^bucket/ } keys %args;
  2         11  
193             }
194              
195 2 50       6 my $metric_class = $self->{use_histograms} ? "Net::Prometheus::Histogram" :
196             "Net::Prometheus::Summary";
197              
198 2         17 $self->{metrics}{$handle} = Net::Prometheus::Registry->register(
199             $metric_class->new(
200             name => $name,
201             help => $help,
202             %args,
203             )
204             );
205             }
206              
207             sub inc_distribution_by
208             {
209 2     2 0 214 my $self = shift;
210 2         4 my ( $handle, $amount, @labelvalues ) = @_;
211              
212             # TODO: Sanity-check that @labelvalues is as long as the label count
213              
214 2   33     18 ( $self->{metrics}{$handle} or croak "No such distribution named '$handle'" )
215             ->observe( @labelvalues, $amount );
216             }
217              
218             sub make_gauge
219             {
220 1     1 0 4368 my $self = shift;
221 1         3 my ( $handle, %args ) = @_;
222              
223 1   33     5 my $name = $self->mangle_name( delete $args{name} // $handle );
224 1   33     7 my $help = delete $args{description} // "Metrics::Any gauge $handle";
225              
226 1         8 $self->{metrics}{$handle} = Net::Prometheus::Registry->register(
227             Net::Prometheus::Gauge->new(
228             name => $name,
229             help => $help,
230             %args,
231             )
232             );
233             }
234              
235             sub set_gauge_to
236             {
237 1     1 0 160 my $self = shift;
238 1         2 my ( $handle, $amount, @labelvalues ) = @_;
239              
240 1   33     8 ( $self->{metrics}{$handle} or croak "No such gauge named '$handle'" )
241             ->set( @labelvalues, $amount );
242             }
243              
244             sub inc_gauge_by
245             {
246 1     1 0 63 my $self = shift;
247 1         2 my ( $handle, $amount, @labelvalues ) = @_;
248              
249 1   33     4 ( $self->{metrics}{$handle} or croak "No such gauge named '$handle'" )
250             ->inc( @labelvalues, $amount );
251             }
252              
253             sub make_timer
254             {
255 1     1 0 2190 my $self = shift;
256 1         3 my ( $handle, %args ) = @_;
257              
258 1   33     10 $args{description} //= "Metrics::Any timer $handle";
259              
260 1         4 return $self->make_distribution( $handle,
261             %args,
262             units => "seconds",
263             );
264             }
265              
266             *inc_timer_by = \&inc_distribution_by;
267              
268             =head1 AUTHOR
269              
270             Paul Evans
271              
272             =cut
273              
274             0x55AA;