File Coverage

blib/lib/Mojolicious/Plugin/Prometheus.pm
Criterion Covered Total %
statement 74 77 96.1
branch 4 8 50.0
condition 25 34 73.5
subroutine 23 24 95.8
pod 1 1 100.0
total 127 144 88.1


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Prometheus;
2 9     9   7695 use Mojo::Base 'Mojolicious::Plugin';
  9         185671  
  9         67  
3 9     9   2703 use Time::HiRes qw/gettimeofday tv_interval/;
  9         22  
  9         74  
4 9     9   6238 use Net::Prometheus;
  9         139228  
  9         263  
5 9     9   4486 use IPC::ShareLite;
  9         28529  
  9         12534  
6              
7             our $VERSION = '1.3.1';
8              
9             has prometheus => sub { Net::Prometheus->new(disable_process_collector => 1) };
10             has route => sub {undef};
11             has http_request_duration_seconds => sub {
12               undef;
13             };
14             has http_request_size_bytes => sub {
15               undef;
16             };
17             has http_response_size_bytes => sub {
18               undef;
19             };
20             has http_requests_total => sub {
21               undef;
22             };
23              
24              
25             sub register {
26 8     8 1 424   my ($self, $app, $config) = @_;
27              
28 8   50     94   $self->{key} = $config->{shm_key} || '12345';
29              
30 8     215   90   $app->helper(prometheus => sub { $self->prometheus });
  215         19861  
31              
32             # Only the two built-in servers are supported for now
33 8     16   1215   $app->hook(before_server_start => sub { $self->_start(@_, $config) });
  16         27179  
34              
35               $self->http_request_duration_seconds(
36                 $self->prometheus->new_histogram(
37                   namespace => $config->{namespace} // undef,
38                   subsystem => $config->{subsystem} // undef,
39                   name => "http_request_duration_seconds",
40                   help => "Histogram with request processing time",
41                   labels => [qw/worker method/],
42                   buckets => $config->{duration_buckets} // undef,
43                 )
44 8   100     259   );
      50        
      50        
45              
46               $self->http_request_size_bytes(
47                 $self->prometheus->new_histogram(
48                   namespace => $config->{namespace} // undef,
49                   subsystem => $config->{subsystem} // undef,
50                   name => "http_request_size_bytes",
51                   help => "Histogram containing request sizes",
52                   labels => [qw/worker method/],
53                   buckets => $config->{request_buckets}
54 8   100     1359         // [(1, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000)],
      50        
      100        
55                 )
56               );
57              
58               $self->http_response_size_bytes(
59                 $self->prometheus->new_histogram(
60                   namespace => $config->{namespace} // undef,
61                   subsystem => $config->{subsystem} // undef,
62                   name => "http_response_size_bytes",
63                   help => "Histogram containing response sizes",
64                   labels => [qw/worker method code/],
65                   buckets => $config->{response_buckets}
66 8   100     1017         // [(5, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000)],
      50        
      100        
67                 )
68               );
69              
70               $self->http_requests_total(
71                 $self->prometheus->new_counter(
72                   namespace => $config->{namespace} // undef,
73                   subsystem => $config->{subsystem} // undef,
74 8   100     954       name => "http_requests_total",
      50        
75                   help =>
76                     "How many HTTP requests processed, partitioned by status code and HTTP method.",
77                   labels => [qw/worker method code/]
78                 )
79               );
80              
81             # Collect stats
82               $app->hook(
83                 after_render => sub {
84 214     214   189797       my ($c) = @_;
85                   $self->_guard->_change(
86                     sub {
87 214         833           $_->{$$} = $app->prometheus->render;
88                     }
89 214         735       );
90              
91             #$self->_guard->_store({$$ => $app->prometheus->render});
92                 }
93 8         800   );
94              
95               $app->hook(
96                 before_dispatch => sub {
97 214     214   1605149       my ($c) = @_;
98 214         1462       $c->stash('prometheus.start_time' => [gettimeofday]);
99 214         4251       $self->http_request_size_bytes->observe($$, $c->req->method,
100                     $c->req->content->body_size);
101                 }
102 8         180   );
103              
104               $app->hook(
105                 after_render => sub {
106 214     214   5071       my ($c) = @_;
107 214         806       $self->http_request_duration_seconds->observe($$, $c->req->method,
108                     tv_interval($c->stash('prometheus.start_time')));
109                 }
110 8         189   );
111              
112               $app->hook(
113                 after_dispatch => sub {
114 214     214   82563       my ($c) = @_;
115 214         737       $self->http_requests_total->inc($$, $c->req->method, $c->res->code);
116 214         22389       $self->http_response_size_bytes->observe($$, $c->req->method,
117                     $c->res->code, $c->res->content->body_size);
118                 }
119 8         126   );
120              
121              
122 8   66     165   my $prefix = $config->{route} // $app->routes->under('/');
123 8   100     2628   $self->route($prefix->get($config->{path} // '/metrics'));
124               $self->route->to(
125                 cb => sub {
126 10     10   31223       my ($c) = @_;
127                   $c->render(
128                     text => join("\n",
129 10         53           map { ($self->_guard->_fetch->{$_}) }
130 10         29           sort keys %{$self->_guard->_fetch}),
  10         46  
131                     format => 'txt'
132                   );
133                 }
134 8         2549   );
135              
136             }
137              
138             sub _guard {
139 250     250   462   my $self = shift;
140              
141               my $share = $self->{share}
142 250   50     950     ||= IPC::ShareLite->new(-key => $self->{key}, -create => 1, -destroy => 0)
      66        
143                 || die $!;
144              
145 250         3770   return Mojolicious::Plugin::Mojolicious::_Guard->new(share => $share);
146             }
147              
148             sub _start {
149              
150             #my ($self, $app, $config) = @_;
151 16     16   46   my ($self, $server, $app, $config) = @_;
152 16 50       122   return unless $server->isa('Mojo::Server::Daemon');
153              
154               Mojo::IOLoop->next_tick(
155                 sub {
156 16     16   18816       my $pc = Net::Prometheus::ProcessCollector->new(labels => [worker => $$]);
157 16 50       14518       $self->prometheus->register($pc) if $pc;
158 16         256       $self->_guard->_store({$$ => $self->prometheus->render});
159                 }
160 16         151   );
161              
162             # Remove stopped workers
163               $server->on(
164                 reap => sub {
165 0     0   0       my ($server, $pid) = @_;
166 0         0       $self->_guard->_change(sub { delete $_->{$pid} });
  0         0  
167                 }
168 16 50       1191   ) if $server->isa('Mojo::Server::Prefork');
169             }
170              
171              
172             package Mojolicious::Plugin::Mojolicious::_Guard;
173 9     9   85 use Mojo::Base -base;
  9         23  
  9         73  
174              
175 9     9   1384 use Fcntl ':flock';
  9         21  
  9         1213  
176 9     9   4283 use Sereal qw(get_sereal_decoder get_sereal_encoder);
  9         8077  
  9         2741  
177              
178             my ($DECODER, $ENCODER) = (get_sereal_decoder, get_sereal_encoder);
179              
180 250     250   6924 sub DESTROY { shift->{share}->unlock }
181              
182             sub new {
183 250     250   831   my $self = shift->SUPER::new(@_);
184 250         2460   $self->{share}->lock(LOCK_EX);
185 250         6379   return $self;
186             }
187              
188             sub _change {
189 214     214   516   my ($self, $cb) = @_;
190 214         514   my $stats = $self->_fetch;
191 214         755   $cb->($_) for $stats;
192 214         1590604   $self->_store($stats);
193             }
194              
195             sub _fetch {
196 234 50   234   760   return {} unless my $data = shift->{share}->fetch;
197 234         6323   return $DECODER->decode($data);
198             }
199              
200 230     230   33784 sub _store { shift->{share}->store($ENCODER->encode(shift)) }
201              
202             1;
203             __END__
204            
205             =for stopwords prometheus
206            
207             =encoding utf8
208            
209             =head1 NAME
210            
211             Mojolicious::Plugin::Prometheus - Mojolicious Plugin
212            
213             =head1 SYNOPSIS
214            
215             # Mojolicious
216             $self->plugin('Prometheus');
217            
218             # Mojolicious::Lite
219             plugin 'Prometheus';
220            
221             # Mojolicious::Lite, with custom response buckets (seconds)
222             plugin 'Prometheus' => { response_buckets => [qw/4 5 6/] };
223            
224             # You can add your own route to do access control
225             my $under = app->routes->under('/secret' =>sub {
226             my $c = shift;
227             return 1 if $c->req->url->to_abs->userinfo eq 'Bender:rocks';
228             $c->res->headers->www_authenticate('Basic');
229             $c->render(text => 'Authentication required!', status => 401);
230             return undef;
231             });
232             plugin Prometheus => {route => $under};
233            
234             =head1 DESCRIPTION
235            
236             L<Mojolicious::Plugin::Prometheus> is a L<Mojolicious> plugin that exports Prometheus metrics from Mojolicious.
237            
238             Hooks are also installed to measure requests response time and count requests based on method and HTTP return code.
239            
240             =head1 HELPERS
241            
242             =head2 prometheus
243            
244             Create further instrumentation into your application by using this helper which gives access to the L<Net::Prometheus> object.
245             See L<Net::Prometheus> for usage.
246            
247             =head1 METHODS
248            
249             L<Mojolicious::Plugin::Prometheus> inherits all methods from
250             L<Mojolicious::Plugin> and implements the following new ones.
251            
252             =head2 register
253            
254             $plugin->register($app, \%config);
255            
256             Register plugin in L<Mojolicious> application.
257            
258             C<%config> can have:
259            
260             =over 2
261            
262             =item * route
263            
264             L<Mojolicious::Routes::Route> object to attach the metrics to, defaults to generating a new one for '/'.
265            
266             Default: /
267            
268             =item * path
269            
270             The path to mount the exporter.
271            
272             Default: /metrics
273            
274             =item * prometheus
275            
276             Override the L<Net::Prometheus> object. The default is a new singleton instance of L<Net::Prometheus>.
277            
278             =item * namespace, subsystem
279            
280             These will be prefixed to the metrics exported.
281            
282             =item * request_buckets
283            
284             Override buckets for request sizes histogram.
285            
286             Default: C<(1, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000)>
287            
288             =item * response_buckets
289            
290             Override buckets for response sizes histogram.
291            
292             Default: C<(5, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000)>
293            
294             =item * duration_buckets
295            
296             Override buckets for request duration histogram.
297            
298             Default: C<(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10)> (actually see L<Net::Prometheus|https://metacpan.org/source/PEVANS/Net-Prometheus-0.05/lib/Net/Prometheus/Histogram.pm#L19>)
299            
300             =item * shm_key
301            
302             Key used for shared memory access between workers, see L<$key in IPc::ShareLite|https://metacpan.org/pod/IPC::ShareLite> for details.
303            
304             =back
305            
306             =head1 METRICS
307            
308             In addition to exposing the default process metrics that L<Net::Prometheus> already expose
309             this plugin will also expose
310            
311             =over 2
312            
313             =item * C<http_requests_total>, request counter partitioned over HTTP method and HTTP response code
314            
315             =item * C<http_request_duration_seconds>, request duration histogram partitioned over HTTP method
316            
317             =item * C<http_request_size_bytes>, request size histogram partitioned over HTTP method
318            
319             =item * C<http_response_size_bytes>, response size histogram partitioned over HTTP method
320            
321             =back
322            
323             =head1 AUTHOR
324            
325             Vidar Tyldum
326            
327             (the IPC::ShareLite parts of this code is shamelessly stolen from L<Mojolicious::Plugin::Status> written by Sebastian Riedel and mangled into something that works for me)
328            
329             =head1 COPYRIGHT AND LICENSE
330            
331             Copyright (C) 2018, Vidar Tyldum
332            
333             This program is free software, you can redistribute it and/or modify it under
334             the terms of the Artistic License version 2.0.
335            
336             =head1 SEE ALSO
337            
338             =over 2
339            
340             =item L<Net::Prometheus>
341            
342             =item L<Mojolicious::Plugin::Status>
343            
344             =item L<Mojolicious>
345            
346             =item L<Mojolicious::Guides>
347            
348             =item L<http://mojolicious.org>
349            
350             =back
351            
352             =cut
353