File Coverage

blib/lib/Net/Prometheus.pm
Criterion Covered Total %
statement 117 135 86.6
branch 12 16 75.0
condition 4 8 50.0
subroutine 29 33 87.8
pod 13 13 100.0
total 175 205 85.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-2020 -- leonerd@leonerd.org.uk
5              
6             package Net::Prometheus;
7              
8 9     9   563040 use strict;
  9         106  
  9         270  
9 9     9   49 use warnings;
  9         17  
  9         373  
10              
11             our $VERSION = '0.11';
12              
13 9     9   51 use Carp;
  9         18  
  9         644  
14              
15 9     9   58 use List::Util 1.29 qw( pairmap );
  9         269  
  9         1014  
16              
17 9     9   3924 use Net::Prometheus::Gauge;
  9         24  
  9         294  
18 9     9   4090 use Net::Prometheus::Counter;
  9         24  
  9         237  
19 9     9   3760 use Net::Prometheus::Summary;
  9         23  
  9         233  
20 9     9   3875 use Net::Prometheus::Histogram;
  9         22  
  9         253  
21              
22 9     9   3778 use Net::Prometheus::Registry;
  9         24  
  9         275  
23              
24 9     9   3651 use Net::Prometheus::ProcessCollector;
  9         25  
  9         281  
25 9     9   3715 use Net::Prometheus::PerlCollector;
  9         23  
  9         276  
