File Coverage

blib/lib/Mojolicious/Plugin/ServerStatus.pm
Criterion Covered Total %
statement 21 194 10.8
branch 0 78 0.0
condition 0 40 0.0
subroutine 7 15 46.6
pod 1 4 25.0
total 29 331 8.7


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::ServerStatus;
2              
3 1     1   20591 use Mojo::Base 'Mojolicious::Plugin';
  1         10317  
  1         8  
4 1     1   1879 use Net::CIDR::Lite;
  1         4028  
  1         31  
5 1     1   808 use Parallel::Scoreboard;
  1         15975  
  1         32  
6 1     1   1005 use JSON;
  1         32973  
  1         7  
7 1     1   172 use Fcntl qw(:DEFAULT :flock);
  1         2  
  1         454  
8 1     1   6 use IO::Handle;
  1         3  
  1         933  
9              
10             our $VERSION = '0.03';
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           $plugin->set_state("A", $env);
101 0           });
102              
103             $app->hook(after_render => sub {
104 0     0     my ($c, $output, $format) = @_;
105 0 0         if ( $plugin->conf->{counter_file} ) {
106 0           $plugin->counter(1, length($output) );
107             }
108 0           });
109             }
110              
111             my $prev={};
112             sub set_state {
113 0     0 0   my $self = shift;
114 0 0         return if !$self->{__scoreboard};
115              
116 0   0       my $status = shift || '_';
117 0           my $env = shift;
118 0 0         if ( $env ) {
119 1     1   6 no warnings 'uninitialized';
  1         2  
  1         1675  
120             $prev = {
121             remote_addr => $env->{REMOTE_ADDR},
122             host => defined $env->{HTTP_HOST} ? $env->{HTTP_HOST} : '-',
123             method => $env->{REQUEST_METHOD},
124             uri => $env->{REQUEST_URI},
125             protocol => $env->{SERVER_PROTOCOL},
126 0 0         time => time(),
127             };
128             }
129             $self->{__scoreboard}->update($JSON->encode({
130 0           %{$prev},
131             pid => $$,
132             ppid => getppid(),
133             uptime => $self->{uptime},
134 0           status => $status,
135             }));
136             }
137              
138             sub _handle_server_status {
139 0     0     my ($self) = @_;
140              
141              
142 0           my $upsince = time - $self->{uptime};
143 0           my $duration = "";
144 0           my @spans = (86400 => 'days', 3600 => 'hours', 60 => 'minutes');
145 0           while (@spans) {
146 0           my ($seconds,$unit) = (shift @spans, shift @spans);
147 0 0         if ($upsince > $seconds) {
148 0           $duration .= int($upsince/$seconds) . " $unit, ";
149 0           $upsince = $upsince % $seconds;
150             }
151             }
152 0           $duration .= "$upsince seconds";
153              
154 0           my $body="Uptime: $self->{uptime} ($duration)\n";
155 0           my %status = ( 'Uptime' => $self->{uptime} );
156              
157 0 0         if ( $self->conf->{counter_file} ) {
158 0           my ($counter,$bytes) = $self->counter;
159 0           my $kbytes = int($bytes / 1_000);
160 0           $body .= sprintf "Total Accesses: %s\n", $counter;
161 0           $body .= sprintf "Total Kbytes: %s\n", $kbytes;
162 0           $status{TotalAccesses} = $counter;
163 0           $status{TotalKbytes} = $kbytes;
164             }
165              
166 0 0         if ( my $scoreboard = $self->{__scoreboard} ) {
167 0           my $stats = $scoreboard->read_all();
168 0           my $idle = 0;
169 0           my $busy = 0;
170              
171 0           my @all_workers = ();
172 0           my $parent_pid = getppid;
173            
174 0 0         if ( $self->skip_ps_command ) {
    0          
    0          
175             # none
176 0           @all_workers = keys %$stats;
177             }
178             elsif ( $^O eq 'cygwin' ) {
179 0           my $ps = `ps -ef`;
180 0           $ps =~ s/^\s+//mg;
181 0           for my $line ( split /\n/, $ps ) {
182 0 0         next if $line =~ m/^\D/;
183 0           my @proc = split /\s+/, $line;
184 0 0         push @all_workers, $proc[1] if $proc[2] == $parent_pid;
185             }
186             }
187             elsif ( $^O !~ m!mswin32!i ) {
188 0 0         my $psopt = $^O =~ m/bsd$/ ? '-ax' : '-e';
189 0           my $ps = `LC_ALL=C command ps $psopt -o ppid,pid`;
190 0           $ps =~ s/^\s+//mg;
191 0           for my $line ( split /\n/, $ps ) {
192 0 0         next if $line =~ m/^\D/;
193 0           my ($ppid, $pid) = split /\s+/, $line, 2;
194 0 0         push @all_workers, $pid if $ppid == $parent_pid;
195             }
196             }
197             else {
198             # todo windows?
199 0           @all_workers = keys %$stats;
200             }
201              
202 0           my $process_status = '';
203 0           my @process_status;
204 0           for my $pid ( @all_workers ) {
205 0           my $json = $stats->{$pid};
206 0           my $pstatus = eval {
207 0   0       $JSON->decode($json || '{}');
208             };
209 0   0       $pstatus ||= {};
210 0 0 0       if ( $pstatus->{status} && $pstatus->{status} eq 'A' ) {
211 0           $busy++;
212             }
213             else {
214 0           $idle++;
215             }
216              
217 0 0         if ( defined $pstatus->{time} ) {
218 0           $pstatus->{ss} = time - $pstatus->{time};
219             }
220 0   0       $pstatus->{pid} ||= $pid;
221 0           delete $pstatus->{time};
222 0           delete $pstatus->{ppid};
223 0           delete $pstatus->{uptime};
224             $process_status .= sprintf "%s\n",
225 0 0         join(" ", map { defined $pstatus->{$_} ? $pstatus->{$_} : '' } qw/pid status remote_addr host method uri protocol ss/);
  0            
226 0           push @process_status, $pstatus;
227             }
228 0           $body .= <
229             BusyWorkers: $busy
230             IdleWorkers: $idle
231             --
232             pid status remote_addr host method uri protocol ss
233             $process_status
234             EOF
235 0           chomp $body;
236 0           $status{BusyWorkers} = $busy;
237 0           $status{IdleWorkers} = $idle;
238 0           $status{stats} = \@process_status;
239             }
240             else {
241 0           $body .= "WARN: Scoreboard has been disabled\n";
242 0           $status{WARN} = 'Scoreboard has been disabled';
243             }
244 0           return ($body, \%status);
245              
246             }
247              
248             sub allowed {
249 0     0 0   my ( $self , $address ) = @_;
250 0 0         if ( $address =~ /:/) {
251 0 0         return unless $self->{__cidr6};
252 0           return $self->{__cidr6}->find( $address );
253             }
254 0 0         return unless $self->{__cidr4};
255 0           return $self->{__cidr4}->find( $address );
256             }
257              
258             sub counter {
259 0     0 0   my $self = shift;
260 0           my $parent_pid = getppid;
261 0 0         if ( ! $self->{__counter} ) {
262 0 0         open( my $fh, '+<:unix', $self->conf->{counter_file} ) or die "cannot open counter_file: $!";
263 0           $self->{__counter} = $fh;
264 0           flock $fh, LOCK_EX;
265 0           my $len = sysread $fh, my $buf, 10;
266 0 0 0       if ( !$len || $buf != $parent_pid ) {
267 0           seek $fh, 0, 0;
268 0           syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, 0, 0);
269             }
270 0           flock $fh, LOCK_UN;
271             }
272 0 0         if ( @_ ) {
273 0           my ($count, $bytes) = @_;
274 0   0       $count ||= 1;
275 0   0       $bytes ||= 0;
276 0           my $fh = $self->{__counter};
277 0           flock $fh, LOCK_EX;
278 0           seek $fh, 10, 0;
279 0           sysread $fh, my $buf, 40;
280 0           my $counter = substr($buf, 0, 20);
281 0           my $total_bytes = substr($buf, 20, 20);
282 0   0       $counter ||= 0;
283 0   0       $total_bytes ||= 0;
284 0           $counter += $count;
285 0 0         if ($total_bytes + $bytes > 2**53){ # see docs
286 0           $total_bytes = 0;
287             } else {
288 0           $total_bytes += $bytes;
289             }
290 0           seek $fh, 0, 0;
291 0           syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, $counter, $total_bytes);
292 0           flock $fh, LOCK_UN;
293 0           return $counter;
294             }
295             else {
296 0           my $fh = $self->{__counter};
297 0           flock $fh, LOCK_EX;
298 0           seek $fh, 10, 0;
299 0           sysread $fh, my $counter, 20;
300 0           sysread $fh, my $total_bytes, 20;
301 0           flock $fh, LOCK_UN;
302 0           return $counter + 0, $total_bytes + 0;
303             }
304             }
305              
306             1;
307             __END__