File Coverage

blib/lib/Mojolicious/Plugin/PrometheusTiny.pm
Criterion Covered Total %
statement 68 68 100.0
branch 19 26 73.0
condition 12 15 80.0
subroutine 10 10 100.0
pod 1 1 100.0
total 110 120 91.6


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::PrometheusTiny;
2 7     7   6501 use Mojo::Base 'Mojolicious::Plugin', -signatures;
  7         20  
  7         69  
3 7     7   24148 use Time::HiRes qw/gettimeofday tv_interval/;
  7         16  
  7         72  
4 7     7   5048 use Prometheus::Tiny::Shared;
  7         90413  
  7         7921  
5              
6             our $VERSION = '0.02';
7              
8             has prometheus => sub {Prometheus::Tiny::Shared->new()};
9             has route => undef;
10             has setup_metrics => undef;
11             has update_metrics => undef;
12              
13 6     6 1 320 sub register($self, $app, $config = {}) {
  6         19  
  6         12  
  6         15  
  6         10  
14 6 100       27 if (my $update = $config->{update}) {
15 1         13 $self->update_metrics($update);
16             }
17 6   50     66 $config->{worker_label} //= 1;
18 6   50     38 $config->{method_label} //= 1;
19 6     9   80 $app->helper(prometheus => sub {$self->prometheus});
  9         1938  
20 6         981 $self->_setup($config);
21 6 100       150 if (my $setup = $config->{setup}) {
22 1         7 $self->setup_metrics($setup);
23 1         9 $app->$setup($self->prometheus);
24             }
25 6   66     150 my $prefix = $config->{route} // $app->routes->under('/');
26 6   100     2370 $self->route($prefix->get($config->{path} // '/metrics'));
27 6         2269 our $endpoint = $self->route->to_string; # Use direct string comparison instead of $route->match
28 7         20 $self->route->to(
29 7     7   23 cb => sub($c) {
  7         25273  
30 7 100       38 if (my $update = $self->update_metrics) {
31 2         13 $c->$update($self->prometheus);
32             }
33             $c->render(
34 7         317 text => $c->prometheus->format,
35             format => 'txt',
36             );
37 6         302 });
38 210         488 $app->hook(
39 210     210   395 before_dispatch => sub($c, @args) {
  210         1770617  
  210         369  
40 210 100       791 return if $c->req->url->path eq $endpoint;
41 203         17226 $c->stash('prometheus.start_time' => [ gettimeofday ]);
42             $self->prometheus->histogram_observe(
43             http_request_size_bytes => $c->req->content->body_size => {
44             $config->{worker_label} ? (worker => $$) : (),
45 203 50       4084 $config->{method_label} ? (method => $c->req->method) : (),
    50          
46             },
47             );
48             }
49 6         312 );
50 210         443 $app->hook(
51 210     210   329 after_render => sub($c, @args) {
  210         321290  
  210         478  
52 210 100       660 return if $c->req->url->path eq $endpoint;
53             $self->prometheus->histogram_observe(
54             http_request_duration_seconds => tv_interval($c->stash('prometheus.start_time')) => {
55             $config->{worker_label} ? (worker => $$) : (),
56 203 50       15738 $config->{method_label} ? (method => $c->req->method) : (),
    50          
57             },
58             );
59              
60             }
61 6         173 );
62 210         461 $app->hook(
63 210     210   350 after_dispatch => sub($c, @args) {
  210         330955  
  210         388  
64 210 100       606 return if $c->req->url->path eq $endpoint;
65             $self->prometheus->inc(http_requests_total => {
66 203 50       16197 $config->{worker_label} ? (worker => $$) : (),
67             method => $c->req->method,
68             code => $c->res->code,
69             });
70             $self->prometheus->histogram_observe(
71             http_response_size_bytes => $c->res->body_size => {
72             $config->{worker_label} ? (worker => $$) : (),
73 203 50       18733 $config->{method_label} ? (method => $c->req->method) : (),
    50          
74             code => $c->res->code,
75             },
76             );
77             }
78 6         115 );
79              
80             }
81              
82 6     6   13 sub _setup($self, $config) {
  6         12  
  6         17  
  6         12  
83 6         23 my $p = $self->prometheus();
84 6         8490 $p->declare('perl_info', type => 'gauge');
85 6         1762 $p->set(perl_info => 1, { version => $^V });
86             $p->declare('http_request_duration_seconds',
87             help => 'Histogram with request processing time',
88             type => 'histogram',
89             buckets => $config->{duration_buckets}
90 6   100     567 // [ 1 .. 10, 20, 30, 60, 120, 300, 600, 1_200, 3_600, 6_000, 12_000 ],
91             );
92 6         168 $p->declare('http_requests_total',
93             help => 'How many HTTP requests processed, partitioned by status code and HTTP method',
94             type => 'counter',
95             );
96             $p->declare('http_request_size_bytes',
97             help => 'Histogram containing request sizes',
98             type => 'histogram',
99             buckets => $config->{request_buckets}
100 6   100     163 // [ 1, 10, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000 ],
101             );
102             $p->declare('http_response_size_bytes',
103             help => 'Histogram containing response sizes',
104             type => 'histogram',
105             buckets => $config->{response_buckets}
106 6   100     165 // [ 1, 10, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000 ],
107             );
108             }
109              
110             1;
111              
112             =encoding utf8
113              
114             =head1 NAME
115              
116             Mojolicious::Plugin::PrometheusTiny - Export metrics using Prometheus::Tiny::Shared
117              
118             =head1 SYNOPSIS
119              
120             # Mojolicious
121             $self->plugin('PrometheusTiny');
122              
123             # Mojolicious::Lite
124             plugin 'PrometheusTiny';
125              
126             # Mojolicious::Lite, with custom response buckets (seconds)
127             plugin 'PrometheusTiny' => { response_buckets => [qw/4 5 6/] };
128              
129             # You can add your own route to do access control
130             my $under = app->routes->under('/secret' =>sub {
131             my $c = shift;
132             return 1 if $c->req->url->to_abs->userinfo eq 'Bender:rocks';
133             $c->res->headers->www_authenticate('Basic');
134             $c->render(text => 'Authentication required!', status => 401);
135             return undef;
136             });
137             plugin PrometheusTiny => {
138             route => $under,
139             # You may declare additional metrics with their own TYPE and HELP...
140             setup => sub($app, $p) {
141             $p->declare('mojo_random',
142             type => 'gauge',
143             help => "Custom prometheus gauge"
144             );
145             },
146             # ...and set up a callback to update them right before exporting them
147             update => sub($c, $p) {
148             $p->set(mojo_random => rand(100));
149             },
150             };
151              
152             =head1 DESCRIPTION
153              
154             L is a L plugin that exports Prometheus metrics from Mojolicious.
155             It's based on L but uses L instead of L.
156              
157             Default hooks are installed to measure requests response time and count requests by HTTP return code,
158             with optional labeling of worker PID and HTTP method.
159             It is easy to add custom metrics and update them right before the metrics are exported.
160              
161             There is no support for namespaces, subsystems or any other fancy Net::Prometheus features.
162              
163             =head1 CODE QUALITY NOTICE
164              
165             This is BETA code =head1 HELPERS
166              
167             =head2 prometheus
168              
169             Create further instrumentation into your application by using this helper which gives access to the
170             L object.
171             See L for usage.
172              
173             =head1 METHODS
174              
175             L inherits all methods from
176             L and implements the following new ones.
177              
178             =head2 register
179              
180             $plugin->register($app, \%config);
181              
182             Register plugin in L application.
183              
184             C<%config> can have:
185              
186             =over 2
187              
188             =item * route
189              
190             L object to attach the metrics to, defaults to generating a new one for '/'.
191              
192             Default: /
193              
194             =item * path
195              
196             The path to mount the exporter.
197              
198             Default: /metrics
199              
200             =item * prometheus
201              
202             Override the L object.
203             The default is a new singleton instance of L.
204              
205             =item * request_buckets
206              
207             Override buckets for request sizes histogram.
208              
209             Default: C<[ 1, 10, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000 ]>
210              
211             =item * response_buckets
212              
213             Override buckets for response sizes histogram.
214              
215             Default: C<[ 1, 10, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000 ]>
216              
217             =item * duration_buckets
218              
219             Override buckets for request duration setup => sub($app, $p) {
220             $p->declare('mojo_random',
221             type => 'gauge',
222             help => "Custom prometheus gauge"
223             );
224             }histogram.
225              
226             Default: C<[1..10, 20, 30, 60, 120, 300, 600, 1_200, 3_600, 6_000, 12_000]>
227              
228             =item * worker_label
229              
230             Label metrics by worker PID, which might increase significantly the number of Prometheus time series.
231              
232             Default: true
233              
234             =item * method_label
235              
236             Label metrics by HTTP method, which might increase significantly the number of Prometheus time series.
237              
238             Default: true
239              
240             =item * setup
241              
242             Coderef to be executed during setup. Receives as arguments Application and Prometheus instances.
243             Can be used to declare and/or initialize new metrics. Though it is trivial to use $app->prometheus
244             to declare metrics after plugin setup, code is more readable and easier to maintain
245             when actions are listed in their natural order.
246              
247             =item * update
248              
249             Coderef to be executed right before invoking exporter action configured in C.
250             Receives as arguments Controller and Prometheus instances.
251              
252             =back
253              
254             =head1 METRICS
255              
256             This plugin exposes
257              
258             =over 2
259              
260             =item * C, request counter partitioned over HTTP method and HTTP response code
261              
262             =item * C, request duration histogram partitioned over HTTP method
263              
264             =item * C, request size histogram partitioned over HTTP method
265              
266             =item * C, response size histogram partitioned over HTTP method
267              
268             =back
269              
270             =head1 TO DO
271              
272             =over 2
273              
274             =item * Add optional L-like process metrics.
275              
276             =back
277              
278             =head1 AUTHOR
279              
280             Javier Arturo Rodriguez
281              
282             A significant part of this code has been ripped off L written by Vidar Tyldum
283              
284             =head1 COPYRIGHT AND LICENSE
285              
286             Copyright (c) 2023 by Javier Arturo Rodriguez.
287              
288             This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.
289              
290             =head1 SEE ALSO
291              
292             L, L, L,
293             L, L, L.
294              
295             =cut