File Coverage

blib/lib/Net/Prometheus.pm
Criterion Covered Total %
statement 117 136 86.0
branch 11 16 68.7
condition 6 10 60.0
subroutine 29 33 87.8
pod 13 13 100.0
total 176 208 84.6


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;
7              
8 7     7   415642 use strict;
  7         68  
  7         205  
9 7     7   37 use warnings;
  7         11  
  7         258  
10              
11             our $VERSION = '0.10';
12              
13 7     7   37 use Carp;
  7         13  
  7         455  
14              
15 7     7   42 use List::Util 1.29 qw( pairmap );
  7         188  
  7         737  
16              
17 7     7   2875 use Net::Prometheus::Gauge;
  7         15  
  7         233  
18 7     7   2958 use Net::Prometheus::Counter;
  7         20  
  7         223  
19 7     7   2835 use Net::Prometheus::Summary;
  7         21  
  7         176  
20 7     7   2915 use Net::Prometheus::Histogram;
  7         16  
  7         206  
21              
22 7     7   2829 use Net::Prometheus::Registry;
  7         19  
  7         206  
23              
24 7     7   2824 use Net::Prometheus::ProcessCollector;
  7         22  
  7         231  
25              
26 7     7   43 use Net::Prometheus::Types qw( MetricSamples );
  7         14  
  7         367  
27              
28 7     7   41 use constant HAVE_PERLCOLLECTOR => defined eval { require Net::Prometheus::PerlCollector };
  7         14  
  7         11  
  7         13020  
