File Coverage

blib/lib/Plack/Middleware/ServerStatus/Lite.pm
Criterion Covered Total %
statement 200 216 92.5
branch 58 82 70.7
condition 22 31 70.9
subroutine 21 21 100.0
pod 2 5 40.0
total 303 355 85.3


line stmt bran cond sub pod time code
1             package Plack::Middleware::ServerStatus::Lite;
2              
3 68     68   2436026 use strict;
  68         92  
  68         1699  
4 68     68   255 use warnings;
  68         77  
  68         1616  
5 68     68   618 use parent qw(Plack::Middleware);
  68         340  
  68         387  
6 68     68   15882 use Plack::Util::Accessor qw(scoreboard path allow counter_file skip_ps_command);
  68         106  
  68         399  
7 68     68   3192 use Plack::Util;
  68         82  
  68         933  
8 68     68   23883 use Parallel::Scoreboard;
  68         240397  
  68         1487  
9 68     68   28669 use Net::CIDR::Lite;
  68         172705  
  68         1633  
10 68     68   1224 use Try::Tiny;
  68         2072  
  68         3083  
11 68     68   1509 use JSON;
  68         17072  
  68         421  
12 68     68   6133 use Fcntl qw(:DEFAULT :flock);
  68         76  
  68         20483  
13 68     68   268 use IO::Handle;
  68         75  
  68         29916  
