File Coverage

blib/lib/HealthCheck/Diagnostic.pm
Criterion Covered Total %
statement 110 112 98.2
branch 73 80 91.2
condition 51 59 86.4
subroutine 16 16 100.0
pod 8 8 100.0
total 258 275 93.8


line stmt bran cond sub pod time code
1             package HealthCheck::Diagnostic;
2              
3             # ABSTRACT: A base clase for writing health check diagnositics
4 2     2   227128 use version;
  2         3911  
  2         10  
5             our $VERSION = 'v1.8.0'; # VERSION
6              
7 2     2   205 use 5.010;
  2         14  
8 2     2   11 use strict;
  2         4  
  2         36  
9 2     2   9 use warnings;
  2         4  
  2         44  
10              
11 2     2   10 use Carp;
  2         3  
  2         113  
12 2     2   1692 use Time::HiRes qw< gettimeofday tv_interval >;
  2         2993  
  2         9  
13              
14             my $rfc3339_timestamp = qr/^(?:
15             (?P[0-9]{4})-
16             (?P1[0-2]|0[1-9])-
17             (?P3[01]|0[1-9]|[12][0-9])
18             [tT ]
19             (?P2[0-3]|[01][0-9]):
20             (?P[0-5][0-9]):
21             (?P[0-5][0-9]|60)
22             (?: \. (?P[0-9]+) )?
23             (? [-+][0-9]{2}:[0-9]{2} | [zZ] )
24             )$/x;
25              
26             #pod =head1 SYNOPSIS
27             #pod
28             #pod package HealthCheck::Diagnostic::Sample;
29             #pod use parent 'HealthCheck::Diagnostic';
30             #pod
31             #pod # Required implementation of the check
32             #pod # or you can override the 'check' method and avoid the
33             #pod # automatic call to 'summarize'
34             #pod sub run {
35             #pod my ( $class_or_self, %params ) = @_;
36             #pod
37             #pod # will be passed to 'summarize' by 'check'
38             #pod return { %params, status => 'OK' };
39             #pod }
40             #pod
41             #pod You can then either instantiate an instance and run the check.
42             #pod
43             #pod my $diagnostic = HealthCheck::Diagnostic::Sample->new( id => 'my_id' );
44             #pod my $result = $diagnostic->check;
45             #pod
46             #pod Or as a class method.
47             #pod
48             #pod my $result = HealthCheck::Diagnostic::Sample->check();
49             #pod
50             #pod Set C to a truthy value in the params for check and the
51             #pod time spent checking will be returned in the results.
52             #pod
53             #pod my $result = HealthCheck::Diagnostic::Sample->check( runtime => 1 );
54             #pod
55             #pod =head1 DESCRIPTION
56             #pod
57             #pod A base class for writing Health Checks.
58             #pod Provides some helpers for validation of results returned from the check.
59             #pod
60             #pod This module does not require that an instance is created to run checks against.
61             #pod If your code requires an instance, you will need to verify that yourself.
62             #pod
63             #pod Results returned by these checks should correspond to the GSG
64             #pod L.
65             #pod
66             #pod Implementing a diagnostic should normally be done in L
67             #pod to allow use of the helper features that L provides.
68             #pod
69             #pod =head1 REQUIRED METHODS
70             #pod
71             #pod =head2 run
72             #pod
73             #pod sub run {
74             #pod my ( $class_or_self, %params ) = @_;
75             #pod return { %params, status => 'OK' };
76             #pod }
77             #pod
78             #pod A subclass must either implement a C method,
79             #pod which will be called by L
80             #pod have its return value passed through L,
81             #pod or override C and handle all validation itself.
82             #pod
83             #pod See the L method documentation for suggestions on when it
84             #pod might be overridden.
85             #pod
86             #pod =head1 METHODS
87             #pod
88             #pod =head2 new
89             #pod
90             #pod my $diagnostic
91             #pod = HealthCheck::Diagnostic::Sample->new( id => 'my_diagnostic' );
92             #pod
93             #pod =head3 ATTRIBUTES
94             #pod
95             #pod Attributes set on the object created will be copied into the result
96             #pod by L, without overriding anything already set in the result.
97             #pod
98             #pod =over
99             #pod
100             #pod =item collapse_single_result
101             #pod
102             #pod If truthy, will collapse a single sub-result into the current result,
103             #pod with the child result overwriting the values from the parent.
104             #pod
105             #pod For example:
106             #pod
107             #pod { id => "my_id",
108             #pod label => "My Label",
109             #pod runbook => "https://grantstreetgroup.github.io/HealthCheck.html",
110             #pod results => [ {
111             #pod label => "Sub Label",
112             #pod status => "OK",
113             #pod } ]
114             #pod }
115             #pod
116             #pod Collapses to:
117             #pod
118             #pod { id => "my_id",
119             #pod label => "Sub Label",
120             #pod runbook => "https://grantstreetgroup.github.io/HealthCheck.html",
121             #pod status => "OK",
122             #pod }
123             #pod
124             #pod
125             #pod =item tags
126             #pod
127             #pod An arrayref used as the default set of tags for any checks that don't
128             #pod override them.
129             #pod
130             #pod =back
131             #pod
132             #pod Any other parameters are included in the "Result" hashref returned.
133             #pod
134             #pod Some recommended things to include are:
135             #pod
136             #pod =over
137             #pod
138             #pod =item id
139             #pod
140             #pod The unique id for this check.
141             #pod
142             #pod =item label
143             #pod
144             #pod A human readable name for this check.
145             #pod
146             #pod =item runbook
147             #pod
148             #pod A runbook link to help troubleshooting if the status is not OK.
149             #pod
150             #pod =back
151             #pod
152             #pod =cut
153              
154             sub new {
155 13     13 1 13252 my ($class, @params) = @_;
156              
157             # Allow either a hashref or even-sized list of params
158             my %params = @params == 1 && ( ref $params[0] || '' ) eq 'HASH'
159 13 50 33     63 ? %{ $params[0] } : @params;
  0         0  
160              
161 13         81 bless \%params, $class;
162             }
163              
164             #pod =head2 collapse_single_result
165             #pod
166             #pod Read only accessor for the C attribute.
167             #pod
168             #pod =cut
169              
170             sub collapse_single_result {
171 16 100   16 1 43 return unless ref $_[0]; return shift->{collapse_single_result};
  13         38  
172             }
173              
174             #pod =head2 tags
175             #pod
176             #pod Read only accessor that returns the list of tags registered with this object.
177             #pod
178             #pod =cut
179              
180 8 100   8 1 24 sub tags { return unless ref $_[0]; @{ shift->{tags} || [] } }
  7 100       11  
  7         46  
