File Coverage

blib/lib/Mojolicious/Plugin/ServerStatus.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


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