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   7879 use Mojo::Base 'Mojolicious::Plugin';
  9         188874  
  9         67  
3 9     9   2682 use Time::HiRes qw/gettimeofday tv_interval/;
  9         21  
  9         61  
4 9     9   6340 use Net::Prometheus;
  9         141587  
  9         278  
5 9     9   4637 use IPC::ShareLite;
  9         28736  
  9         12389  
6              
7             our $VERSION = '1.3.0';
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 483   my ($self, $app, $config) = @_;
27              
28 8   50     103   $self->{key} = $config->{shm_key} || '12345';
29              
30 8     215   97   $app->helper(prometheus => sub { $self->prometheus });
  215         20042  
31              
32             # Only the two built-in servers are supported for now
33 8     16   1244   $app->hook(before_server_start => sub { $self->_start(@_, $config) });
  16         27908  
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     212   );
      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     1453         // [(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     1047         // [(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     965       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   187063       my ($c) = @_;
85                   $self->_guard->_change(
86                     sub {
87 214         796           $_->{$$} = $app->prometheus->render;
88                     }
89 214         735       );
90              
91             #$self->_guard->_store({$$ => $app->prometheus->render});
92                 }
93 8         779   );
94              
95               $app->hook(
96                 before_dispatch => sub {
97 214     214   1666108       my ($c) = @_;
98 214         1371       $c->stash('prometheus.start_time' => [gettimeofday]);
99 214         4303       $self->http_request_size_bytes->observe($$, $c->req->method,
100                     $c->req->content->body_size);
101                 }
102 8         153   );
103              
104               $app->hook(
105                 after_render => sub {
106 214     214   4965       my ($c) = @_;
107 214         789       $self->http_request_duration_seconds->observe($$, $c->req->method,
108                     tv_interval($c->stash('prometheus.start_time')));
109                 }
110 8         202   );
111              
112               $app->hook(
113                 after_dispatch => sub {
114 214     214   81388       my ($c) = @_;
115 214         670       $self->http_requests_total->inc($$, $c->req->method, $c->res->code);
116 214         22378       $self->http_response_size_bytes->observe($$, $c->req->method,
117                     $c->res->code, $c->res->content->body_size);
118                 }
119 8         152   );
120              
121              
122 8   66     154   my $prefix = $config->{route} // $app->routes->under('/');
123 8   100     2641   $self->route($prefix->get($config->{path} // '/metrics'));
124               $self->route->to(
125                 cb => sub {
126 10     10   31814       my ($c) = @_;
127                   $c->render(
128                     text => join("\n",
129 10         54           map { ($self->_guard->_fetch->{$_}) }
130 10         33           sort keys %{$self->_guard->_fetch}),
  10         40  
131                     format => 'txt'
132                   );
133                 }
134 8         2673   );
135              
136             }
137              
138             sub _guard {
139 250     250   499   my $self = shift;
140              
141               my $share = $self->{share}
142 250   50     859     ||= IPC::ShareLite->new(-key => $self->{key}, -create => 1, -destroy => 0)
      66        
143                 || die $!;
144              
145 250         3727   return Mojolicious::Plugin::Mojolicious::_Guard->new(share => $share);
146             }
147              
148             sub _start {
149              
150             #my ($self, $app, $config) = @_;
151 16     16   45   my ($self, $server, $app, $config) = @_;
152 16 50       125   return unless $server->isa('Mojo::Server::Daemon');
153              
154               Mojo::IOLoop->next_tick(
155                 sub {
156 16     16   19289       my $pc = Net::Prometheus::ProcessCollector->new(labels => [worker => $$]);
157 16 50       15239       $self->prometheus->register($pc) if $pc;
158 16         255       $self->_guard->_store({$$ => $self->prometheus->render});
159                 }
160 16         147   );
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       1267   ) if $server->isa('Mojo::Server::Prefork');
169             }
170              
171              
172             package Mojolicious::Plugin::Mojolicious::_Guard;
173 9     9   90 use Mojo::Base -base;
  9         23  
  9         73  
174              
175 9     9   1388 use Fcntl ':flock';
  9         22  
  9         1601  