29              
30             =head1 NAME
31              
32             C - export monitoring metrics for F
33              
34             =head1 SYNOPSIS
35              
36             use Net::Prometheus;
37              
38             my $client = Net::Prometheus->new;
39              
40             my $counter = $client->new_counter(
41             name => "requests",
42             help => "Number of received requests",
43             );
44              
45             sub handle_request
46             {
47             $counter->inc;
48             ...
49             }
50              
51             use Plack::Builder;
52              
53             builder {
54             mount "/metrics" => $client->psgi_app;
55             ...
56             }
57              
58             =head1 DESCRIPTION
59              
60             This module provides the ability for a program to collect monitoring metrics
61             and export them to the F monitoring server.
62              
63             As C will expect to collect the metrics by making an HTTP request,
64             facilities are provided to yield a L application that the containing
65             program can embed in its own structure to provide the results, or the
66             application can generate a plain-text result directly and serve them by its
67             own means.
68              
69             =head2 Metrics::Any
70              
71             For more flexibility of metrics reporting, other modules may wish to use
72             L as an abstraction interface instead of directly using this
73             API.
74              
75             By using C instead, the module does not directly depend on
76             C, and in addition program ultimately using the module gets
77             the flexibility to use Prometheus (via L)
78             or use another reporting system via a different adapter.
79              
80             =cut
81              
82             =head1 CONSTRUCTOR
83              
84             =cut
85              
86             =head2 new
87              
88             $prometheus = Net::Prometheus->new;
89              
90             Returns a new C instance.
91              
92             Takes the following named arguments:
93              
94             =over
95              
96             =item disable_process_collector => BOOL
97              
98             If present and true, this instance will not load the default process collector
99             from L. If absent or false, such a
100             collector will be loaded by default.
101              
102             =item disable_perl_collector => BOOL
103              
104             If present and true, this instance will not load perl-specific collector from
105             L. If absent or false this collector is loaded
106             if the module is installed and useable.
107              
108             These two options are provided for testing purposes, or for specific use-cases
109             where such features are not required. Usually it's best just to leave these
110             enabled.
111              
112             =back
113              
114             =cut
115              
116             sub new
117             {
118 7     7 1 1179 my $class = shift;
119 7         31 my %args = @_;
120              
121 7         48 my $self = bless {
122             registry => Net::Prometheus::Registry->new,
123             }, $class;
124              
125 7 100 66     54 if( not $args{disable_process_collector} and
126             my $process_collector = Net::Prometheus::ProcessCollector->new ) {
127 2         13 $self->register( $process_collector );
128             }
129              
130 7 50 100     35 if( not $args{disable_perl_collector} and HAVE_PERLCOLLECTOR ) {
131 0         0 $self->register( Net::Prometheus::PerlCollector->new );
132             }
133              
134 7         27 return $self;
135             }
136              
137             =head1 METHODS
138              
139             =cut
140              
141             =head2 register
142              
143             $collector = $prometheus->register( $collector )
144              
145             Registers a new L to be collected from by the C
146             method. The collector instance itself is returned, for convenience.
147              
148             =cut
149              
150             sub register
151             {
152 13     13 1 61 my $self = shift;
153 13         26 my ( $collector ) = @_;
154              
155 13         76 return $self->{registry}->register( $collector );
156             }
157              
158             =head2 unregister
159              
160             $prometheus->unregister( $collector )
161              
162             Removes a previously-registered collector.
163              
164             =cut
165              
166             sub unregister
167             {
168 2     2 1 1119 my $self = shift;
169 2         4 my ( $collector ) = @_;
170              
171 2         7 return $self->{registry}->unregister( $collector );
172             }
173              
174             =head2 new_gauge
175              
176             $gauge = $prometheus->new_gauge( %args )
177              
178             Constructs a new L using the arguments given and
179             registers it with the exporter. The newly-constructed gauge is returned.
180              
181             =cut
182              
183             sub new_gauge
184             {
185 5     5 1 18 my $self = shift;
186 5         17 my %args = @_;
187              
188 5         32 return $self->register( Net::Prometheus::Gauge->new( %args ) );
189             }
190              
191             =head2 new_counter
192              
193             $counter = $prometheus->new_counter( %args )
194              
195             Constructs a new L using the arguments given and
196             registers it with the exporter. The newly-constructed counter is returned.
197              
198             =cut
199              
200             sub new_counter
201             {
202 1     1 1 3 my $self = shift;
203 1         3 my %args = @_;
204              
205 1         11 return $self->register( Net::Prometheus::Counter->new( %args ) );
206             }
207              
208             =head2 new_summary
209              
210             $summary = $prometheus->new_summary( %args )
211              
212             Constructs a new L using the arguments given
213             and registers it with the exporter. The newly-constructed summary is returned.
214              
215             =cut
216              
217             sub new_summary
218             {
219 0     0 1 0 my $self = shift;
220 0         0 my %args = @_;
221              
222 0         0 return $self->register( Net::Prometheus::Summary->new( %args ) );
223             }
224              
225             =head2 new_histogram
226              
227             $histogram = $prometheus->new_histogram( %args )
228              
229             Constructs a new L using the arguments given
230             and registers it with the exporter. The newly-constructed histogram is
231             returned.
232              
233             =cut
234              
235             sub new_histogram
236             {
237 0     0 1 0 my $self = shift;
238 0         0 my %args = @_;
239              
240 0         0 return $self->register( Net::Prometheus::Histogram->new( %args ) );
241             }
242              
243             =head2 new_metricgroup
244              
245             $group = $prometheus->new_metricgroup( %args )
246              
247             Returns a new Metric Group instance as a convenience for registering multiple
248             metrics using the same C and C arguments. Takes the
249             following named arguments:
250              
251             =over
252              
253             =item namespace => STR
254              
255             =item subsystem => STR
256              
257             String values to pass by default into new metrics the group will construct.
258              
259             =back
260              
261             Once constructed, the group acts as a proxy to the other C methods,
262             passing in these values as overrides.
263              
264             $gauge = $group->new_gauge( ... )
265             $counter = $group->new_counter( ... )
266             $summary = $group->new_summary( ... )
267             $histogram = $group->new_histogram( ... )
268              
269             =cut
270              
271             sub new_metricgroup
272             {
273 2     2 1 583 my $self = shift;
274 2         7 my ( %args ) = @_;
275              
276 2         12 return Net::Prometheus::_MetricGroup->new(
277             $self, %args
278             );
279             }
280              
281             =head2 collect
282              
283             @metricsamples = $prometheus->collect( $opts )
284              
285             Returns a list of L obtained from all
286             of the currently-registered collectors.
287              
288             =cut
289              
290             sub collect
291             {
292 15     15 1 33 my $self = shift;
293 15         31 my ( $opts ) = @_;
294              
295 15   100     76 $opts //= {};
296              
297 15         27 my %samples_by_name;
298 15         56 foreach my $collector ( $self->{registry}->collectors, Net::Prometheus::Registry->collectors ) {
299 16         96 push @{ $samples_by_name{ $_->fullname } }, $_ for $collector->collect( $opts );
  29         327  
300             }
301              
302             return map {
303 15         221 my @results = @{ $samples_by_name{ $_ } };
  28         42  
  28         70  
304 28         42 my $first = $results[0];
305              
306             @results > 1 ?
307             MetricSamples( $first->fullname, $first->type, $first->help,
308 28 100       98 [ map { @{ $_->samples } } @results ]
  2         20  
  2         6  
309             ) :
310             $first;
311             } sort keys %samples_by_name;
312             }
313              
314             =head2 render
315              
316             $str = $prometheus->render
317              
318             Returns a string in the Prometheus text exposition format containing the
319             current values of all the registered metrics.
320              
321             $str = $prometheus->render( { options => "for collectors" } )
322              
323             An optional HASH reference may be provided; if so it will be passed into the
324             C method of every registered collector.
325              
326             =cut
327              
328             sub _render_label_value
329             {
330 8     8   14 my ( $v ) = @_;
331              
332 8         32 $v =~ s/(["\\])/\\$1/g;
333 8         17 $v =~ s/\n/\\n/g;
334              
335 8         54 return qq("$v");
336             }
337              
338             sub _render_labels
339             {
340 27     27   220 my ( $labels ) = @_;
341              
342 27 100       82 return "" if !scalar @$labels;
343              
344             return "{" .
345 7     8   51 join( ",", pairmap { $a . "=" . _render_label_value( $b ) } @$labels ) .
  8         20  
346             "}";
347             }
348              
349             sub render
350             {
351 14     14 1 619 my $self = shift;
352 14         29 my ( $opts ) = @_;
353              
354             return join "", map {
355 14         46 my $metricsamples = $_;
  27         190  
356              
357 27         62 my $fullname = $metricsamples->fullname;
358              
359 27         147 my $help = $metricsamples->help;
360 27         143 $help =~ s/\\/\\\\/g;
361 27         46 $help =~ s/\n/\\n/g;
362              
363             "# HELP $fullname $help\n",
364             "# TYPE $fullname " . $metricsamples->type . "\n",
365             map {
366 27         125 my $sample = $_;
367 27         60 sprintf "%s%s %s\n",
368             $sample->varname,
369             _render_labels( $sample->labels ),
370             $sample->value
371 27         100 } @{ $metricsamples->samples }
  27         167  
372             } $self->collect( $opts );
373             }
374              
375             =head2 handle
376              
377             $response = $prometheus->handle( $request )
378              
379             Given an HTTP request in an L instance, renders the metrics in
380             response to it and returns an L instance.
381              
382             This application will respond to any C request, and reject requests for
383             any other method. If a query string is present on the URI it will be parsed
384             for collector options to pass into the L method.
385              
386             This method is useful for integrating metrics into an existing HTTP server
387             application which uses these objects. For example:
388              
389             my $prometheus = Net::Prometheus->new;
390              
391             sub serve_request
392             {
393             my ( $request ) = @_;
394              
395             if( $request->uri->path eq "/metrics" ) {
396             return $prometheus->handle( $request );
397             }
398              
399             ...
400             }
401              
402             =cut
403              
404             # Some handy pseudomethods to make working on HTTP::Response less painful
405             my $set_header = sub {
406             my $resp = shift;
407             $resp->header( @_ );
408             $resp;
409             };
410             my $set_content = sub {
411             my $resp = shift;
412             $resp->content( @_ );
413             $resp;
414             };
415             my $fix_content_length = sub {
416             my $resp = shift;
417             $resp->content_length or $resp->content_length( length $resp->content );
418             $resp;
419             };
420              
421             sub handle
422             {
423 1     1 1 8476 my $self = shift;
424 1         3 my ( $request ) = @_;
425              
426 1         512 require HTTP::Response;
427              
428 1 50       6964 $request->method eq "GET" or return
429             HTTP::Response->new( 405 )
430             ->$set_header( Content_Type => "text/plain" )
431             ->$set_content( "Method " . $request->method . " not supported" )
432             ->$fix_content_length;
433              
434 1         18 my $opts;
435 1 50       5 $opts = { $request->uri->query_form } if length $request->uri->query;
436              
437 1         84 return HTTP::Response->new( 200 )
438             ->$set_header( Content_Type => "text/plain; version=0.0.4; charset=utf-8" )
439             ->$set_content( $self->render( $opts ) )
440             ->$fix_content_length;
441             }
442              
443             =head2 psgi_app
444              
445             $app = $prometheus->psgi_app
446              
447             Returns a new L application as a C reference. This application
448             will render the metrics in the Prometheus text exposition format, suitable for
449             scraping by the Prometheus collector.
450              
451             This application will respond to any C request, and reject requests for
452             any other method. If a C is present in the environment it will
453             be parsed for collector options to pass into the L method.
454              
455             This method is useful for integrating metrics into an existing HTTP server
456             application which is uses or is based on PSGI. For example:
457              
458             use Plack::Builder;
459              
460             my $prometheus = Net::Prometheus::->new;
461              
462             builder {
463             mount "/metrics" => $prometheus->psgi_app;
464             ...
465             }
466              
467             =cut
468              
469             sub psgi_app
470             {
471 1     1 1 6 my $self = shift;
472              
473 1         542 require URI;
474              
475             return sub {
476 1     1   1124 my $env = shift;
477 1         3 my $method = $env->{REQUEST_METHOD};
478              
479 1 50       6 $method eq "GET" or return [
480             405,
481             [ "Content-Type" => "text/plain" ],
482             [ "Method $method not supported" ],
483             ];
484              
485 1         2 my $opts;
486 1 50       5 if( defined $env->{QUERY_STRING} ) {
487 0         0 $opts = +{ URI->new( "?$env->{QUERY_STRING}", "http" )->query_form };
488             }
489              
490             return [
491 1         5 200,
492             [ "Content-Type" => "text/plain; version=0.0.4; charset=utf-8" ],
493             [ $self->render( $opts ) ],
494             ];
495 1         4688 };
496             }
497              
498             =head2 export_to_IO_Async
499              
500             $prometheus->export_to_IO_Async( $loop, %args )
501              
502             Performs the necessary steps to create an HTTP server for exporting metrics
503             over HTTP via L. This will involve creating a new
504             L instance added to the loop.
505              
506             This new server will listen on its own port number for any incoming request,
507             and will serve metrics regardless of path.
508              
509             Note this should only be used in applications that don't otherwise have an
510             HTTP server, such as self-contained monitoring exporters or exporting metrics
511             as a side-effect of other activity. For existing HTTP server applications it
512             is better to integrate with the existing request/response processing of the
513             application, such as by using the L or L methods.
514              
515             Takes the following named arguments:
516              
517             =over 4
518              
519             =item port => INT
520              
521             Port number on which to listen for incoming HTTP requests.
522              
523             =back
524              
525             =cut
526              
527             sub export_to_IO_Async
528             {
529 0     0 1 0 my $self = shift;
530 0         0 my ( $loop, %args ) = @_;
531              
532 0         0 require IO::Async::Loop;
533 0         0 require Net::Async::HTTP::Server;
534              
535 0   0     0 $loop //= IO::Async::Loop->new;
536              
537             my $httpserver = Net::Async::HTTP::Server->new(
538             on_request => sub {
539 0     0   0 my $httpserver = shift;
540 0         0 my ( $req ) = @_;
541              
542 0         0 $req->respond( $self->handle( $req->as_http_request ) );
543             },
544 0         0 );
545              
546 0         0 $loop->add( $httpserver );
547              
548             # Yes this is a blocking call
549             $httpserver->listen(
550             socktype => "stream",
551             service => $args{port},
552 0         0 )->get;
553             }
554              
555             {
556             package
557             Net::Prometheus::_MetricGroup;
558              
559             sub new
560             {
561 2     2   3 my $class = shift;
562 2         6 my ( $prometheus, %args ) = @_;
563             return bless {
564             prometheus => $prometheus,
565             namespace => $args{namespace},
566             subsystem => $args{subsystem},
567 2         15 }, $class;
568             }
569              
570             foreach my $method (qw( new_gauge new_counter new_summary new_histogram )) {
571 7     7   46 no strict 'refs';
  7         15  
  7         780  
572             *$method = sub {
573 2     2   5 my $self = shift;
574             $self->{prometheus}->$method(
575             namespace => $self->{namespace},
576             subsystem => $self->{subsystem},
577 2         11 @_,
578             );
579             };
580             }
581             }
582              
583             =head1 COLLECTORS
584              
585             The toplevel C object stores a list of "collector" instances,
586             which are used to generate the values that will be made visible via the
587             L method. A collector can be any object instance that has a method
588             called C, which when invoked is passed no arguments and expected to
589             return a list of L structures.
590              
591             @metricsamples = $collector->collect( $opts )
592              
593             The L class is already a valid collector (and hence,
594             so too are the individual metric type subclasses). This interface allows the
595             creation of new custom collector objects, that more directly collect
596             information to be exported.
597              
598             Collectors might choose to behave differently in the presence of some
599             specifically-named option; typically to provide extra detail not normally
600             provided (maybe at the expense of extra processing time to calculate it).
601             Collectors must not complain about the presence of unrecognised options; the
602             hash is shared among all potential collectors.
603              
604             =cut
605              
606             =head1 TODO
607              
608             =over 8
609              
610             =item *
611              
612             Histogram/Summary 'start_timer' support
613              
614             =item *
615              
616             Add other C methods for other event systems and HTTP-serving
617             frameworks, e.g. L.
618              
619             =back
620              
621             =cut
622              
623             =head1 AUTHOR
624              
625             Paul Evans
626              
627             =cut
628              
629             0x55AA;