File Coverage

blib/lib/Net/Prometheus.pm
Criterion Covered Total %
statement 154 190 81.0
branch 16 24 66.6
condition 4 8 50.0
subroutine 35 40 87.5
pod 14 14 100.0
total 223 276 80.8


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