File Coverage

blib/lib/Mojolicious/Plugin/PrometheusTiny.pm
Criterion Covered Total %
statement 67 68 98.5
branch 18 26 69.2
condition 12 15 80.0
subroutine 10 10 100.0
pod 1 1 100.0
total 108 120 90.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::PrometheusTiny;
2 7     7   6271 use Mojo::Base 'Mojolicious::Plugin', -signatures;
  7         19  
  7         55  
3 7     7   24257 use Time::HiRes qw/gettimeofday tv_interval/;
  7         28  
  7         68  
4 7     7   5059 use Prometheus::Tiny::Shared;
  7         91721  
  7         7960  
5              
6             our $VERSION = '0.01';
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 368 sub register($self, $app, $config = {}) {
  6         16  
  6         15  
  6         14  
  6         12  
14 6 100       26 if (my $update = $config->{update}) {
15 1         5 $self->$update($self->prometheus);
16             }
17 6   50     1710 $config->{worker_label} //= 1;
18 6   50     35 $config->{method_label} //= 1;
19 6     9   100 $app->helper(prometheus => sub {$self->prometheus});
  9         2209  
20 6         1025 $self->_setup($config);
21 6 100       155 if (my $setup = $config->{setup}) {
22 1         5 $self->setup_metrics($setup);
23 1         10 $self->$setup($self->prometheus);
24             }
25 6   66     98 my $prefix = $config->{route} // $app->routes->under('/');
26 6   100     2232 $self->route($prefix->get($config->{path} // '/metrics'));
27 6         2363 our $endpoint = $self->route->to_string; # Use direct string comparison instead of $route->match
28 7         46 $self->route->to(
29 7     7   25 cb => sub($c) {
  7         25392  
30 7 50       53 if (my $update = $self->update_metrics) {
31 0         0 $c->$update($self->prometheus);
32             }
33             $c->render(
34 7         103 text => $c->prometheus->format,
35             format => 'txt',
36             );
37 6         360 });
38 210         441 $app->hook(
39 210     210   364 before_dispatch => sub($c, @args) {
  210         1756170  
  210         337  
40 210 100       718 return if $c->req->url->path eq $endpoint;
41 203         17191 $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       4068 $config->{method_label} ? (method => $c->req->method) : (),
    50          
46             },
47             );
48             }
49 6         285 );
50 210         401 $app->hook(
51 210     210   314 after_render => sub($c, @args) {
  210         317312  
  210         412  
52 210 100       622 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       15694 $config->{method_label} ? (method => $c->req->method) : (),
    50          
57             },
58             );
59              
60             }
61 6         189 );
62 210         408 $app->hook(
63 210     210   327 after_dispatch => sub($c, @args) {
  210         324360  
  210         370  
64 210 100       584 return if $c->req->url->path eq $endpoint;
65             $self->prometheus->inc(http_requests_total => {
66 203 50       16042 $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       18364 $config->{method_label} ? (method => $c->req->method) : (),
    50          
74             code => $c->res->code,
75             },
76             );
77             }
78 6         231 );
79              
80             }
81              
82 6     6   15 sub _setup($self, $config) {
  6         14  
  6         13  
  6         16  
83 6         36 my $p = $self->prometheus();
84 6         7036 $p->declare('perl_info', type => 'gauge');
85 6         1508 $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     503 // [ 1 .. 10, 20, 30, 60, 120, 300, 600, 1_200, 3_600, 6_000, 12_000 ],
91             );
92 6         145 $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     131 // [ 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     177 // [ 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 'Prometheus' => { 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 => {route => $under};
138              
139             =head1 DESCRIPTION
140              
141             L is a L plugin that exports Prometheus metrics from Mojolicious.
142             It's based on L but uses L instead of L.
143              
144             Default hooks are installed to measure requests response time and count requests by HTTP return code,
145             with optional labeling of worker PID and HTTP method.
146             It is easy to add custom metrics and update them right before the metrics are exported.
147              
148             There is no support for namespaces, subsystems or any other fancy Net::Prometheus features.
149              
150             =head1 CODE QUALITY NOTICE
151              
152             This is BETA code which is still subject to change.
153              
154             =head1 HELPERS
155              
156             =head2 prometheus
157              
158             Create further instrumentation into your application by using this helper which gives access to the
159             L object.
160             See L for usage.
161              
162             =head1 METHODS
163              
164             L inherits all methods from
165             L and implements the following new ones.
166              
167             =head2 register
168              
169             $plugin->register($app, \%config);
170              
171             Register plugin in L application.
172              
173             C<%config> can have:
174              
175             =over 2
176              
177             =item * route
178              
179             L object to attach the metrics to, defaults to generating a new one for '/'.
180              
181             Default: /
182              
183             =item * path
184              
185             The path to mount the exporter.
186              
187             Default: /metrics
188              
189             =item * prometheus
190              
191             Override the L object.
192             The default is a new singleton instance of L.
193              
194             =item * request_buckets
195              
196             Override buckets for request sizes histogram.
197              
198             Default: C<[ 1, 10, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000 ]>
199              
200             =item * response_buckets
201              
202             Override buckets for response sizes histogram.
203              
204             Default: C<[ 1, 10, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000 ]>
205              
206             =item * duration_buckets
207              
208             Override buckets for request duration histogram.
209              
210             Default: C<[1..10, 20, 30, 60, 120, 300, 600, 1_200, 3_600, 6_000, 12_000]>
211              
212             =item * worker_label
213              
214             Label metrics by worker PID, which might increase significantly the number of Prometheus time series.
215              
216             Default: true
217              
218             =item * method_label
219              
220             Label metrics by HTTP method, which might increase significantly the number of Prometheus time series.
221              
222             Default: true
223              
224             =item * setup
225              
226             Coderef to be executed during setup. Receives as arguments Application and Prometheus instances.
227             It can be used to declare and/or initialize new metrics.
228              
229             setup => sub($app, $p) {
230             $p->declare('mojo_random',
231             type => 'gauge',
232             help => "Custom prometheus gauge"
233             );
234             }
235              
236             =item * update
237              
238             Coderef to be executed right before invoking exporter action configured in C.
239             Receives as arguments Controller and Prometheus instances.
240              
241             update => sub($c, $p) {
242             $p->set(mojo_random => rand(100));
243             }
244              
245             =back
246              
247             =head1 METRICS
248              
249             This plugin exposes
250              
251             =over 2
252              
253             =item * C, request counter partitioned over HTTP method and HTTP response code
254              
255             =item * C, request duration histogram partitioned over HTTP method
256              
257             =item * C, request size histogram partitioned over HTTP method
258              
259             =item * C, response size histogram partitioned over HTTP method
260              
261             =back
262              
263             =head1 TO DO
264              
265             =over 2
266              
267             =item * Add optional L-like process metrics.
268              
269             =back
270              
271             =head1 AUTHOR
272              
273             Javier Arturo Rodriguez
274              
275             A significant part of this code has been ripped off L written by Vidar Tyldum
276              
277             =head1 COPYRIGHT AND LICENSE
278              
279             Copyright (c) 2023 by Javier Arturo Rodriguez.
280              
281             This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.
282              
283             =head1 SEE ALSO
284              
285             L, L, L,
286             L, L, L.
287              
288             =cut