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

298             Screenshot
299             width="600px">
300            

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