181              
182             #pod =head2 id
183             #pod
184             #pod Read only accessor that returns the id registered with this object.
185             #pod
186             #pod =cut
187              
188 5 50   5 1 13 sub id { return unless ref $_[0]; return shift->{id} }
  5         15  
189              
190             #pod =head2 label
191             #pod
192             #pod Read only accessor that returns the label registered with this object.
193             #pod
194             #pod =cut
195              
196 5 50   5 1 27 sub label { return unless ref $_[0]; return shift->{label} }
  5         13  
197              
198             #pod =head2 runbook
199             #pod
200             #pod Read only accessor that returns the runbook registered with this object.
201             #pod
202             #pod =cut
203              
204 7 50   7 1 16 sub runbook { return unless ref $_[0]; return shift->{runbook} }
  7         15  
205              
206             #pod =head2 check
207             #pod
208             #pod my %results = %{ $diagnostic->check(%params) }
209             #pod
210             #pod This method is what is normally called by the L runner,
211             #pod but this version expects you to implement a L method for the
212             #pod body of your diagnostic.
213             #pod This thin wrapper
214             #pod makes sure C<%params> is an even-sided list (possibly unpacking a hashref)
215             #pod before passing it to L,
216             #pod trapping any exceptions,
217             #pod and passing the return value through L unless a falsy
218             #pod C parameter is passed.
219             #pod
220             #pod This could be used to validate parameters or to modify the the return value
221             #pod in some way.
222             #pod
223             #pod sub check {
224             #pod my ( $self, @params ) = @_;
225             #pod
226             #pod # Require check as an instance method
227             #pod croak("check cannot be called as a class method") unless ref $self;
228             #pod
229             #pod # Allow either a hashref or even-sized list of params
230             #pod my %params = @params == 1 && ( ref $params[0] || '' ) eq 'HASH'
231             #pod ? %{ $params[0] } : @params;
232             #pod
233             #pod # Validate any required parameters and that they look right.
234             #pod my $required_param = $params{required} || $self->{required};
235             #pod return {
236             #pod status => 'UNKNOWN',
237             #pod info => 'The "required" parameter is required',
238             #pod } unless $required_param and ref $required_param == 'HASH';
239             #pod
240             #pod # Calls $self->run and then passes the result through $self->summarize
241             #pod my $res = $self->SUPER::check( %params, required => $required_param );
242             #pod
243             #pod # Modify the result after it has been summarized
244             #pod delete $res->{required};
245             #pod
246             #pod # and return it
247             #pod return $res;
248             #pod }
249             #pod
250             #pod =cut
251              
252             sub check {
253 45     45 1 5630 my ( $class_or_self, @params ) = @_;
254              
255             # Allow either a hashref or even-sized list of params
256             my %params = @params == 1 && ( ref $params[0] || '' ) eq 'HASH'
257 45 50 33     187 ? %{ $params[0] } : @params;
  0         0  
258              
259 45   66     135 my $class = ref $class_or_self || $class_or_self;
260 45 100       455 croak("$class does not implement a 'run' method")
261             unless $class_or_self->can('run');
262              
263             my $summarize
264             = exists $params{summarize_result}
265             ? $params{summarize_result}
266 43 100       100 : 1;
267              
268 43         85 local $@;
269 43 100       95 my $start = $params{runtime} ? [ gettimeofday ] : undef;
270 43         75 my @res = eval { $class_or_self->run(%params) };
  43         126  
271 43 100       161 @res = { status => 'CRITICAL', info => "$@" } if $@;
272              
273 43 100 100     202 if ( @res == 1 && ( ref $res[0] || '' ) eq 'HASH' ) { } # noop, OK
    100 100        
274 12         47 elsif ( @res % 2 == 0 ) { @res = {@res}; }
275             else {
276 4         343 carp("Invalid return from $class\->run (@res)");
277 4         83 @res = { status => 'UNKNOWN' };
278             }
279              
280 43 100       121 $res[0]->{runtime} = sprintf "%.03f", tv_interval($start) if $start;
281              
282 43 100       179 return $res[0] unless $summarize;
283 39         109 return $class_or_self->summarize(@res);
284             }
285              
286             #pod =head2 summarize
287             #pod
288             #pod %result = %{ $diagnostic->summarize( \%result ) };
289             #pod
290             #pod Validates, pre-formats, and returns the C so that it is easily
291             #pod usable by HealthCheck.
292             #pod
293             #pod The attributes C, C
294             #pod get copied from the C<$diagnostic> into the C
295             #pod if they exist in the former and not in the latter.
296             #pod
297             #pod The C and C are summarized when we have multiple
298             #pod C in the C. All of the C values get appended
299             #pod together. One C value is selected from the list of C
300             #pod values.
301             #pod
302             #pod Used by L.
303             #pod
304             #pod Carps a warning if validation fails on several keys, and sets the
305             #pod C from C to C.
306             #pod
307             #pod =over
308             #pod
309             #pod =item status
310             #pod
311             #pod Expects it to be one of C, C, C, or C.
312             #pod
313             #pod Also carps if it does not exist.
314             #pod
315             #pod =item results
316             #pod
317             #pod Complains if it is not an arrayref.
318             #pod
319             #pod =item id
320             #pod
321             #pod Complains if the id contains anything but
322             #pod lowercase ascii letters, numbers, and underscores.
323             #pod
324             #pod =item timestamp
325             #pod
326             #pod Expected to look like an
327             #pod L
328             #pod which is a more strict subset of an ISO8601 timestamp.
329             #pod
330             #pod =back
331             #pod
332             #pod Modifies the passed in hashref in-place.
333             #pod
334             #pod =cut
335              
336             sub summarize {
337 176     176 1 111264 my ( $self, $result ) = @_;
338              
339 176         492 $self->_set_default_fields($result, qw(id label runbook tags));
340              
341 176   100     779 return $self->_summarize( $result, $result->{id} // 0 );
342             }
343              
344             sub _set_default_fields {
345 206     206   492 my ($self, $target, @fields) = @_;
346 206 100       603 if ( ref $self ) {
347             $target->{$_} = $self->{$_}
348 68         122 for grep { not exists $target->{$_} }
  62         182  
349 182         456 grep { exists $self->{$_} } @fields;
350             }
351             }
352              
353             sub _summarize {
354 280     280   534 my ($self, $result, $id) = @_;
355              
356             # Indexes correspond to Nagios Plugin Return Codes
357             # https://assets.nagios.com/downloads/nagioscore/docs/nagioscore/3/en/pluginapi.html
358 280         405 state $forward = [ qw( OK WARNING CRITICAL UNKNOWN ) ];
359              
360             # The order of preference to inherit from a child. The highest priority
361             # has the lowest number.
362 280         351 state $statuses = { map { state $i = 1; $_ => $i++ } qw(
  8         14  
  8         20  
363             CRITICAL
364             WARNING
365             UNKNOWN
366             OK
367             ) };
368              
369 280   100     718 my $status = uc( $result->{status} || '' );
370 280 100       572 $status = '' unless exists $statuses->{$status};
371              
372 280         371 my @results;
373 280 100       531 if ( exists $result->{results} ) {
374 44 100 100     131 if ( ( ref $result->{results} || '' ) eq 'ARRAY' ) {
375 40         62 @results = @{ $result->{results} };
  40         89  
376              
377             # Merge if there is only a single result.
378 40 100 100     125 if ( @results == 1 and $self->collapse_single_result ) {
379 5         9 my ($r) = @{ delete $result->{results} };
  5         11  
380 5         10 %{$result} = ( %{$result}, %{$r} );
  5         12  
  5         11  
  5         12  
381              
382             # Now that we've merged, need to redo everything again
383 5         80 return $self->_summarize($result, $id);
384             }
385             }
386             else {
387             my $disp
388             = defined $result->{results}
389 4 100       13 ? "invalid results '$result->{results}'"
390             : 'undefined results';
391 4         371 carp("Result $id has $disp");
392             }
393             }
394              
395 275         483 my %seen_ids;
396 275         665 foreach my $i ( 0 .. $#results ) {
397 99         153 my $r = $results[$i];
398 99   100     488 $self->_summarize( $r, "$id-" . ( $r->{id} // $i ) );
399              
400             # If this result has an ID we have seen already, append a number
401 99 100 100     387 if ( exists $r->{id} and my $i = $seen_ids{ $r->{id} // '' }++ ) {
      100        
402 10 100 100     42 $r->{id} .= defined $r->{id} && length $r->{id} ? "_$i" : $i;
403             }
404              
405 99 50       224 if ( defined( my $s = $r->{status} ) ) {
406 99         165 $s = uc $s;
407 99 100       265 $s = $forward->[$s] if $s =~ /^[0-3]$/;
408              
409             $status = $s
410             if exists $statuses->{$s}
411 99 100 100     459 and $statuses->{$s} < ( $statuses->{$status} // 5 );
      100        
412             }
413             }
414              
415             # If we've found a valid status in our children,
416             # use that if we don't have our own.
417             # Removing the // here will force "worse" status inheritance
418 275 100 66     818 $result->{status} //= $status if $status;
419              
420 275         411 my @errors;
421              
422 275 100       525 if ( exists $result->{id} ) {
423 75         113 my $rid = $result->{id};
424 75 100 100     397 unless ( defined $rid and $rid =~ /^[a-z0-9_]+$/ ) {
425 14 100       44 push @errors, defined $rid ? "invalid id '$rid'" : 'undefined id';
426             }
427             }
428              
429 275 100       514 if ( exists $result->{timestamp} ) {
430 125         206 my $ts = $result->{timestamp};
431 125 100 66     1056 unless ( defined $ts and $ts =~ /$rfc3339_timestamp/ ) {
432 121 50       396 my $disp_timestamp
433             = defined $ts
434             ? "invalid timestamp '$ts'"
435             : 'undefined timestamp';
436 121         306 push @errors, "$disp_timestamp";
437             }
438             }
439              
440 275 100 50     1029 if ( not exists $result->{status} ) {
    100          
    100          
441 4         11 push @errors, "missing status";
442             }
443             elsif ( not defined $result->{status} ) {
444 1         2 push @errors, "undefined status";
445             }
446             elsif ( not exists $statuses->{ uc( $result->{status} // '' ) } ) {
447 11         29 push @errors, "invalid status '$result->{status}'";
448             }
449              
450             $result->{status} = 'UNKNOWN'
451 275 100 100     903 unless defined $result->{status} and length $result->{status};
452              
453 275 100       540 if (@errors) {
454 151         14219 carp("Result $id has $_") for @errors;
455             $result->{status} = 'UNKNOWN'
456             if $result->{status}
457             and $statuses->{ $result->{status} }
458 151 100 100     5237 and $statuses->{UNKNOWN} < $statuses->{ $result->{status} };
      100        
459 151         325 $result->{info} = join "\n", grep {$_} $result->{info}, @errors;
  302         740  
460             }
461              
462 275         1454 return $result;
463             }
464              
465             1;
466              
467             __END__