File Coverage

blib/lib/Mojolicious/Plugin/ServerStatus.pm
Criterion Covered Total %
statement 21 196 10.7
branch 0 80 0.0
condition 0 40 0.0
subroutine 7 15 46.6
pod 1 4 25.0
total 29 335 8.6


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::ServerStatus;
2              
3 1     1   17508 use Mojo::Base 'Mojolicious::Plugin';
  1         6887  
  1         4  
4 1     1   1191 use Net::CIDR::Lite;
  1         2743  
  1         25  
5 1     1   420 use Parallel::Scoreboard;
  1         9756  
  1         24  
6 1     1   571 use JSON;
  1         11760  
  1         5  
7 1     1   174 use Fcntl qw(:DEFAULT :flock);
  1         2  
  1         515  
8 1     1   8 use IO::Handle;
  1         1  
  1         1115  
9              
10             our $VERSION = '0.04';
11              
12             my $JSON = JSON->new->utf8(0);
13              
14             has conf => sub { +{} };
15             has skip_ps_command => 0;
16              
17             sub register {
18 0     0 1   my ($plugin, $app, $args) = @_;
19              
20 0           $plugin->{uptime} = time;
21 0   0       $args->{allow} ||= [ '127.0.0.1', '192.168.0.0/16' ];
22 0   0       $args->{path} ||= '/server-status';
23 0   0       $args->{counter_file} ||= '/tmp/counter_file';
24 0   0       $args->{scoreboard} ||= '/var/run/server';
25 0 0         $plugin->conf( $args ) if $args;
26              
27 0 0         if ( $args->{allow} ) {
28 0 0         my @ip = ref $args->{allow} ? @{ $args->{allow} } : ($args->{allow});
  0            
29 0           my @ipv4;
30             my @ipv6;
31 0           for (@ip) {
32             # hacky check, but actual checks are done in Net::CIDR::Lite.
33 0 0         if (/:/) {
34 0           push @ipv6, $_;
35             } else {
36 0           push @ipv4, $_;
37             }
38             }
39 0 0         if ( @ipv4 ) {
40 0           my $cidr4 = Net::CIDR::Lite->new();
41 0           $cidr4->add_any($_) for @ipv4;
42 0           $plugin->{__cidr4} = $cidr4;
43             }
44 0 0         if ( @ipv6 ) {
45 0           my $cidr6 = Net::CIDR::Lite->new();
46 0           $cidr6->add_any($_) for @ipv6;
47 0           $plugin->{__cidr6} = $cidr6;
48             }
49             }
50             else {
51 0           warn "[Mojolicious::Plugin::ServerStatus] 'allow' is not provided. Any host will not be able to access server-status page.\n";
52             }
53            
54 0 0         if ( $args->{scoreboard} ) {
55             my $scoreboard = Parallel::Scoreboard->new(
56             base_dir => $args->{scoreboard}
57 0           );
58 0           $plugin->{__scoreboard} = $scoreboard;
59             }
60              
61 0 0 0       if ( $args->{counter_file} && ! -f $args->{counter_file} ) {
62             open( my $fh, '>>:unix', $args->{counter_file} )
63 0 0         or die "could not open counter_file: $!";
64             }
65              
66 0           my $r = $app->routes;
67             $r->route($args->{path})->to(
68             cb => sub {
69 0     0     my $self = shift;
70 0           my $req = $self->req;
71 0           my $env = $req->env;
72 0           my $tx = $self->tx;
73              
74 0 0         if ( ! $plugin->allowed($tx->remote_address) ) {
75 0           return $self->render(text => 'Forbidden', status => 403);
76             }
77              
78 0           my ($body, $status) = $plugin->_handle_server_status;
79              
80 0 0 0       if ( ($env->{QUERY_STRING} || $req->url->query->to_string ||'') =~ m!\bjson\b!i ) {
81 0           return $self->render(json => $status)
82             }
83 0           return $self->render(text => $body, format => 'txt');
84             }
85 0           );
86              
87             $app->hook(before_dispatch => sub {
88 0     0     my $self = shift;
89 0           my $tx = $self->tx;
90 0           my $req = $self->req;
91 0           my $headers = $req->headers;
92 0 0 0       my $env = %{ $req->env } ? $req->env
  0 0 0        
93             : {
94             REMOTE_ADDR => $tx->remote_address,
95             HTTP_HOST => $headers->host || '',
96             REQUEST_METHOD => $req->method,
97             REQUEST_URI => $req->url->path->to_string || '',
98             SERVER_PROTOCOL => $req->is_secure ? 'HTTPS' : 'HTTP',
99             };
100 0 0         ($env->{USER}) = defined $self->req->url->to_abs->userinfo ? (split /:/smx,$self->req->url->to_abs->userinfo,2) : '-';
101 0           $plugin->set_state("A", $env);
102 0           });
103              
104             $app->hook(after_render => sub {
105 0     0     my ($c, $output, $format) = @_;
106 0 0         if ( $plugin->conf->{counter_file} ) {
107 0           $plugin->counter(1, length($output) );
108             }
109 0           $plugin->set_state('.');
110 0           });
111             }
112              
113             my $prev={};
114             sub set_state {
115 0     0 0   my $self = shift;
116 0 0         return if !$self->{__scoreboard};
117              
118 0   0       my $status = shift || '_';
119 0           my $env = shift;
120 0 0         if ( $env ) {
121 1     1   6 no warnings 'uninitialized';
  1         2  
  1         1838  
122             $prev = {
123             remote_addr => $env->{REMOTE_ADDR},
124             host => defined $env->{HTTP_HOST} ? $env->{HTTP_HOST} : '-',
125             method => $env->{REQUEST_METHOD},
126             uri => $env->{REQUEST_URI},
127             protocol => $env->{SERVER_PROTOCOL},
128             user => $env->{USER},
129 0 0         time => time(),
130             };
131             }
132             $self->{__scoreboard}->update($JSON->encode({
133 0           %{$prev},
134             pid => $$,
135             ppid => getppid(),
136             uptime => $self->{uptime},
137 0           status => $status,
138             }));
139             }
140              
141             sub _handle_server_status {
142 0     0     my ($self) = @_;
143              
144              
145 0           my $upsince = time - $self->{uptime};
146 0           my $duration = "";
147 0           my @spans = (86400 => 'days', 3600 => 'hours', 60 => 'minutes');
148 0           while (@spans) {
149 0           my ($seconds,$unit) = (shift @spans, shift @spans);
150 0 0         if ($upsince > $seconds) {
151 0           $duration .= int($upsince/$seconds) . " $unit, ";
152 0           $upsince = $upsince % $seconds;
153             }
154             }
155 0           $duration .= "$upsince seconds";
156              
157 0           my $body="Uptime: $self->{uptime} ($duration)\n";
158 0           my %status = ( 'Uptime' => $self->{uptime} );
159              
160 0 0         if ( $self->conf->{counter_file} ) {
161 0           my ($counter,$bytes) = $self->counter;
162 0           my $kbytes = int($bytes / 1_000);
163 0           $body .= sprintf "Total Accesses: %s\n", $counter;
164 0           $body .= sprintf "Total Kbytes: %s\n", $kbytes;
165 0           $status{TotalAccesses} = $counter;
166 0           $status{TotalKbytes} = $kbytes;
167             }
168              
169 0 0         if ( my $scoreboard = $self->{__scoreboard} ) {
170 0           my $stats = $scoreboard->read_all();
171 0           my $idle = 0;
172 0           my $busy = 0;
173              
174 0           my @all_workers = ();
175 0           my $parent_pid = getppid;
176            
177 0 0         if ( $self->skip_ps_command ) {
    0          
    0          
178             # none
179 0           @all_workers = keys %$stats;
180             }
181             elsif ( $^O eq 'cygwin' ) {
182 0           my $ps = `ps -ef`;
183 0           $ps =~ s/^\s+//mg;
184 0           for my $line ( split /\n/, $ps ) {
185 0 0         next if $line =~ m/^\D/;
186 0           my @proc = split /\s+/, $line;
187 0 0         push @all_workers, $proc[1] if $proc[2] == $parent_pid;
188             }
189             }
190             elsif ( $^O !~ m!mswin32!i ) {
191 0 0         my $psopt = $^O =~ m/bsd$/ ? '-ax' : '-e';
192 0           my $ps = `LC_ALL=C command ps $psopt -o ppid,pid`;
193 0           $ps =~ s/^\s+//mg;
194 0           for my $line ( split /\n/, $ps ) {
195 0 0         next if $line =~ m/^\D/;
196 0           my ($ppid, $pid) = split /\s+/, $line, 2;
197 0 0         push @all_workers, $pid if $ppid == $parent_pid;
198             }
199             }
200             else {
201             # todo windows?
202 0           @all_workers = keys %$stats;
203             }
204              
205 0           my $process_status = '';
206 0           my @process_status;
207 0           for my $pid ( @all_workers ) {
208 0           my $json = $stats->{$pid};
209 0           my $pstatus = eval {
210 0   0       $JSON->decode($json || '{}');
211             };
212 0   0       $pstatus ||= {};
213 0 0 0       if ( $pstatus->{status} && $pstatus->{status} eq 'A' ) {
214 0           $busy++;
215             }
216             else {
217 0           $idle++;
218             }
219              
220 0 0         if ( defined $pstatus->{time} ) {
221 0           $pstatus->{ss} = time - $pstatus->{time};
222             }
223 0   0       $pstatus->{pid} ||= $pid;
224 0           delete $pstatus->{time};
225 0           delete $pstatus->{ppid};
226 0           delete $pstatus->{uptime};
227             $process_status .= sprintf "%s\n",
228 0 0         join(" ", map { defined $pstatus->{$_} ? $pstatus->{$_} : '' } qw/pid status remote_addr host user method uri protocol ss/);
  0            
229 0           push @process_status, $pstatus;
230             }
231 0           $body .= <
232             BusyWorkers: $busy
233             IdleWorkers: $idle
234             --
235             pid status remote_addr host user method uri protocol ss
236             $process_status
237             EOF
238 0           chomp $body;
239 0           $status{BusyWorkers} = $busy;
240 0           $status{IdleWorkers} = $idle;
241 0           $status{stats} = \@process_status;
242             }
243             else {
244 0           $body .= "WARN: Scoreboard has been disabled\n";
245 0           $status{WARN} = 'Scoreboard has been disabled';
246             }
247 0           return ($body, \%status);
248              
249             }
250              
251             sub allowed {
252 0     0 0   my ( $self , $address ) = @_;
253 0 0         if ( $address =~ /:/) {
254 0 0         return unless $self->{__cidr6};
255 0           return $self->{__cidr6}->find( $address );
256             }
257 0 0         return unless $self->{__cidr4};
258 0           return $self->{__cidr4}->find( $address );
259             }
260              
261             sub counter {
262 0     0 0   my $self = shift;
263 0           my $parent_pid = getppid;
264 0 0         if ( ! $self->{__counter} ) {
265 0 0         open( my $fh, '+<:unix', $self->conf->{counter_file} ) or die "cannot open counter_file: $!";
266 0           $self->{__counter} = $fh;
267 0           flock $fh, LOCK_EX;
268 0           my $len = sysread $fh, my $buf, 10;
269 0 0 0       if ( !$len || $buf != $parent_pid ) {
270 0           seek $fh, 0, 0;
271 0           syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, 0, 0);
272             }
273 0           flock $fh, LOCK_UN;
274             }
275 0 0         if ( @_ ) {
276 0           my ($count, $bytes) = @_;
277 0   0       $count ||= 1;
278 0   0       $bytes ||= 0;
279 0           my $fh = $self->{__counter};
280 0           flock $fh, LOCK_EX;
281 0           seek $fh, 10, 0;
282 0           sysread $fh, my $buf, 40;
283 0           my $counter = substr($buf, 0, 20);
284 0           my $total_bytes = substr($buf, 20, 20);
285 0   0       $counter ||= 0;
286 0   0       $total_bytes ||= 0;
287 0           $counter += $count;
288 0 0         if ($total_bytes + $bytes > 2**53){ # see docs
289 0           $total_bytes = 0;
290             } else {
291 0           $total_bytes += $bytes;
292             }
293 0           seek $fh, 0, 0;
294 0           syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, $counter, $total_bytes);
295 0           flock $fh, LOCK_UN;
296 0           return $counter;
297             }
298             else {
299 0           my $fh = $self->{__counter};
300 0           flock $fh, LOCK_EX;
301 0           seek $fh, 10, 0;
302 0           sysread $fh, my $counter, 20;
303 0           sysread $fh, my $total_bytes, 20;
304 0           flock $fh, LOCK_UN;
305 0           return $counter + 0, $total_bytes + 0;
306             }
307             }
308              
309             1;
310             __END__