File Coverage

blib/lib/Mojolicious/Plugin/Status.pm
Criterion Covered Total %
statement 166 176 94.3
branch 29 50 58.0
condition 4 9 44.4
subroutine 34 36 94.4
pod 1 1 100.0
total 234 272 86.0


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Status;
2 1     1   1335 use Mojo::Base 'Mojolicious::Plugin';
  1         3  
  1         9  
3              
4 1     1   916 use BSD::Resource qw(getrusage);
  1         2216  
  1         6  
5 1     1   113 use Time::HiRes qw(time);
  1         2  
  1         8  
6 1     1   131 use Mojo::File qw(path);
  1         2  
  1         46  
7 1     1   5 use Mojo::IOLoop;
  1         2  
  1         12  
8 1     1   502 use Mojo::MemoryMap;
  1         5  
  1         12  
9 1     1   41 use Mojo::Util qw(humanize_bytes);
  1         2  
  1         64  
10              
11 1     1   6 use constant MACOS => $^O eq 'darwin';
  1         4  
  1         3537  
12              
13             our $VERSION = '1.17';
14              
15             sub register {
16 1     1 1 56 my ($self, $app, $config) = @_;
17              
18             # Config
19 1   33     6 my $prefix = $config->{route} // $app->routes->any('/mojo-status');
20 1   50     9 $prefix->to(return_to => $config->{return_to} // '/');
21 1   50     49 $self->{slowest} = $config->{slowest} // 10;
22              
23             # Initialize cache
24 1         5 my $map = $self->{map} = Mojo::MemoryMap->new($config->{size});
25 1         11 $map->writer->store({processed => 0, started => time, stats => _stats(), slowest => []});
26              
27             # Only the two built-in servers are supported for now
28 1     2   9 $app->hook(before_server_start => sub { $self->_start(@_) });
  2         7475  
29              
30             # Static files
31 1         50 my $resources = path(__FILE__)->sibling('resources');
32 1         211 push @{$app->static->paths}, $resources->child('public')->to_string;
  1         10  
33              
34             # Templates
35 1         78 push @{$app->renderer->paths}, $resources->child('templates')->to_string;
  1         8  
36              
37             # Routes
38 1         62 $prefix->get('/' => => [format => ['json']] => {format => undef, mojo_status => $self} => \&_dashboard)
39             ->name('mojo_status');
40             }
41              
42             sub _activity {
43 2     2   53 my $all = shift;
44              
45             # Workers
46 2         6 my @table;
47 2         6 for my $pid (sort keys %{$all->{workers}}) {
  2         19  
48 2         10 my $worker = $all->{workers}{$pid};
49 2         49 my $cpu = sprintf '%.2f', $worker->{utime} + $worker->{stime};
50 2         9 my @worker = ($pid, $cpu, humanize_bytes($worker->{maxrss}));
51              
52             # Connections
53 2         32 my $connections = $worker->{connections};
54 2 50       13 if (keys %$connections) {
55 2         5 my $repeat;
56 2         11 for my $cid (sort keys %$connections) {
57 2         4 my $conn = $connections->{$cid};
58 2 50       14 @worker = ('', '', '') if $repeat++;
59 2         7 my $bytes_read = humanize_bytes $conn->{bytes_read};
60 2         40 my $bytes_written = humanize_bytes $conn->{bytes_written};
61 2         30 my $rw = "$bytes_read/$bytes_written";
62 2         21 my @conn = ($conn->{client}, $rw, $conn->{processed});
63              
64             # Request
65 2 50       10 if (my $req = $conn->{request}) {
66 2 50       11 my $active = $req->{finished} ? 0 : 1;
67 2         4 my ($rid, $proto) = @{$req}{qw(request_id protocol)};
  2         9  
68              
69 2         7 my $str = "$req->{method} $req->{path}";
70 2 100       10 $str .= "?$req->{query}" if $req->{query};
71 2 50       10 $str .= " → $req->{status}" if $req->{status};
72              
73 2 50       23 my $finished = $active ? time : $req->{finished};
74 2         19 my $time = sprintf '%.2f', $finished - $req->{started};
75 2         19 push @table, [@worker, @conn, $rid, $active, $time, $proto, $str];
76             }
77 0         0 else { push @table, [@worker, @conn] }
78             }
79             }
80 0         0 else { push @table, \@worker }
81             }
82              
83 2         10 return \@table;
84             }
85              
86             sub _dashboard {
87 9     9   15778 my $c = shift;
88              
89 9         35 my $map = $c->stash('mojo_status')->{map};
90 9 100       140 if ($c->param('reset')) {
91 1     1   341 $map->writer->change(sub { @{$_}{qw(slowest stats)} = ([], _stats()) });
  1         7  
  1         10  
92 1         8 return $c->redirect_to('mojo_status');
93             }
94              
95 8         2361 my $all = $map->writer->fetch;
96             $c->respond_to(
97             html => sub {
98 2     2   1090 $c->render(
99             'mojo-status/dashboard',
100             now => time,
101             usage => humanize_bytes($map->usage),
102             size => humanize_bytes($map->size),
103             activity => _activity($all),
104             slowest => _slowest($all),
105             all => $all
106             );
107             },
108 8         47 json => {json => $all}
109             );
110             }
111              
112             sub _read_write {
113 64     64   161 my ($all, $id) = @_;
114 64 50       393 return unless my $stream = Mojo::IOLoop->stream($id);
115 64         1290 @{$all->{workers}{$$}{connections}{$id}}{qw(bytes_read bytes_written)}
  64         623  
116             = ($stream->bytes_read, $stream->bytes_written);
117             }
118              
119             sub _request {
120 64     64   152 my ($self, $c) = @_;
121              
122             # Request start
123 64         184 my $tx = $c->tx;
124 64         369 my $id = $tx->connection;
125 64         482 my $req = $tx->req;
126 64         388 my $url = $req->url->to_abs;
127 64 50       12610 my $proto = $tx->is_websocket ? 'ws' : 'http';
128 64 50       405 $proto .= 's' if $req->is_secure;
129             $self->{map}->writer->change(sub {
130             $_->{workers}{$$}{connections}{$id}{request} = {
131 64     64   275 request_id => $req->request_id,
132             method => $req->method,
133             protocol => $proto,
134             path => $url->path->to_abs_string,
135             query => $url->query->to_string,
136             started => time
137             };
138 64         14389 _read_write($_, $id);
139 64         243 $_->{workers}{$$}{connections}{$id}{client} = $tx->remote_address;
140 64         1674 });
141              
142             # Request end
143             $tx->on(
144             finish => sub {
145 64     64   126800 my $tx = shift;
146             $self->{map}->writer->change(sub {
147 64 50       291 return unless my $worker = $_->{workers}{$$};
148              
149 64   50     228 my $code = $tx->res->code || 0;
150 64 50       795 if ($code > 499) { $_->{stats}{server_error}++ }
  0 50       0  
    100          
    50          
    0          
151 0         0 elsif ($code > 399) { $_->{stats}{client_error}++ }
152 2         6 elsif ($code > 299) { $_->{stats}{redirect}++ }
153 62         131 elsif ($code > 199) { $_->{stats}{success}++ }
154 0         0 elsif ($code) { $_->{stats}{info}++ }
155              
156 64         228 @{$worker->{connections}{$id}{request}}{qw(finished status)} = (time, $code);
  64         241  
157 64         142 $worker->{connections}{$id}{processed}++;
158 64         109 $worker->{processed}++;
159 64         188 $_->{processed}++;
160 64         345 });
161             }
162 64         502 );
163             }
164              
165             sub _rendered {
166 64     64   184 my ($self, $c) = @_;
167              
168 64         220 my $id = $c->tx->connection;
169             $self->{map}->writer->change(sub {
170 64 50   64   296 return unless my $conn = $_->{workers}{$$}{connections}{$id};
171 64 50       170 return unless my $req = $conn->{request};
172 64         275 $req->{time} = time - $req->{started};
173 64         248 @{$req}{qw(client status worker)} = ($conn->{client}, $c->res->code, $$);
  64         1061  
174              
175 64         158 my $slowest = $_->{slowest};
176 64         332 @$slowest = sort { $b->{time} <=> $a->{time} } @$slowest, $req;
  470         1074  
177 64         114 my %seen;
178 64         149 @$slowest = grep { !$seen{"$_->{method} $_->{path}"}++ } @$slowest;
  324         1192  
179 64         499 pop @$slowest while @$slowest > $self->{slowest};
180 64         875 });
181             }
182              
183             sub _resources {
184 2     2   5 my $self = shift;
185              
186             $self->{map}->writer->change(sub {
187 2     2   41 @{$_->{workers}{$$}}{qw(utime stime maxrss)} = (getrusage)[0, 1, 2];
  2         768  
188              
189             # macOS actually returns bytes instead of kilobytes
190 2         12 $_->{workers}{$$}{maxrss} = $_->{workers}{$$}{maxrss} * 1000 unless MACOS;
191              
192 2         4 for my $id (keys %{$_->{workers}{$$}{connections}}) { _read_write($_, $id) }
  2         13  
  0         0  
193 2         7 });
194             }
195              
196             sub _slowest {
197 2     2   6 my $all = shift;
198              
199 2         5 my @table;
200 2         4 for my $req (@{$all->{slowest}}) {
  2         14  
201 8         24 my $str = "$req->{method} $req->{path}";
202 8 100       25 $str .= "?$req->{query}" if $req->{query};
203 8 50       39 $str .= " → $req->{status}" if $req->{status};
204 8         35 my $time = sprintf '%.2f', $req->{time};
205 8         18 push @table, [$time, $str, @{$req}{qw(request_id worker client started)}];
  8         30  
206             }
207              
208 2         17 return \@table;
209             }
210              
211             sub _start {
212 2     2   8 my ($self, $server, $app) = @_;
213              
214 2 50       17 return $app->log->warn('Server not suported by Mojolicious::Plugin::Status')
215             unless $server->isa('Mojo::Server::Daemon');
216              
217             # Register started workers
218             Mojo::IOLoop->next_tick(sub {
219 2     2   1868 $self->{map}->writer->change(sub { $_->{workers}{$$} = {started => time, processed => 0} });
  2         16  
220 2         40 });
221              
222             # Remove stopped workers
223             $server->on(
224             reap => sub {
225 0     0   0 my ($server, $pid) = @_;
226 0         0 $self->{map}->writer->change(sub { delete $_->{workers}{$pid} });
  0         0  
227             }
228 2 50       210 ) if $server->isa('Mojo::Server::Prefork');
229              
230             # Collect stats
231 2     64   35 $app->hook(after_build_tx => sub { $self->_tx(@_) });
  64         242419  
232 2     64   58 $app->hook(before_dispatch => sub { $self->_request(@_) });
  64         41673  
233 2     64   34 $app->hook(after_dispatch => sub { $self->_rendered(@_) });
  64         108898  
234 2     2   32 Mojo::IOLoop->next_tick(sub { $self->_resources });
  2         25  
235 2     0   65 Mojo::IOLoop->recurring(5 => sub { $self->_resources });
  0         0  
236             }
237              
238 2     2   39 sub _stats { {started => time, info => 0, success => 0, redirect => 0, client_error => 0, server_error => 0} }
239              
240             sub _stream {
241 1     1   5 my ($self, $id) = @_;
242              
243 1         7 my $stream = Mojo::IOLoop->stream($id);
244             $stream->on(
245             close => sub {
246 1 50   1   34974 $self->{map}->writer->change(sub { delete $_->{workers}{$$}{connections}{$id} if $_->{workers}{$$} });
  1         11  
247             }
248 1         24 );
249             }
250              
251             sub _tx {
252 64     64   172 my ($self, $tx, $app) = @_;
253              
254             $tx->on(
255             connection => sub {
256 64     64   2192 my ($tx, $id) = @_;
257              
258 64         205 my $map = $self->{map};
259 64 100       284 return if $map->writer->fetch->{workers}{$$}{connections}{$id};
260              
261             $map->writer->change(sub {
262 1         11 $_->{workers}{$$}{connections}{$id} = {started => time, processed => 0, bytes_read => 0, bytes_written => 0};
263 1         8 });
264 1         6 $self->_stream($id);
265             }
266 64         403 );
267             }
268              
269             1;
270              
271             =encoding utf8
272              
273             =head1 NAME
274              
275             Mojolicious::Plugin::Status - Mojolicious server status
276              
277             =head1 SYNOPSIS
278              
279             # Mojolicious
280             $self->plugin('Status');
281              
282             # Mojolicious::Lite
283             plugin 'Status';
284              
285             # Secure access to the server status ui with Basic authentication
286             my $under = $self->routes->under('/status' => sub ($c) {
287             return 1 if $c->req->url->to_abs->userinfo eq 'Bender:rocks';
288             $c->res->headers->www_authenticate('Basic');
289             $c->render(text => 'Authentication required!', status => 401);
290             return undef;
291             });
292             $self->plugin('Status' => {route => $under});
293              
294             =head1 DESCRIPTION
295              
296             =begin html
297              
298            

299             Screenshot
300             width="600px">
301            

302              
303             =end html
304              
305             L is a L plugin providing a server status ui for L and
306             L. Note that this module is B and should therefore only be used for debugging
307             purposes.
308              
309             =head1 OPTIONS
310              
311             L supports the following options.
312              
313             =head2 return_to
314              
315             # Mojolicious::Lite
316             plugin Status => {return_to => 'some_route'};
317              
318             Name of route or path to return to when leaving the server status ui, defaults to C.
319              
320             =head2 route
321              
322             # Mojolicious::Lite
323             plugin Status => {route => app->routes->any('/status')};
324              
325             L object to attach the server status ui to, defaults to generating a new one with the
326             prefix C.
327              
328             =head2 size
329              
330             # Mojolicious::Lite
331             plugin Status => {size => 1234};
332              
333             Size of anonymous mapped memory to use for storing statistics, defaults to C<52428800> (50 MiB).
334              
335             =head2 slowest
336              
337             # Mojolicious::Lite
338             plugin Status => {slowest => 5};
339              
340             Number of slowest requests to track, defaults to C<10>.
341              
342             =head1 METHODS
343              
344             L inherits all methods from L and implements the following new ones.
345              
346             =head2 register
347              
348             my $route = $plugin->register(Mojolicious->new);
349              
350             Register renderer and helper in L application.
351              
352             =head1 BUNDLED FILES
353              
354             The L distribution includes a few files with different licenses that have been bundled for
355             internal use.
356              
357             =head2 Artwork
358              
359             Copyright (C) 2018, Sebastian Riedel.
360              
361             Licensed under the CC-SA License, Version 4.0 L.
362              
363             =head2 Bootstrap
364              
365             Copyright (C) 2011-2018 The Bootstrap Authors.
366              
367             Licensed under the MIT License, L.
368              
369             =head2 Font Awesome
370              
371             Copyright (C) Dave Gandy.
372              
373             Licensed under the MIT License, L, and the SIL OFL 1.1,
374             L.
375              
376             =head1 AUTHOR
377              
378             Sebastian Riedel, C.
379              
380             =head1 COPYRIGHT AND LICENSE
381              
382             Copyright (C) 2018-2021, Sebastian Riedel and others.
383              
384             This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version
385             2.0.
386              
387             =head1 SEE ALSO
388              
389             L, L, L.
390              
391             =cut