File Coverage

blib/lib/Net/Prometheus/ServerStatusLiteCollector.pm
Criterion Covered Total %
statement 29 120 24.1
branch 0 30 0.0
condition 0 18 0.0
subroutine 10 16 62.5
pod 0 5 0.0
total 39 189 20.6


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