PageRenderTime 98ms CodeModel.GetById 27ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Plack/Middleware/ServerStatus/Lite.pm

https://github.com/kazeburo/Plack-Middleware-ServerStatus-Lite
Perl | 436 lines | 398 code | 35 blank | 3 comment | 55 complexity | 443e74f271ed92bfdccd8d59e7c4bbc7 MD5 | raw file
Possible License(s): AGPL-1.0
  1. package Plack::Middleware::ServerStatus::Lite;
  2. use strict;
  3. use warnings;
  4. use parent qw(Plack::Middleware);
  5. use Plack::Util::Accessor qw(scoreboard path allow counter_file skip_ps_command);
  6. use Plack::Util;
  7. use Parallel::Scoreboard;
  8. use Net::CIDR::Lite;
  9. use Try::Tiny;
  10. use JSON;
  11. use Fcntl qw(:DEFAULT :flock);
  12. use IO::Handle;
  13. our $VERSION = '0.36';
  14. my $JSON = JSON->new->utf8(0);
  15. sub prepare_app {
  16. my $self = shift;
  17. $self->{uptime} = time;
  18. if ( $self->allow ) {
  19. my @ip = ref $self->allow ? @{$self->allow} : ($self->allow);
  20. my @ipv4;
  21. my @ipv6;
  22. for (@ip) {
  23. # hacky check, but actual checks are done in Net::CIDR::Lite.
  24. if (/:/) {
  25. push @ipv6, $_;
  26. } else {
  27. push @ipv4, $_;
  28. }
  29. }
  30. if ( @ipv4 ) {
  31. my $cidr4 = Net::CIDR::Lite->new();
  32. $cidr4->add_any($_) for @ipv4;
  33. $self->{__cidr4} = $cidr4;
  34. }
  35. if ( @ipv6 ) {
  36. my $cidr6 = Net::CIDR::Lite->new();
  37. $cidr6->add_any($_) for @ipv6;
  38. $self->{__cidr6} = $cidr6;
  39. }
  40. }
  41. else {
  42. warn "[Plack::Middleware::ServerStatus::Lite] 'allow' is not provided. Any host will not be able to access server-status page.\n";
  43. }
  44. if ( $self->scoreboard ) {
  45. my $scoreboard = Parallel::Scoreboard->new(
  46. base_dir => $self->scoreboard
  47. );
  48. $self->{__scoreboard} = $scoreboard;
  49. }
  50. if ( $self->counter_file && ! -f $self->counter_file ) {
  51. open( my $fh, '>>:unix', $self->counter_file )
  52. or die "could not open counter_file: $!";
  53. }
  54. }
  55. sub call {
  56. my ($self, $env) = @_;
  57. $self->set_state("A", $env);
  58. my $back_state = sub {
  59. $self->set_state("_");
  60. };
  61. my $guard = bless $back_state, 'Plack::Middleware::ServerStatus::Lite::Guard';
  62. if( $self->path && $env->{PATH_INFO} eq $self->path ) {
  63. my $res = $self->_handle_server_status($env);
  64. if ( $self->counter_file ) {
  65. my $length = Plack::Util::content_length($res->[2]);
  66. $self->counter(1,$length);
  67. }
  68. return $res;
  69. }
  70. my $res = $self->app->($env);
  71. Plack::Util::response_cb($res, sub {
  72. my $res = shift;
  73. if ( defined $res->[2] ) {
  74. if ( $self->counter_file ) {
  75. my $length = Plack::Util::content_length($res->[2]);
  76. $self->counter(1,$length);
  77. }
  78. undef $guard;
  79. return ;
  80. }
  81. my $length = 0;
  82. return sub {
  83. my $chunk = shift;
  84. if ( ! defined $chunk ) {
  85. if ( $self->counter_file ) {
  86. $self->counter(1,$length);
  87. }
  88. undef $guard;
  89. return;
  90. }
  91. $length += length($chunk);
  92. return $chunk;
  93. };
  94. });
  95. }
  96. my $prev={};
  97. sub set_state {
  98. my $self = shift;
  99. return if !$self->{__scoreboard};
  100. my $status = shift || '_';
  101. my $env = shift;
  102. if ( $env ) {
  103. no warnings 'uninitialized';
  104. $prev = {
  105. remote_addr => $env->{REMOTE_ADDR},
  106. host => defined $env->{HTTP_HOST} ? $env->{HTTP_HOST} : '-',
  107. method => $env->{REQUEST_METHOD},
  108. uri => $env->{REQUEST_URI},
  109. protocol => $env->{SERVER_PROTOCOL},
  110. time => time(),
  111. };
  112. }
  113. $self->{__scoreboard}->update($JSON->encode({
  114. %{$prev},
  115. pid => $$,
  116. ppid => getppid(),
  117. uptime => $self->{uptime},
  118. status => $status,
  119. }));
  120. }
  121. sub _handle_server_status {
  122. my ($self, $env ) = @_;
  123. if ( ! $self->allowed($env->{REMOTE_ADDR}) ) {
  124. return [403, ['Content-Type' => 'text/plain'], [ 'Forbidden' ]];
  125. }
  126. my $server_uptime_seconds = time - $self->{uptime};
  127. my $upsince = $server_uptime_seconds;
  128. my $duration = "";
  129. my @spans = (86400 => 'days', 3600 => 'hours', 60 => 'minutes');
  130. while (@spans) {
  131. my ($seconds,$unit) = (shift @spans, shift @spans);
  132. if ($upsince > $seconds) {
  133. $duration .= int($upsince/$seconds) . " $unit, ";
  134. $upsince = $upsince % $seconds;
  135. }
  136. }
  137. $duration .= "$upsince seconds";
  138. my $body="ServerUptime: $duration\nUptime: $server_uptime_seconds\n";
  139. my %status = ( 'Uptime' => $self->{uptime} . "");
  140. if ( $self->counter_file ) {
  141. my ($counter,$bytes) = $self->counter;
  142. my $kbytes = int($bytes / 1_000);
  143. $body .= sprintf "Total Accesses: %s\n", $counter;
  144. $body .= sprintf "Total kBytes: %s\n", $kbytes;
  145. $status{TotalAccesses} = $counter;
  146. $status{TotalKbytes} = $kbytes;
  147. }
  148. if ( my $scoreboard = $self->{__scoreboard} ) {
  149. my $stats = $scoreboard->read_all();
  150. my $idle = 0;
  151. my $busy = 0;
  152. my @all_workers = ();
  153. my $parent_pid = getppid;
  154. if ( $self->skip_ps_command ) {
  155. # none
  156. @all_workers = keys %$stats;
  157. }
  158. elsif ( $^O eq 'cygwin' ) {
  159. my $ps = `ps -ef`;
  160. $ps =~ s/^\s+//mg;
  161. for my $line ( split /\n/, $ps ) {
  162. next if $line =~ m/^\D/;
  163. my @proc = split /\s+/, $line;
  164. push @all_workers, $proc[1] if $proc[2] == $parent_pid;
  165. }
  166. }
  167. elsif ( $^O !~ m!mswin32!i ) {
  168. my $psopt = $^O =~ m/bsd$/ ? '-ax' : '-e';
  169. my $ps = `LC_ALL=C command ps $psopt -o ppid,pid`;
  170. $ps =~ s/^\s+//mg;
  171. for my $line ( split /\n/, $ps ) {
  172. next if $line =~ m/^\D/;
  173. my ($ppid, $pid) = split /\s+/, $line, 2;
  174. push @all_workers, $pid if $ppid == $parent_pid;
  175. }
  176. }
  177. else {
  178. # todo windows?
  179. @all_workers = keys %$stats;
  180. }
  181. my $process_status = '';
  182. my @process_status;
  183. for my $pid ( @all_workers ) {
  184. my $json = $stats->{$pid};
  185. my $pstatus = eval {
  186. $JSON->decode($json || '{}');
  187. };
  188. $pstatus ||= {};
  189. if ( $pstatus->{status} && $pstatus->{status} eq 'A' ) {
  190. $busy++;
  191. }
  192. else {
  193. $idle++;
  194. }
  195. if ( defined $pstatus->{time} ) {
  196. $pstatus->{ss} = time - $pstatus->{time};
  197. }
  198. $pstatus->{pid} ||= $pid;
  199. delete $pstatus->{time};
  200. delete $pstatus->{ppid};
  201. delete $pstatus->{uptime};
  202. $process_status .= sprintf "%s\n",
  203. join(" ", map { defined $pstatus->{$_} ? $pstatus->{$_} : '' } qw/pid status remote_addr host method uri protocol ss/);
  204. push @process_status, $pstatus;
  205. }
  206. $body .= <<EOF;
  207. BusyWorkers: $busy
  208. IdleWorkers: $idle
  209. --
  210. pid status remote_addr host method uri protocol ss
  211. $process_status
  212. EOF
  213. chomp $body;
  214. $status{BusyWorkers} = $busy;
  215. $status{IdleWorkers} = $idle;
  216. $status{stats} = \@process_status;
  217. }
  218. else {
  219. $body .= "WARN: Scoreboard has been disabled\n";
  220. $status{WARN} = 'Scoreboard has been disabled';
  221. }
  222. if ( ($env->{QUERY_STRING} || '') =~ m!\bjson\b!i ) {
  223. return [200, ['Content-Type' => 'application/json; charset=utf-8'], [ JSON::encode_json(\%status) ]];
  224. }
  225. return [200, ['Content-Type' => 'text/plain'], [ $body ]];
  226. }
  227. sub allowed {
  228. my ( $self , $address ) = @_;
  229. if ( $address =~ /:/) {
  230. return unless $self->{__cidr6};
  231. return $self->{__cidr6}->find( $address );
  232. }
  233. return unless $self->{__cidr4};
  234. return $self->{__cidr4}->find( $address );
  235. }
  236. sub counter {
  237. my $self = shift;
  238. my $parent_pid = getppid;
  239. if ( ! $self->{__counter} ) {
  240. open( my $fh, '+<:unix', $self->counter_file ) or die "cannot open counter_file: $!";
  241. $self->{__counter} = $fh;
  242. flock $fh, LOCK_EX;
  243. my $len = sysread $fh, my $buf, 10;
  244. if ( !$len || $buf != $parent_pid ) {
  245. seek $fh, 0, 0;
  246. syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, 0, 0);
  247. }
  248. flock $fh, LOCK_UN;
  249. }
  250. if ( @_ ) {
  251. my ($count, $bytes) = @_;
  252. $count ||= 1;
  253. $bytes ||= 0;
  254. my $fh = $self->{__counter};
  255. flock $fh, LOCK_EX;
  256. seek $fh, 10, 0;
  257. sysread $fh, my $buf, 40;
  258. my $counter = substr($buf, 0, 20);
  259. my $total_bytes = substr($buf, 20, 20);
  260. $counter ||= 0;
  261. $total_bytes ||= 0;
  262. $counter += $count;
  263. if ($total_bytes + $bytes > 2**53){ # see docs
  264. $total_bytes = 0;
  265. } else {
  266. $total_bytes += $bytes;
  267. }
  268. seek $fh, 0, 0;
  269. syswrite $fh, sprintf("%-10d%-20d%-20d", $parent_pid, $counter, $total_bytes);
  270. flock $fh, LOCK_UN;
  271. return $counter;
  272. }
  273. else {
  274. my $fh = $self->{__counter};
  275. flock $fh, LOCK_EX;
  276. seek $fh, 10, 0;
  277. sysread $fh, my $counter, 20;
  278. sysread $fh, my $total_bytes, 20;
  279. flock $fh, LOCK_UN;
  280. return $counter + 0, $total_bytes + 0;
  281. }
  282. }
  283. 1;
  284. package
  285. Plack::Middleware::ServerStatus::Lite::Guard;
  286. sub DESTROY {
  287. $_[0]->();
  288. }
  289. 1;
  290. __END__
  291. =head1 NAME
  292. Plack::Middleware::ServerStatus::Lite - show server status like Apache's mod_status
  293. =head1 SYNOPSIS
  294. use Plack::Builder;
  295. builder {
  296. enable "Plack::Middleware::ServerStatus::Lite",
  297. path => '/server-status',
  298. allow => [ '127.0.0.1', '192.168.0.0/16' ],
  299. counter_file => '/tmp/counter_file',
  300. scoreboard => '/var/run/server';
  301. $app;
  302. };
  303. % curl http://server:port/server-status
  304. Uptime: 1234567789
  305. Total Accesses: 123
  306. BusyWorkers: 2
  307. IdleWorkers: 3
  308. --
  309. pid status remote_addr host method uri protocol ss
  310. 20060 A 127.0.0.1 localhost:10001 GET / HTTP/1.1 1
  311. 20061 .
  312. 20062 A 127.0.0.1 localhost:10001 GET /server-status HTTP/1.1 0
  313. 20063 .
  314. 20064 .
  315. # JSON format
  316. % curl http://server:port/server-status?json
  317. {"Uptime":"1332476669","BusyWorkers":"2",
  318. "stats":[
  319. {"protocol":null,"remote_addr":null,"pid":"78639",
  320. "status":".","method":null,"uri":null,"host":null,"ss":null},
  321. {"protocol":"HTTP/1.1","remote_addr":"127.0.0.1","pid":"78640",
  322. "status":"A","method":"GET","uri":"/","host":"localhost:10226","ss":0},
  323. ...
  324. ],"IdleWorkers":"3"}
  325. =head1 DESCRIPTION
  326. Plack::Middleware::ServerStatus::Lite is a middleware that display server status in multiprocess Plack servers such as Starman and Starlet. This middleware changes status only before and after executing the application. so cannot monitor keepalive session and network i/o wait.
  327. =head1 CONFIGURATIONS
  328. =over 4
  329. =item path
  330. path => '/server-status',
  331. location that displays server status
  332. =item allow
  333. allow => '127.0.0.1'
  334. allow => ['192.168.0.0/16', '10.0.0.0/8']
  335. host based access control of a page of server status. supports IPv6 address.
  336. =item scoreboard
  337. scoreboard => '/path/to/dir'
  338. Scoreboard directory, Middleware::ServerStatus::Lite stores processes activity information in
  339. =item counter_file
  340. counter_file => '/path/to/counter_file'
  341. Enable Total Access counter
  342. =item skip_ps_command
  343. skip_ps_command => 1 or 0
  344. ServerStatus::Lite executes `ps command` to find all worker processes. But in some systems
  345. that does not mount "/proc" can not find any processes.
  346. IF 'skip_ps_command' is true, ServerStatus::Lite does not `ps`, and checks only processes that
  347. already did process requests.
  348. =back
  349. =head1 TOTAL BYTES
  350. The largest integer that 32-bit Perl can store without loss of precision
  351. is 2**53. So rather than getting all fancy with Math::BigInt, we're just
  352. going to be conservative and wrap that around to 0. That's enough to count
  353. 1 GB per second for a hundred days.
  354. =head1 WHAT DOES "SS" MEAN IN STATUS
  355. Seconds since beginning of most recent request
  356. =head1 AUTHOR
  357. Masahiro Nagano E<lt>kazeburo {at} gmail.comE<gt>
  358. =head1 SEE ALSO
  359. Original ServerStatus by cho45 <http://github.com/cho45/Plack-Middleware-ServerStatus>
  360. L<Plack::Middleware::ServerStatus::Tiny>
  361. =head1 LICENSE
  362. This library is free software; you can redistribute it and/or modify
  363. it under the same terms as Perl itself.
  364. =cut