File Coverage

blib/lib/Net/Prometheus.pm
Criterion Covered Total %
statement 118 143 82.5
branch 16 24 66.6
condition 4 8 50.0
subroutine 29 34 85.2
pod 14 14 100.0
total 181 223 81.1


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