File Coverage

blib/lib/Plack/Middleware/ServerStatus/Lite.pm
Criterion Covered Total %
statement 201 215 93.4
branch 63 82 76.8
condition 22 31 70.9
subroutine 21 21 100.0
pod 2 5 40.0
total 309 354 87.2


line stmt bran cond sub pod time code
1             package Plack::Middleware::ServerStatus::Lite;
2              
3 68     68   4627298 use strict;
  68         152  
  68         2576  
4 68     68   400 use warnings;
  68         153  
  68         2339  
5 68     68   1191 use parent qw(Plack::Middleware);
  68         472  
  68         634  
6 68     68   39064 use Plack::Util::Accessor qw(scoreboard path allow counter_file skip_ps_command);
  68         160  
  68         687  
7 68     68   5377 use Plack::Util;
  68         138  
  68         1613  
8 68     68   58781 use Parallel::Scoreboard;
  68         306136  
  68         2112  
9 68     68   62083 use Net::CIDR::Lite;
  68         260822  
  68         2291  
10 68     68   2565 use Try::Tiny;
  68         3114  
  68         4360  
11 68     68   2486 use JSON;
  68         225332  
  68         592  
12 68     68   10170 use Fcntl qw(:DEFAULT :flock);
  68         173  
  68         37859  
13 68     68   429 use IO::Handle;
  68         138  
  68         53937  
