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   2419981 use strict;
  68         108  
  68         1549  
4 68     68   190 use warnings;
  68         90  
  68         1399  
5 68     68   679 use parent qw(Plack::Middleware);
  68         311  
  68         353  
6 68     68   15191 use Plack::Util::Accessor qw(scoreboard path allow counter_file skip_ps_command);
  68         99  
  68         419  
7 68     68   3552 use Plack::Util;
  68         76  
  68         975  
8 68     68   25477 use Parallel::Scoreboard;
  68         164189  
  68         1494  
9 68     68   29226 use Net::CIDR::Lite;
  68         170739  
  68         1644  
10 68     68   1282 use Try::Tiny;
  68         2213  
  68         3024  
11 68     68   1416 use JSON;
  68         17204  
  68         465  
12 68     68   6236 use Fcntl qw(:DEFAULT :flock);
  68         78  
  68         18547  
13 68     68   295 use IO::Handle;
  68         68  
  68         31379  
14              
15             our $VERSION = '0.36';
16              
17             my $JSON = JSON->new->utf8(0);
18              
19             sub prepare_app {
20 81     81 1 31813820 my $self = shift;
21 81         2102 $self->{uptime} = time;
22              
23 81 50       266 if ( $self->allow ) {
24 81 50       556 my @ip = ref $self->allow ? @{$self->allow} : ($self->allow);
  81         428  
25 81         356 my @ipv4;
26             my @ipv6;
27 81         149 for (@ip) {
28             # hacky check, but actual checks are done in Net::CIDR::Lite.
29 162 100       470 if (/:/) {
30 81         134 push @ipv6, $_;
31             } else {
32 81         134 push @ipv4, $_;
33             }
34             }
35 81 50       244 if ( @ipv4 ) {
36 81         394 my $cidr4 = Net::CIDR::Lite->new();
37 81         729 $cidr4->add_any($_) for @ipv4;
38 81         9577 $self->{__cidr4} = $cidr4;
39             }
40 81 50       230 if ( @ipv6 ) {
41 81         213 my $cidr6 = Net::CIDR::Lite->new();
42 81         531 $cidr6->add_any($_) for @ipv6;
43 81         18705 $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       986 if ( $self->scoreboard ) {
51 81         595 my $scoreboard = Parallel::Scoreboard->new(
52             base_dir => $self->scoreboard
53             );
54 81         4657 $self->{__scoreboard} = $scoreboard;
55             }
56              
57 81 100 100     214 if ( $self->counter_file && ! -f $self->counter_file ) {
58 21 50       568 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 21168127 my ($self, $env) = @_;
65              
66 69         494 $self->set_state("A", $env);
67             my $back_state = sub {
68 69     69   190 $self->set_state("_");
69 69         48983 };
70 69         880 my $guard = bless $back_state, 'Plack::Middleware::ServerStatus::Lite::Guard';
71              
72 69 100 66     539 if( $self->path && $env->{PATH_INFO} eq $self->path ) {
73 8         222 my $res = $self->_handle_server_status($env);
74 8 100       133 if ( $self->counter_file ) {
75 3         91 my $length = Plack::Util::content_length($res->[2]);
76 3         43 $self->counter(1,$length);
77             }
78 8         91 return $res;
79             }
80              
81 61         1856 my $res = $self->app->($env);
82             Plack::Util::response_cb($res, sub {
83 61     61   6002174 my $res = shift;
84              
85 61 100       245 if ( defined $res->[2] ) {
86 46 100       219 if ( $self->counter_file ) {
87 43         354 my $length = Plack::Util::content_length($res->[2]);
88 43         531 $self->counter(1,$length);
89             }
90 46         190 undef $guard;
91 46         642 return ;
92             }
93              
94 15         38 my $length = 0;
95             return sub {
96 30         5080 my $chunk = shift;
97 30 100       76 if ( ! defined $chunk ) {
98 15 100       63 if ( $self->counter_file ) {
99 14         156 $self->counter(1,$length);
100             }
101 15         106 undef $guard;
102 15         671 return;
103             }
104 15         41 $length += length($chunk);
105 15         88 return $chunk;
106 15         69 };
107 61         9002088 });
108             }
109              
110             my $prev={};
111             sub set_state {
112 138     138 0 243 my $self = shift;
113 138 50       711 return if !$self->{__scoreboard};
114              
115 138   50     519 my $status = shift || '_';
116 138         211 my $env = shift;
117 138 100       310 if ( $env ) {
118 68     68   328 no warnings 'uninitialized';
  68         77  
  68         64816  
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       1314 time => time(),
126             };
127             }
128             $self->{__scoreboard}->update($JSON->encode({
129 138         5140 %{$prev},
130             pid => $$,
131             ppid => getppid(),
132             uptime => $self->{uptime},
133 138         398 status => $status,
134             }));
135             }
136              
137             sub _handle_server_status {
138 8     8   24 my ($self, $env ) = @_;
139              
140 8 50       46 if ( ! $self->allowed($env->{REMOTE_ADDR}) ) {
141 0         0 return [403, ['Content-Type' => 'text/plain'], [ 'Forbidden' ]];
142             }
143              
144 8         1744 my $server_uptime_seconds = time - $self->{uptime};
145              
146 8         17 my $upsince = $server_uptime_seconds;
147 8         38 my $duration = "";
148 8         78 my @spans = (86400 => 'days', 3600 => 'hours', 60 => 'minutes');
149 8         37 while (@spans) {
150 24         36 my ($seconds,$unit) = (shift @spans, shift @spans);
151 24 50       82 if ($upsince > $seconds) {
152 0         0 $duration .= int($upsince/$seconds) . " $unit, ";
153 0         0 $upsince = $upsince % $seconds;
154             }
155             }
156 8         30 $duration .= "$upsince seconds";
157              
158 8         35 my $body="ServerUptime: $duration\nUptime: $server_uptime_seconds\n";
159 8         45 my %status = ( 'Uptime' => $self->{uptime} . "");
160              
161 8 100       38 if ( $self->counter_file ) {
162 3         22 my ($counter,$bytes) = $self->counter;
163 3         16 my $kbytes = int($bytes / 1_000);
164 3         14 $body .= sprintf "Total Accesses: %s\n", $counter;
165 3         9 $body .= sprintf "Total kBytes: %s\n", $kbytes;
166 3         7 $status{TotalAccesses} = $counter;
167 3         7 $status{TotalKbytes} = $kbytes;
168             }
169              
170 8 50       80 if ( my $scoreboard = $self->{__scoreboard} ) {
171 8         46 my $stats = $scoreboard->read_all();
172 8         3074 my $idle = 0;
173 8         27 my $busy = 0;
174              
175 8         15 my @all_workers = ();
176 8         24 my $parent_pid = getppid;
177            
178 8 100       35 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       191 my $psopt = $^O =~ m/bsd$/ ? '-ax' : '-e';
193 7         34799 my $ps = `LC_ALL=C command ps $psopt -o ppid,pid`;
194 7         189 $ps =~ s/^\s+//mg;
195 7         69 for my $line ( split /\n/, $ps ) {
196 123 100       249 next if $line =~ m/^\D/;
197 116         307 my ($ppid, $pid) = split /\s+/, $line, 2;
198 116 100       285 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         89 my $process_status = '';
207 8         23 my @process_status;
208 8         29 for my $pid ( @all_workers ) {
209 36         52 my $json = $stats->{$pid};
210 36         47 my $pstatus = eval {
211 36   100     7163 $JSON->decode($json || '{}');
212             };
213 36   50     76 $pstatus ||= {};
214 36 100 100     194 if ( $pstatus->{status} && $pstatus->{status} eq 'A' ) {
215 12         28 $busy++;
216             }
217             else {
218 24         23 $idle++;
219             }
220              
221 36 100       70 if ( defined $pstatus->{time} ) {
222 24         49 $pstatus->{ss} = time - $pstatus->{time};
223             }
224 36   66     85 $pstatus->{pid} ||= $pid;
225 36         42 delete $pstatus->{time};
226 36         37 delete $pstatus->{ppid};
227 36         27 delete $pstatus->{uptime};
228             $process_status .= sprintf "%s\n",
229 36 100       63 join(" ", map { defined $pstatus->{$_} ? $pstatus->{$_} : '' } qw/pid status remote_addr host method uri protocol ss/);
  288         520  
230 36         80 push @process_status, $pstatus;
231             }
232 8         53 $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         24 chomp $body;
240 8         83 $status{BusyWorkers} = $busy;
241 8         18 $status{IdleWorkers} = $idle;
242 8         63 $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     100 if ( ($env->{QUERY_STRING} || '') =~ m!\bjson\b!i ) {
249 1         23 return [200, ['Content-Type' => 'application/json; charset=utf-8'], [ JSON::encode_json(\%status) ]];
250             }
251 7         83 return [200, ['Content-Type' => 'text/plain'], [ $body ]];
252             }
253              
254             sub allowed {
255 8     8 0 22 my ( $self , $address ) = @_;
256 8 50       69 if ( $address =~ /:/) {
257 0 0       0 return unless $self->{__cidr6};
258 0         0 return $self->{__cidr6}->find( $address );
259             }
260 8 50       51 return unless $self->{__cidr4};
261 8         133 return $self->{__cidr4}->find( $address );
262             }
263              
264             sub counter {
265 63     63 0 161 my $self = shift;
266 63         127 my $parent_pid = getppid;
267 63 100       183 if ( ! $self->{__counter} ) {
268 20 50       130 open( my $fh, '+<:unix', $self->counter_file ) or die "cannot open counter_file: $!";
269 20         745 $self->{__counter} = $fh;
270 20         89 flock $fh, LOCK_EX;
271 20         135 my $len = sysread $fh, my $buf, 10;
272 20 100 66     265 if ( !$len || $buf != $parent_pid ) {
273 4         18 seek $fh, 0, 0;
274 4         138 syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, 0, 0);
275             }
276 20         83 flock $fh, LOCK_UN;
277             }
278 63 100       182 if ( @_ ) {
279 60         86 my ($count, $bytes) = @_;
280 60   50     171 $count ||= 1;
281 60   50     248 $bytes ||= 0;
282 60         95 my $fh = $self->{__counter};
283 60         260 flock $fh, LOCK_EX;
284 60         104 seek $fh, 10, 0;
285 60         239 sysread $fh, my $buf, 40;
286 60         6465 my $counter = substr($buf, 0, 20);
287 60         103 my $total_bytes = substr($buf, 20, 20);
288 60   50     25508 $counter ||= 0;
289 60   50     129 $total_bytes ||= 0;
290 60         120 $counter += $count;
291 60 50       234 if ($total_bytes + $bytes > 2**53){ # see docs
292 0         0 $total_bytes = 0;
293             } else {
294 60         84 $total_bytes += $bytes;
295             }
296 60         100 seek $fh, 0, 0;
297 60         727 syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, $counter, $total_bytes);
298 60         177 flock $fh, LOCK_UN;
299 60         128 return $counter;
300             }
301             else {
302 3         7 my $fh = $self->{__counter};
303 3         18 flock $fh, LOCK_EX;
304 3         6 seek $fh, 10, 0;
305 3         19 sysread $fh, my $counter, 20;
306 3         7 sysread $fh, my $total_bytes, 20;
307 3         9 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   783 $_[0]->();
319             }
320              
321             1;
322              
323             __END__