176 9     9   4705 use Sereal qw(get_sereal_decoder get_sereal_encoder);
  9         8207  
  9         2865  
177              
178             my ($DECODER, $ENCODER) = (get_sereal_decoder, get_sereal_encoder);
179              
180 250     250   6345 sub DESTROY { shift->{share}->unlock }
181              
182             sub new {
183 250     250   880   my $self = shift->SUPER::new(@_);
184 250         2350   $self->{share}->lock(LOCK_EX);
185 250         6134   return $self;
186             }
187              
188             sub _change {
189 214     214   541   my ($self, $cb) = @_;
190 214         526   my $stats = $self->_fetch;
191 214         740   $cb->($_) for $stats;
192 214         1594107   $self->_store($stats);
193             }
194              
195             sub _fetch {
196 234 50   234   740   return {} unless my $data = shift->{share}->fetch;
197 234         6074   return $DECODER->decode($data);
198             }
199              
200 230     230   33407 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             =head1 DESCRIPTION
225            
226             L<Mojolicious::Plugin::Prometheus> is a L<Mojolicious> plugin that exports Prometheus metrics from Mojolicious.
227            
228             Hooks are also installed to measure requests response time and count requests based on method and HTTP return code.
229            
230             =head1 HELPERS
231            
232             =head2 prometheus
233            
234             Create further instrumentation into your application by using this helper which gives access to the L<Net::Prometheus> object.
235             See L<Net::Prometheus> for usage.
236            
237             =head1 METHODS
238            
239             L<Mojolicious::Plugin::Prometheus> inherits all methods from
240             L<Mojolicious::Plugin> and implements the following new ones.
241            
242             =head2 register
243            
244             $plugin->register($app, \%config);
245            
246             Register plugin in L<Mojolicious> application.
247            
248             C<%config> can have:
249            
250             =over 2
251            
252             =item * path
253            
254             The path to mount the exporter.
255            
256             Default: /metrics
257            
258             =item * prometheus
259            
260             Override the L<Net::Prometheus> object. The default is a new singleton instance of L<Net::Prometheus>.
261            
262             =item * namespace, subsystem
263            
264             These will be prefixed to the metrics exported.
265            
266             =item * request_buckets
267            
268             Override buckets for request sizes histogram.
269            
270             Default: C<(1, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000)>
271            
272             =item * response_buckets
273            
274             Override buckets for response sizes histogram.
275            
276             Default: C<(5, 50, 100, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000)>
277            
278             =item * duration_buckets
279            
280             Override buckets for request duration histogram.
281            
282             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>)
283            
284             =item * shm_key
285            
286             Key used for shared memory access between workers, see L<$key in IPc::ShareLite|https://metacpan.org/pod/IPC::ShareLite> for details.
287            
288             =back
289            
290             =head1 METRICS
291            
292             In addition to exposing the default process metrics that L<Net::Prometheus> already expose
293             this plugin will also expose
294            
295             =over 2
296            
297             =item * C<http_requests_total>, request counter partitioned over HTTP method and HTTP response code
298            
299             =item * C<http_request_duration_seconds>, request duration histogram partitioned over HTTP method
300            
301             =item * C<http_request_size_bytes>, request size histogram partitioned over HTTP method
302            
303             =item * C<http_response_size_bytes>, response size histogram partitioned over HTTP method
304            
305             =back
306            
307             =head1 AUTHOR
308            
309             Vidar Tyldum
310            
311             (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)
312            
313             =head1 COPYRIGHT AND LICENSE
314            
315             Copyright (C) 2018, Vidar Tyldum
316            
317             This program is free software, you can redistribute it and/or modify it under
318             the terms of the Artistic License version 2.0.
319            
320             =head1 SEE ALSO
321            
322             =over 2
323            
324             =item L<Net::Prometheus>
325            
326             =item L<Mojolicious::Plugin::Status>
327            
328             =item L<Mojolicious>
329            
330             =item L<Mojolicious::Guides>
331            
332             =item L<http://mojolicious.org>
333            
334             =back
335            
336             =cut
337