14              
15             our $VERSION = '0.35';
16              
17             my $JSON = JSON->new->utf8(0);
18              
19             sub prepare_app {
20 81     81 1 31868142 my $self = shift;
21 81         2078 $self->{uptime} = time;
22              
23 81 50       246 if ( $self->allow ) {
24 81 50       567 my @ip = ref $self->allow ? @{$self->allow} : ($self->allow);
  81         436  
25 81         288 my @ipv4;
26             my @ipv6;
27 81         161 for (@ip) {
28             # hacky check, but actual checks are done in Net::CIDR::Lite.
29 162 100       393 if (/:/) {
30 81         127 push @ipv6, $_;
31             } else {
32 81         170 push @ipv4, $_;
33             }
34             }
35 81 50       258 if ( @ipv4 ) {
36 81         337 my $cidr4 = Net::CIDR::Lite->new();
37 81         757 $cidr4->add_any($_) for @ipv4;
38 81         9692 $self->{__cidr4} = $cidr4;
39             }
40 81 50       236 if ( @ipv6 ) {
41 81         197 my $cidr6 = Net::CIDR::Lite->new();
42 81         583 $cidr6->add_any($_) for @ipv6;
43 81         19328 $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 81 50       987 if ( $self->scoreboard ) {
51 81         604 my $scoreboard = Parallel::Scoreboard->new(
52             base_dir => $self->scoreboard
53             );
54 81         4924 $self->{__scoreboard} = $scoreboard;
55             }
56              
57 81 100 100     223 if ( $self->counter_file && ! -f $self->counter_file ) {
58 21 50       491 open( my $fh, '>>:unix', $self->counter_file )
59             or die "could not open counter_file: $!";
60             }
61             }
62              
63             sub call {
64 69     69 1 21210750 my ($self, $env) = @_;
65              
66 69         496 $self->set_state("A", $env);
67             my $back_state = sub {
68 69     69   198 $self->set_state("_");
69 69         11520 };
70 69         788 my $guard = bless $back_state, 'Plack::Middleware::ServerStatus::Lite::Guard';
71              
72 69 100 66     464 if( $self->path && $env->{PATH_INFO} eq $self->path ) {
73 8         243 my $res = $self->_handle_server_status($env);
74 8 100       124 if ( $self->counter_file ) {
75 3         96 my $length = Plack::Util::content_length($res->[2]);
76 3         40 $self->counter(1,$length);
77             }
78 8         94 return $res;
79             }
80              
81 61         1882 my $res = $self->app->($env);
82             Plack::Util::response_cb($res, sub {
83 61     61   6002089 my $res = shift;
84              
85 61 100       248 if ( defined $res->[2] ) {
86 46 100       192 if ( $self->counter_file ) {
87 43         418 my $length = Plack::Util::content_length($res->[2]);
88 43         531 $self->counter(1,$length);
89             }
90 46         193 undef $guard;
91 46         639 return ;
92             }
93              
94 15         25 my $length = 0;
95             return sub {
96 30         4781 my $chunk = shift;
97 30 100       86 if ( ! defined $chunk ) {
98 15 100       65 if ( $self->counter_file ) {
99 14         133 $self->counter(1,$length);
100             }
101 15         100 undef $guard;
102 15         652 return;
103             }
104 15         17 $length += length($chunk);
105 15         73 return $chunk;
106 15         82 };
107 61         9002296 });
108             }
109              
110             my $prev={};
111             sub set_state {
112 138     138 0 232 my $self = shift;
113 138 50       712 return if !$self->{__scoreboard};
114              
115 138   50     511 my $status = shift || '_';
116 138         148 my $env = shift;
117 138 100       392 if ( $env ) {
118 68     68   310 no warnings 'uninitialized';
  68         75  
  68         69819  
119             $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 69 50       1479 time => time(),
126             };
127             }
128             $self->{__scoreboard}->update($JSON->encode({
129 138         5198 %{$prev},
130             pid => $$,
131             ppid => getppid(),
132             uptime => $self->{uptime},
133 138         390 status => $status,
134             }));
135             }
136              
137             sub _handle_server_status {
138 8     8   35 my ($self, $env ) = @_;
139              
140 8 50       57 if ( ! $self->allowed($env->{REMOTE_ADDR}) ) {
141 0         0 return [403, ['Content-Type' => 'text/plain'], [ 'Forbidden' ]];
142             }
143              
144 8         1018 my $server_uptime_seconds = time - $self->{uptime};
145              
146 8         16 my $upsince = $server_uptime_seconds;
147 8         39 my $duration = "";
148 8         84 my @spans = (86400 => 'days', 3600 => 'hours', 60 => 'minutes');
149 8         32 while (@spans) {
150 24         32 my ($seconds,$unit) = (shift @spans, shift @spans);
151 24 50       84 if ($upsince > $seconds) {
152 0         0 $duration .= int($upsince/$seconds) . " $unit, ";
153 0         0 $upsince = $upsince % $seconds;
154             }
155             }
156 8         34 $duration .= "$upsince seconds";
157              
158 8         29 my $body="ServerUptime: $duration\nUptime: $server_uptime_seconds\n";
159 8         45 my %status = ( 'Uptime' => $self->{uptime} );
160              
161 8 100       44 if ( $self->counter_file ) {
162 3         24 my ($counter,$bytes) = $self->counter;
163 3         21 my $kbytes = int($bytes / 1_000);
164 3         13 $body .= sprintf "Total Accesses: %s\n", $counter;
165 3         14 $body .= sprintf "Total kBytes: %s\n", $kbytes;
166 3         9 $status{TotalAccesses} = $counter;
167 3         13 $status{TotalKbytes} = $kbytes;
168             }
169              
170 8 50       73 if ( my $scoreboard = $self->{__scoreboard} ) {
171 8         37 my $stats = $scoreboard->read_all();
172 8         3021 my $idle = 0;
173 8         15 my $busy = 0;
174              
175 8         21 my @all_workers = ();
176 8         23 my $parent_pid = getppid;
177            
178 8 100       36 if ( $self->skip_ps_command ) {
    50          
    50          
179             # none
180 1         8 @all_workers = keys %$stats;
181             }
182             elsif ( $^O eq 'cygwin' ) {
183 0         0 my $ps = `ps -ef`;
184 0         0 $ps =~ s/^\s+//mg;
185 0         0 for my $line ( split /\n/, $ps ) {
186 0 0       0 next if $line =~ m/^\D/;
187 0         0 my @proc = split /\s+/, $line;
188 0 0       0 push @all_workers, $proc[1] if $proc[2] == $parent_pid;
189             }
190             }
191             elsif ( $^O !~ m!mswin32!i ) {
192 7 50       182 my $psopt = $^O =~ m/bsd$/ ? '-ax' : '-e';
193 7         39717 my $ps = `LC_ALL=C command ps $psopt -o ppid,pid`;
194 7         215 $ps =~ s/^\s+//mg;
195 7         82 for my $line ( split /\n/, $ps ) {
196 123 100       515 next if $line =~ m/^\D/;
197 116         244 my ($ppid, $pid) = split /\s+/, $line, 2;
198 116 100       273 push @all_workers, $pid if $ppid == $parent_pid;
199             }
200             }
201             else {
202             # todo windows?
203 0         0 @all_workers = keys %$stats;
204             }
205              
206 8         34 my $process_status = '';
207 8         20 my @process_status;
208 8         23 for my $pid ( @all_workers ) {
209 36         56 my $json = $stats->{$pid};
210 36         52 my $pstatus = eval {
211 36   100     7532 $JSON->decode($json || '{}');
212             };
213 36   50     76 $pstatus ||= {};
214 36 100 100     177 if ( $pstatus->{status} && $pstatus->{status} eq 'A' ) {
215 12         20 $busy++;
216             }
217             else {
218 24         29 $idle++;
219             }
220              
221 36 100       61 if ( defined $pstatus->{time} ) {
222 24         47 $pstatus->{ss} = time - $pstatus->{time};
223             }
224 36   66     96 $pstatus->{pid} ||= $pid;
225 36         37 delete $pstatus->{time};
226 36         31 delete $pstatus->{ppid};
227 36         36 delete $pstatus->{uptime};
228             $process_status .= sprintf "%s\n",
229 36 100       66 join(" ", map { defined $pstatus->{$_} ? $pstatus->{$_} : '' } qw/pid status remote_addr host method uri protocol ss/);
  288         500  
230 36         80 push @process_status, $pstatus;
231             }
232 8         46 $body .= <
233             BusyWorkers: $busy
234             IdleWorkers: $idle
235             --
236             pid status remote_addr host method uri protocol ss
237             $process_status
238             EOF
239 8         22 chomp $body;
240 8         72 $status{BusyWorkers} = $busy;
241 8         17 $status{IdleWorkers} = $idle;
242 8         58 $status{stats} = \@process_status;
243             }
244             else {
245 0         0 $body .= "WARN: Scoreboard has been disabled\n";
246 0         0 $status{WARN} = 'Scoreboard has been disabled';
247             }
248 8 100 100     96 if ( ($env->{QUERY_STRING} || '') =~ m!\bjson\b!i ) {
249 1         30 return [200, ['Content-Type' => 'application/json; charset=utf-8'], [ JSON::encode_json(\%status) ]];
250             }
251 7         81 return [200, ['Content-Type' => 'text/plain'], [ $body ]];
252             }
253              
254             sub allowed {
255 8     8 0 25 my ( $self , $address ) = @_;
256 8 50       80 if ( $address =~ /:/) {
257 0 0       0 return unless $self->{__cidr6};
258 0         0 return $self->{__cidr6}->find( $address );
259             }
260 8 50       52 return unless $self->{__cidr4};
261 8         143 return $self->{__cidr4}->find( $address );
262             }
263              
264             sub counter {
265 63     63 0 115 my $self = shift;
266 63         120 my $parent_pid = getppid;
267 63 100       232 if ( ! $self->{__counter} ) {
268 20 50       107 open( my $fh, '+<:unix', $self->counter_file ) or die "cannot open counter_file: $!";
269 20         743 $self->{__counter} = $fh;
270 20         84 flock $fh, LOCK_EX;
271 20         128 my $len = sysread $fh, my $buf, 10;
272 20 100 66     252 if ( !$len || $buf != $parent_pid ) {
273 4         15 seek $fh, 0, 0;
274 4         112 syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, 0, 0);
275             }
276 20         101 flock $fh, LOCK_UN;
277             }
278 63 100       206 if ( @_ ) {
279 60         106 my ($count, $bytes) = @_;
280 60   50     280 $count ||= 1;
281 60   50     288 $bytes ||= 0;
282 60         107 my $fh = $self->{__counter};
283 60         262 flock $fh, LOCK_EX;
284 60         104 seek $fh, 10, 0;
285 60         263 sysread $fh, my $buf, 40;
286 60         133 my $counter = substr($buf, 0, 20);
287 60         76 my $total_bytes = substr($buf, 20, 20);
288 60   50     29801 $counter ||= 0;
289 60   50     145 $total_bytes ||= 0;
290 60         120 $counter += $count;
291 60 50       193 if ($total_bytes + $bytes > 2**53){ # see docs
292 0         0 $total_bytes = 0;
293             } else {
294 60         81 $total_bytes += $bytes;
295             }
296 60         96 seek $fh, 0, 0;
297 60         719 syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, $counter, $total_bytes);
298 60         203 flock $fh, LOCK_UN;
299 60         144 return $counter;
300             }
301             else {
302 3         6 my $fh = $self->{__counter};
303 3         18 flock $fh, LOCK_EX;
304 3         7 seek $fh, 10, 0;
305 3         16 sysread $fh, my $counter, 20;
306 3         10 sysread $fh, my $total_bytes, 20;
307 3         10 flock $fh, LOCK_UN;
308 3         11 return $counter + 0, $total_bytes + 0;
309             }
310             }
311              
312             1;
313              
314             package
315             Plack::Middleware::ServerStatus::Lite::Guard;
316              
317             sub DESTROY {
318 69     69   910 $_[0]->();
319             }
320              
321             1;
322              
323             __END__