14              
15             our $VERSION = '0.33';
16              
17             my $JSON = JSON->new->utf8(0);
18              
19             sub prepare_app {
20 83     83 1 454037694 my $self = shift;
21 83         6678 $self->{uptime} = time;
22              
23 83 50       513 if ( $self->allow ) {
24 83 100       910 my @ip = ref $self->allow ? @{$self->allow} : ($self->allow);
  81         796  
25 83         578 my @ipv4;
26             my @ipv6;
27 83         243 for (@ip) {
28             # hacky check, but actual checks are done in Net::CIDR::Lite.
29 164 100       672 if (/:/) {
30 81         247 push @ipv6, $_;
31             } else {
32 83         282 push @ipv4, $_;
33             }
34             }
35 83 50       428 if ( @ipv4 ) {
36 83         764 my $cidr4 = Net::CIDR::Lite->new();
37 83         1369 $cidr4->add_any($_) for @ipv4;
38 83         17889 $self->{__cidr4} = $cidr4;
39             }
40 83 100       352 if ( @ipv6 ) {
41 81         354 my $cidr6 = Net::CIDR::Lite->new();
42 81         988 $cidr6->add_any($_) for @ipv6;
43 81         33291 $self->{__cidr6} = $cidr6;
44             }
45             }
46             else {
47 0         0 warn "[Plack::Middleware::ServerStatus::Lite] 'allow' is not provided. Any host will not be able to access server-status page.\n";
48             }
49            
50 83 100       1886 if ( $self->scoreboard ) {
51 82         981 my $scoreboard = Parallel::Scoreboard->new(
52             base_dir => $self->scoreboard
53             );
54 82         10204 $self->{__scoreboard} = $scoreboard;
55             }
56              
57 83 100 100     396 if ( $self->counter_file && ! -f $self->counter_file ) {
58 21 50       1095 open( my $fh, '>>:unix', $self->counter_file )
59             or die "could not open counter_file: $!";
60             }
61             }
62              
63             sub call {
64 70     70 1 51762452 my ($self, $env) = @_;
65              
66 70         1243 $self->set_state("A", $env);
67             my $back_state = sub {
68 70     70   334 $self->set_state("_");
69 70         25045 };
70 70         1991 my $guard = bless $back_state, 'Plack::Middleware::ServerStatus::Lite::Guard';
71              
72 70 100 66     764 if( $self->path && $env->{PATH_INFO} eq $self->path ) {
73 9         450 my $res = $self->_handle_server_status($env);
74 9 100       235 if ( $self->counter_file ) {
75 3         216 my $length = Plack::Util::content_length($res->[2]);
76 3         509 $self->counter(1,$length);
77             }
78 9         207 return $res;
79             }
80              
81 61         3738 my $res = $self->app->($env);
82             Plack::Util::response_cb($res, sub {
83 61     61   6005193 my $res = shift;
84              
85 61 100       468 if ( defined $res->[2] ) {
86 46 100       463 if ( $self->counter_file ) {
87 43         923 my $length = Plack::Util::content_length($res->[2]);
88 43         1081 $self->counter(1,$length);
89             }
90 46         485 undef $guard;
91 46         255 return ;
92             }
93              
94 15         32 my $length = 0;
95             return sub {
96 30         10141 my $chunk = shift;
97 30 100       138 if ( ! defined $chunk ) {
98 15 100       339 if ( $self->counter_file ) {
99 14         259 $self->counter(1,$length);
100             }
101 15         53 undef $guard;
102 15         99 return;
103             }
104 15         73 $length += length($chunk);
105 15         118 return $chunk;
106 15         181 };
107 61         9005946 });
108             }
109              
110             my $prev={};
111             sub set_state {
112 140     140 0 499 my $self = shift;
113 140 100       2398 return if !$self->{__scoreboard};
114              
115 138   50     1187 my $status = shift || '_';
116 138         352 my $env = shift;
117 138 100       616 if ( $env ) {
118 68     68   494 no warnings 'uninitialized';
  68         256  
  68         163832  
119 69 50       3446 $prev = {
120             remote_addr => $env->{REMOTE_ADDR},
121             host => defined $env->{HTTP_HOST} ? $env->{HTTP_HOST} : '-',
122             method => $env->{REQUEST_METHOD},
123             uri => $env->{REQUEST_URI},
124             protocol => $env->{SERVER_PROTOCOL},
125             time => time(),
126             };
127             }
128 138         12474 $self->{__scoreboard}->update($JSON->encode({
129 138         815 %{$prev},
130             pid => $$,
131             ppid => getppid(),
132             uptime => $self->{uptime},
133             status => $status,
134             }));
135             }
136              
137             sub _handle_server_status {
138 9     9   35 my ($self, $env ) = @_;
139              
140 9 50       60 if ( ! $self->allowed($env->{REMOTE_ADDR}) ) {
141 0         0 return [403, ['Content-Type' => 'text/plain'], [ 'Forbidden' ]];
142             }
143              
144 9         1931 my $upsince = time - $self->{uptime};
145 9         64 my $duration = "";
146 9         136 my @spans = (86400 => 'days', 3600 => 'hours', 60 => 'minutes');
147 9         45 while (@spans) {
148 27         55 my ($seconds,$unit) = (shift @spans, shift @spans);
149 27 50       102 if ($upsince > $seconds) {
150 0         0 $duration .= int($upsince/$seconds) . " $unit, ";
151 0         0 $upsince = $upsince % $seconds;
152             }
153             }
154 9         57 $duration .= "$upsince seconds";
155              
156 9         71 my $body="Uptime: $self->{uptime} ($duration)\n";
157 9         60 my %status = ( 'Uptime' => $self->{uptime} );
158              
159 9 100       54 if ( $self->counter_file ) {
160 3         37 my ($counter,$bytes) = $self->counter;
161 3         11 my $kbytes = int($bytes / 1_000);
162 3         26 $body .= sprintf "Total Accesses: %s\n", $counter;
163 3         8 $body .= sprintf "Total Kbytes: %s\n", $kbytes;
164 3         30 $status{TotalAccesses} = $counter;
165 3         8 $status{TotalKbytes} = $kbytes;
166             }
167              
168 9 100       129 if ( my $scoreboard = $self->{__scoreboard} ) {
169 8         66 my $stats = $scoreboard->read_all();
170 8         7072 my $idle = 0;
171 8         21 my $busy = 0;
172              
173 8         26 my @all_workers = ();
174 8         44 my $parent_pid = getppid;
175            
176 8 100       184 if ( $self->skip_ps_command ) {
    50          
    50          
177             # none
178 1         8 @all_workers = keys %$stats;
179             }
180             elsif ( $^O eq 'cygwin' ) {
181 0         0 my $ps = `ps -ef`;
182 0         0 $ps =~ s/^\s+//mg;
183 0         0 for my $line ( split /\n/, $ps ) {
184 0 0       0 next if $line =~ m/^\D/;
185 0         0 my @proc = split /\s+/, $line;
186 0 0       0 push @all_workers, $proc[1] if $proc[2] == $parent_pid;
187             }
188             }
189             elsif ( $^O !~ m!mswin32!i ) {
190 7 50       247 my $psopt = $^O =~ m/bsd$/ ? '-ax' : '-e';
191 7         101856 my $ps = `LC_ALL=C command ps $psopt -o ppid,pid`;
192 7         4552 $ps =~ s/^\s+//mg;
193 7         183 for my $line ( split /\n/, $ps ) {
194 117 100       2915 next if $line =~ m/^\D/;
195 110         290 my ($ppid, $pid) = split /\s+/, $line, 2;
196 110 100       561 push @all_workers, $pid if $ppid == $parent_pid;
197             }
198             }
199             else {
200             # todo windows?
201 0         0 @all_workers = keys %$stats;
202             }
203              
204 8         54 my $process_status = '';
205 8         50 my @process_status;
206 8         51 for my $pid ( @all_workers ) {
207 32         89 my $json = $stats->{$pid};
208 32         95 my $pstatus = eval {
209 32   100     1556 $JSON->decode($json || '{}');
210             };
211 32   50     186 $pstatus ||= {};
212 32 100 100     252 if ( $pstatus->{status} && $pstatus->{status} eq 'A' ) {
213 11         37 $busy++;
214             }
215             else {
216 21         39 $idle++;
217             }
218              
219 32 100       110 if ( defined $pstatus->{time} ) {
220 23         100 $pstatus->{ss} = time - $pstatus->{time};
221             }
222 32   66     153 $pstatus->{pid} ||= $pid;
223 32         89 delete $pstatus->{time};
224 32         64 delete $pstatus->{ppid};
225 32         51 delete $pstatus->{uptime};
226 256 100       1101 $process_status .= sprintf "%s\n",
227 32         140 join(" ", map { defined $pstatus->{$_} ? $pstatus->{$_} : '' } qw/pid status remote_addr host method uri protocol ss/);
228 32         132 push @process_status, $pstatus;
229             }
230 8         86 $body .= <
231             BusyWorkers: $busy
232             IdleWorkers: $idle
233             --
234             pid status remote_addr host method uri protocol ss
235             $process_status
236             EOF
237 8         50 chomp $body;
238 8         89 $status{BusyWorkers} = $busy;
239 8         130 $status{IdleWorkers} = $idle;
240 8         97 $status{stats} = \@process_status;
241             }
242             else {
243 1         2 $body .= "WARN: Scoreboard has been disabled\n";
244 1         3 $status{WARN} = 'Scoreboard has been disabled';
245             }
246 9 100 100     151 if ( ($env->{QUERY_STRING} || '') =~ m!\bjson\b!i ) {
247 1         41 return [200, ['Content-Type' => 'application/json; charset=utf-8'], [ JSON::encode_json(\%status) ]];
248             }
249 8         209 return [200, ['Content-Type' => 'text/plain'], [ $body ]];
250             }
251              
252             sub allowed {
253 9     9 0 37 my ( $self , $address ) = @_;
254 9 50       90 if ( $address =~ /:/) {
255 0 0       0 return unless $self->{__cidr6};
256 0         0 return $self->{__cidr6}->find( $address );
257             }
258 9 50       62 return unless $self->{__cidr4};
259 9         168 return $self->{__cidr4}->find( $address );
260             }
261              
262             sub counter {
263 63     63 0 162 my $self = shift;
264 63         334 my $parent_pid = getppid;
265 63 100       384 if ( ! $self->{__counter} ) {
266 20 50       243 open( my $fh, '+<:unix', $self->counter_file ) or die "cannot open counter_file: $!";
267 20         1624 $self->{__counter} = $fh;
268 20         187 flock $fh, LOCK_EX;
269 20         395 my $len = sysread $fh, my $buf, 10;
270 20 100 66     483 if ( !$len || $buf != $parent_pid ) {
271 4         23 seek $fh, 0, 0;
272 4         227 syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, 0, 0);
273             }
274 20         177 flock $fh, LOCK_UN;
275             }
276 63 100       275 if ( @_ ) {
277 60         147 my ($count, $bytes) = @_;
278 60   50     470 $count ||= 1;
279 60   50     758 $bytes ||= 0;
280 60         163 my $fh = $self->{__counter};
281 60         616 flock $fh, LOCK_EX;
282 60         266 seek $fh, 10, 0;
283 60         543 sysread $fh, my $buf, 40;
284 60         217 my $counter = substr($buf, 0, 20);
285 60         151 my $total_bytes = substr($buf, 20, 20);
286 60   50     286 $counter ||= 0;
287 60   50     189 $total_bytes ||= 0;
288 60         221 $counter += $count;
289 60 50       454 if ($total_bytes + $bytes > 2**53){ # see docs
290 0         0 $total_bytes = 0;
291             } else {
292 60         119 $total_bytes += $bytes;
293             }
294 60         245 seek $fh, 0, 0;
295 60         1138 syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, $counter, $total_bytes);
296 60         373 flock $fh, LOCK_UN;
297 60         232 return $counter;
298             }
299             else {
300 3         30 my $fh = $self->{__counter};
301 3         37 flock $fh, LOCK_EX;
302 3         13 seek $fh, 10, 0;
303 3         30 sysread $fh, my $counter, 20;
304 3         19 sysread $fh, my $total_bytes, 20;
305 3         17 flock $fh, LOCK_UN;
306 3         15 return $counter + 0, $total_bytes + 0;
307             }
308             }
309              
310             1;
311              
312             package
313             Plack::Middleware::ServerStatus::Lite::Guard;
314              
315             sub DESTROY {
316 70     70   1580 $_[0]->();
317             }
318              
319             1;
320              
321             __END__