26              
27 9     9   63 use Net::Prometheus::Types qw( MetricSamples );
  9         17  
  9         15731  
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 9     9 1 1384 my $class = shift;
118 9         61 my %args = @_;
119              
120 9         74 my $self = bless {
121             registry => Net::Prometheus::Registry->new,
122             }, $class;
123              
124 9 100 66     79 if( not $args{disable_process_collector} and
125             my $process_collector = Net::Prometheus::ProcessCollector->new ) {
126 4         24 $self->register( $process_collector );
127             }
128              
129 9 100       35 if( not $args{disable_perl_collector} ) {
130 4         101 $self->register( Net::Prometheus::PerlCollector->new );
131             }
132              
133 9         33 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 19     19 1 72 my $self = shift;
152 19         44 my ( $collector ) = @_;
153              
154 19         87 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 1119 my $self = shift;
168 2         6 my ( $collector ) = @_;
169              
170 2         16 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 5     5 1 15 my $self = shift;
185 5         20 my %args = @_;
186              
187 5         27 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 3 my $self = shift;
202 1         4 my %args = @_;
203              
204 1         10 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 577 my $self = shift;
273 2         6 my ( %args ) = @_;
274              
275 2         12 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 20     20 1 50 my $self = shift;
292 20         41 my ( $opts ) = @_;
293              
294 20   100     103 $opts //= {};
295              
296 20         35 my %samples_by_name;
297 20         90 foreach my $collector ( $self->{registry}->collectors, Net::Prometheus::Registry->collectors ) {
298 28         186 push @{ $samples_by_name{ $_->fullname } }, $_ for $collector->collect( $opts );
  93         866  
299             }
300              
301             return map {
302 20         233 my @results = @{ $samples_by_name{ $_ } };
  92         129  
  92         165  
303 92         119 my $first = $results[0];
304              
305             @results > 1 ?
306             MetricSamples( $first->fullname, $first->type, $first->help,
307 92 100       298 [ map { @{ $_->samples } } @results ]
  2         22  
  2         7  
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             =cut
326              
327             sub _render_label_value
328             {
329 56     56   87 my ( $v ) = @_;
330              
331 56         128 $v =~ s/(["\\])/\\$1/g;
332 56         89 $v =~ s/\n/\\n/g;
333              
334 56         258 return qq("$v");
335             }
336              
337             sub _render_labels
338             {
339 129     129   912 my ( $labels ) = @_;
340              
341 129 100       366 return "" if !scalar @$labels;
342              
343             return "{" .
344 55     56   280 join( ",", pairmap { $a . "=" . _render_label_value( $b ) } @$labels ) .
  56         136  
345             "}";
346             }
347              
348             sub render
349             {
350 19     19 1 3690 my $self = shift;
351 19         40 my ( $opts ) = @_;
352              
353             return join "", map {
354 19         65 my $metricsamples = $_;
  91         792  
355              
356 91         199 my $fullname = $metricsamples->fullname;
357              
358 91         473 my $help = $metricsamples->help;
359 91         426 $help =~ s/\\/\\\\/g;
360 91         138 $help =~ s/\n/\\n/g;
361              
362             "# HELP $fullname $help\n",
363             "# TYPE $fullname " . $metricsamples->type . "\n",
364             map {
365 129         710 my $sample = $_;
366 129         250 sprintf "%s%s %s\n",
367             $sample->varname,
368             _render_labels( $sample->labels ),
369             $sample->value
370 91         299 } @{ $metricsamples->samples }
  91         566  
371             } $self->collect( $opts );
372             }
373              
374             =head2 handle
375              
376             $response = $prometheus->handle( $request )
377              
378             Given an HTTP request in an L instance, renders the metrics in
379             response to it and returns an L instance.
380              
381             This application will respond to any C request, and reject requests for
382             any other method. If a query string is present on the URI it will be parsed
383             for collector options to pass into the L method.
384              
385             This method is useful for integrating metrics into an existing HTTP server
386             application which uses these objects. For example:
387              
388             my $prometheus = Net::Prometheus->new;
389              
390             sub serve_request
391             {
392             my ( $request ) = @_;
393              
394             if( $request->uri->path eq "/metrics" ) {
395             return $prometheus->handle( $request );
396             }
397              
398             ...
399             }
400              
401             =cut
402              
403             # Some handy pseudomethods to make working on HTTP::Response less painful
404             my $set_header = sub {
405             my $resp = shift;
406             $resp->header( @_ );
407             $resp;
408             };
409             my $set_content = sub {
410             my $resp = shift;
411             $resp->content( @_ );
412             $resp;
413             };
414             my $fix_content_length = sub {
415             my $resp = shift;
416             $resp->content_length or $resp->content_length( length $resp->content );
417             $resp;
418             };
419              
420             sub handle
421             {
422 1     1 1 8345 my $self = shift;
423 1         4 my ( $request ) = @_;
424              
425 1         550 require HTTP::Response;
426              
427 1 50       7063 $request->method eq "GET" or return
428             HTTP::Response->new( 405 )
429             ->$set_header( Content_Type => "text/plain" )
430             ->$set_content( "Method " . $request->method . " not supported" )
431             ->$fix_content_length;
432              
433 1         22 my $opts;
434 1 50       4 $opts = { $request->uri->query_form } if length $request->uri->query;
435              
436 1         101 return HTTP::Response->new( 200 )
437             ->$set_header( Content_Type => "text/plain; version=0.0.4; charset=utf-8" )
438             ->$set_content( $self->render( $opts ) )
439             ->$fix_content_length;
440             }
441              
442             =head2 psgi_app
443              
444             $app = $prometheus->psgi_app
445              
446             Returns a new L application as a C reference. This application
447             will render the metrics in the Prometheus text exposition format, suitable for
448             scraping by the Prometheus collector.
449              
450             This application will respond to any C request, and reject requests for
451             any other method. If a C is present in the environment it will
452             be parsed for collector options to pass into the L method.
453              
454             This method is useful for integrating metrics into an existing HTTP server
455             application which is uses or is based on PSGI. For example:
456              
457             use Plack::Builder;
458              
459             my $prometheus = Net::Prometheus::->new;
460              
461             builder {
462             mount "/metrics" => $prometheus->psgi_app;
463             ...
464             }
465              
466             =cut
467              
468             sub psgi_app
469             {
470 1     1 1 6 my $self = shift;
471              
472 1         635 require URI;
473              
474             return sub {
475 1     1   1275 my $env = shift;
476 1         4 my $method = $env->{REQUEST_METHOD};
477              
478 1 50       7 $method eq "GET" or return [
479             405,
480             [ "Content-Type" => "text/plain" ],
481             [ "Method $method not supported" ],
482             ];
483              
484 1         3 my $opts;
485 1 50       4 if( defined $env->{QUERY_STRING} ) {
486 0         0 $opts = +{ URI->new( "?$env->{QUERY_STRING}", "http" )->query_form };
487             }
488              
489             return [
490 1         6 200,
491             [ "Content-Type" => "text/plain; version=0.0.4; charset=utf-8" ],
492             [ $self->render( $opts ) ],
493             ];
494 1         4973 };
495             }
496              
497             =head2 export_to_IO_Async
498              
499             $prometheus->export_to_IO_Async( $loop, %args )
500              
501             Performs the necessary steps to create an HTTP server for exporting metrics
502             over HTTP via L. This will involve creating a new
503             L instance added to the loop.
504              
505             This new server will listen on its own port number for any incoming request,
506             and will serve metrics regardless of path.
507              
508             Note this should only be used in applications that don't otherwise have an
509             HTTP server, such as self-contained monitoring exporters or exporting metrics
510             as a side-effect of other activity. For existing HTTP server applications it
511             is better to integrate with the existing request/response processing of the
512             application, such as by using the L or L methods.
513              
514             Takes the following named arguments:
515              
516             =over 4
517              
518             =item port => INT
519              
520             Port number on which to listen for incoming HTTP requests.
521              
522             =back
523              
524             =cut
525              
526             sub export_to_IO_Async
527             {
528 0     0 1 0 my $self = shift;
529 0         0 my ( $loop, %args ) = @_;
530              
531 0         0 require IO::Async::Loop;
532 0         0 require Net::Async::HTTP::Server;
533              
534 0   0     0 $loop //= IO::Async::Loop->new;
535              
536             my $httpserver = Net::Async::HTTP::Server->new(
537             on_request => sub {
538 0     0   0 my $httpserver = shift;
539 0         0 my ( $req ) = @_;
540              
541 0         0 $req->respond( $self->handle( $req->as_http_request ) );
542             },
543 0         0 );
544              
545 0         0 $loop->add( $httpserver );
546              
547             # Yes this is a blocking call
548             $httpserver->listen(
549             socktype => "stream",
550             service => $args{port},
551 0         0 )->get;
552             }
553              
554             {
555             package
556             Net::Prometheus::_MetricGroup;
557              
558             sub new
559             {
560 2     2   4 my $class = shift;
561 2         6 my ( $prometheus, %args ) = @_;
562             return bless {
563             prometheus => $prometheus,
564             namespace => $args{namespace},
565             subsystem => $args{subsystem},
566 2         17 }, $class;
567             }
568              
569             foreach my $method (qw( new_gauge new_counter new_summary new_histogram )) {
570 9     9   77 no strict 'refs';
  9         17  
  9         1105  
571             *$method = sub {
572 2     2   5 my $self = shift;
573             $self->{prometheus}->$method(
574             namespace => $self->{namespace},
575             subsystem => $self->{subsystem},
576 2         11 @_,
577             );
578             };
579             }
580             }
581              
582             =head1 COLLECTORS
583              
584             The toplevel C object stores a list of "collector" instances,
585             which are used to generate the values that will be made visible via the
586             L method. A collector can be any object instance that has a method
587             called C, which when invoked is passed no arguments and expected to
588             return a list of L structures.
589              
590             @metricsamples = $collector->collect( $opts )
591              
592             The L class is already a valid collector (and hence,
593             so too are the individual metric type subclasses). This interface allows the
594             creation of new custom collector objects, that more directly collect
595             information to be exported.
596              
597             Collectors might choose to behave differently in the presence of some
598             specifically-named option; typically to provide extra detail not normally
599             provided (maybe at the expense of extra processing time to calculate it).
600             Collectors must not complain about the presence of unrecognised options; the
601             hash is shared among all potential collectors.
602              
603             =cut
604              
605             =head1 TODO
606              
607             =over 8
608              
609             =item *
610              
611             Histogram/Summary 'start_timer' support
612              
613             =item *
614              
615             Add other C methods for other event systems and HTTP-serving
616             frameworks, e.g. L.
617              
618             =back
619              
620             =cut
621              
622             =head1 AUTHOR
623              
624             Paul Evans
625              
626             =cut
627              
628             0x55AA;