File Coverage

blib/lib/Net/Prometheus/ServerStatusLiteCollector.pm
Criterion Covered Total %
statement 29 118 24.5
branch 0 30 0.0
condition 0 21 0.0
subroutine 10 16 62.5
pod 0 5 0.0
total 39 190 20.5


line stmt bran cond sub pod time code
1             package Net::Prometheus::ServerStatusLiteCollector;
2              
3             # ABSTRACT: A Net::Prometheus Collector that works in tandem with Plack::Middleware::ServerStatus::Lite
4              
5 1     1   729 use 5.008001;
  1         3  
6 1     1   4 use strict;
  1         2  
  1         17  
7 1     1   4 use warnings;
  1         2  
  1         23  
8              
9 1     1   790 use DateTime;
  1         433573  
  1         39  
10 1     1   514 use DateTime::Format::ISO8601::Format;
  1         545  
  1         27  
11 1     1   545 use JSON;
  1         6704  
  1         6  
12 1     1   490 use Net::Prometheus::Types qw(MetricSamples Sample);
  1         1979  
  1         50  
13 1     1   388 use Parallel::Scoreboard;
  1         8141  
  1         69  
14              
15             our $VERSION = "0.02";
16              
17             my $JSON = JSON->new->utf8(0);
18             my $dt_formatter = DateTime::Format::ISO8601::Format->new(second_precision => 3);
19              
20             # Basic get/set attribute methods
21             my @attrs = qw(
22             counter_file
23             scoreboard
24             labels
25             );
26             foreach my $attr (@attrs) {
27 1     1   7 no strict 'refs';
  1         2  
  1         511  
28             *$attr = sub {
29 0     0     my $self = shift;
30 0 0         if (defined $_[0]) {
31 0           return $self->{$attr} = $_[0];
32             } else {
33 0           return $self->{$attr};
34             }
35             };
36             }
37              
38             sub log {
39 0     0 0   my $self = shift;
40 0           my ($level, $msg) = @_;
41              
42 0           my $timestamp = DateTime->now(
43             formatter => $dt_formatter,
44             time_zone => 'America/Los_Angeles',
45             )->stringify;
46              
47 0           print STDERR $JSON->encode({
48             category => 'Plack',
49             middleware => __PACKAGE__,
50             level => $level,
51             pid => $$,
52             timestamp => $timestamp,
53             message => $msg,
54             });
55             }
56              
57             sub warn {
58 0     0 0   my $self = shift;
59 0           my ($msg) = @_;
60              
61 0           $self->log('WARN', $msg);
62             }
63              
64             sub error {
65 0     0 0   my $self = shift;
66 0           my ($msg) = @_;
67              
68 0           $self->log('ERROR', $msg)
69             }
70              
71             # Simple class initialization, pass in a hashref of arguments or directly
72             sub new {
73 0     0 0   my $class = shift;
74 0 0         my %args = ref $_[0] eq 'HASH' ? %{$_[0]} : @_;
  0            
75              
76 0           my $self = bless {}, $class;
77              
78             # Copy keys from %args, but ignore invalid attributes
79 0           for my $k (@attrs) {
80 0 0         $self->$k($args{$k}) if defined $args{$k};
81             }
82              
83 0           return $self
84             }
85              
86             sub collect {
87 0     0 0   my $self = shift;
88              
89             # Ripped from https://metacpan.org/source/KAZEBURO/Plack-Middleware-ServerStatus-Lite-0.36/bin/server-status
90              
91 0           my $stats = {};
92             # Must use an already open instance of Parallel::Scoreboard since
93             # initializing new will clear out scoreboard directory
94 0 0 0       if ($self->scoreboard && ref $self->scoreboard eq 'Parallel::Scoreboard') {
95 0           $stats = $self->scoreboard->read_all();
96             }
97 0 0         if (! scalar %$stats) {
98 0           $self->warn("There is no status file in scoreboard directory. Maybe all processes are idle state and do not serve any request yet.");
99 0           return ();
100             }
101              
102             # Check counter file
103 0           my $counter_fh;
104 0 0         if ($self->counter_file) {
105 0 0         unless (open($counter_fh, '<:unix', $self->counter_file)) {
106 0           $self->error("Could not open counter file: $!");
107 0           return ();
108             }
109             }
110              
111             # Check scoreboard stats is valid
112 0           my @all_workers = keys %$stats;
113 0           my $pstatus = eval {
114 0   0       $JSON->decode($stats->{$all_workers[0]} || '{}');
115             };
116 0 0 0       if (!$pstatus->{ppid} || !$pstatus->{uptime} || !$pstatus->{ppid}) {
      0        
117 0           $self->error("Status file does not have some necessary variables");
118 0           return ();
119             }
120 0           my $parent_pid = $pstatus->{ppid};
121              
122             # Begin compiling stats
123 0           my @samples = ();
124             push @samples,
125             MetricSamples('plack_uptime', gauge => 'Uptime of Plack server',
126 0           [Sample('plack_uptime', $self->labels, (time - $pstatus->{uptime}))]);
127              
128             # Compile request counter stats
129 0 0         if ($counter_fh) {
130 0           seek $counter_fh, 10, 0;
131 0           sysread $counter_fh, my $counter, 20;
132 0           sysread $counter_fh, my $total_bytes, 20;
133 1     1   7 no warnings;
  1         2  
  1         509  
134 0           $counter += 0;
135 0           $total_bytes += 0;
136 0           my $total_kbytes = int($total_bytes / 1_000);
137 0           push @samples,
138             MetricSamples('plack_number_served_requests', gauge => 'Number of requests served by Plack process',
139             [Sample('plack_number_served_requests', $self->labels, $counter)]);
140 0           push @samples,
141             MetricSamples('plack_total_kbytes_served', gauge => 'Total Kilobytes served by Plack process',
142             [Sample('plack_total_kbytes_served', $self->labels, $total_kbytes)]);
143             }
144              
145             # Obtain all worker process IDs
146 0           @all_workers = ();
147 0 0         my $psopt = $^O =~ m/bsd$/ ? '-ax' : '-e';
148 0           my $ps = `LC_ALL=C command ps $psopt -o ppid,pid`;
149 0           $ps =~ s/^\s+//mg;
150 0           for my $line (split /\n/, $ps) {
151 0 0         next if $line =~ m/^\D/;
152 0           my ($ppid, $pid) = split /\s+/, $line, 2;
153 0 0         push @all_workers, $pid if $ppid == $parent_pid;
154             }
155              
156             # Count busy and idle workers
157 0           my $idle = 0;
158 0           my $busy = 0;
159 0           my @process_status;
160 0           for my $pid (@all_workers) {
161 0           my $json = $stats->{$pid};
162 0           $pstatus = eval {
163 0   0       $JSON->decode($json || '{}');
164             };
165 0   0       $pstatus ||= {};
166 0 0 0       if ($pstatus->{status} && $pstatus->{status} eq 'A') {
167 0           $busy++;
168             }
169             else {
170 0           $idle++;
171             }
172              
173 0 0         if (defined $pstatus->{time}) {
174 0           $pstatus->{ss} = time - $pstatus->{time};
175             }
176 0   0       $pstatus->{pid} ||= $pid;
177 0           delete $pstatus->{time};
178 0           delete $pstatus->{ppid};
179 0           delete $pstatus->{uptime};
180 0           push @process_status, $pstatus;
181             }
182 0           push @samples,
183             MetricSamples('plack_busy_workers', gauge => 'Number of busy Plack workers',
184             [Sample('plack_busy_workers', $self->labels, $busy)]);
185 0           push @samples,
186             MetricSamples('plack_idle_workers', gauge => 'Number of idle Plack workers',
187             [Sample('plack_idle_workers', $self->labels, $idle)]);
188              
189 0           $stats = {};
190 0           foreach my $pstatus (@process_status) {
191 0           foreach my $stat (qw(method uri remote_addr protocol)) {
192 0 0         $stats->{$stat}{$pstatus->{$stat}}++ if $pstatus->{$stat};
193             }
194             }
195 0           foreach my $stat (qw(method uri remote_addr protocol)) {
196 0           my $stat_counts = $stats->{$stat};
197             push @samples,
198             MetricSamples("plack_sample_$stat", gauge => "Count of $stat for sample of requests",
199             [
200             map {
201 0           Sample(
202             "plack_sample_$stat",
203 0           [@{$self->labels}, $stat => $_],
204 0           $stat_counts->{$_}
205             )
206             }
207             (keys %$stat_counts)
208             ]
209             );
210             }
211              
212 0           return @samples;
213             }
214              
215             1;
216             __END__
217              
218             =encoding utf-8
219              
220             =head1 SYNOPSIS
221              
222             use Net::Prometheus::ServerStatusLiteCollector;
223              
224             =head1 DESCRIPTION
225              
226             Net::Prometheus::ServerStatusLiteCollector is ...
227              
228             =head1 AUTHOR
229              
230             Steven Leung E<lt>stvleung@gmail.comE<gt>
231              
232             =cut