File Coverage

blib/lib/HealthCheck/Diagnostic.pm
Criterion Covered Total %
statement 109 111 98.2
branch 79 86 91.8
condition 51 59 86.4
subroutine 16 16 100.0
pod 8 8 100.0
total 263 280 93.9


line stmt bran cond sub pod time code
1             package HealthCheck::Diagnostic;
2              
3             # ABSTRACT: A base clase for writing health check diagnositics
4 4     4   560295 use version;
  4         5058  
  4         32  
5             our $VERSION = 'v1.9.2'; # VERSION
6              
7 4     4   1278 use 5.010;
  4         36  
8 4     4   26 use strict;
  4         18  
  4         148  
9 4     4   25 use warnings;
  4         8  
  4         348  
10              
11 4     4   27 use Carp;
  4         8  
  4         427  
12 4     4   1497 use Time::HiRes qw< gettimeofday tv_interval >;
  4         3173  
  4         37  
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 286852 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     114 ? %{ $params[0] } : @params;
  0         0  
160              
161 13         135 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 17 100   17 1 47 return unless ref $_[0]; return shift->{collapse_single_result};
  14         46  
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 103 100   103 1 249 sub tags { return unless ref $_[0]; @{ shift->{tags} || [] } }
  102 100       128  
  102         611  
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 59 50   59 1 133 sub id { return unless ref $_[0]; return shift->{id} }
  59         222  
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 43 50   43 1 92 sub label { return unless ref $_[0]; return shift->{label} }
  43         141  
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 45 50   45 1 91 sub runbook { return unless ref $_[0]; return shift->{runbook} }
  45         110  
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 47     47 1 7333 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 47 50 33     201 ? %{ $params[0] } : @params;
  0         0  
258              
259 47   66     150 my $class = ref $class_or_self || $class_or_self;
260 47 100       592 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 45 100       116 : 1;
267              
268 45         68 local $@;
269 45 100       118 my $start = $params{runtime} ? [ gettimeofday ] : undef;
270 45         116 my @res = eval { $class_or_self->run(%params) };
  45         150  
271 45 100       188 @res = { status => 'CRITICAL', info => "$@" } if $@;
272              
273 45 100 100     280 if ( @res == 1 && ( ref $res[0] || '' ) eq 'HASH' ) { } # noop, OK
    100 100        
274 13         88 elsif ( @res % 2 == 0 ) { @res = {@res}; }
275             else {
276 4         553 carp("Invalid return from $class\->run (@res)");
277 4         38 @res = { status => 'UNKNOWN' };
278             }
279              
280 45 100       121 $res[0]->{runtime} = sprintf "%.03f", tv_interval($start) if $start;
281              
282 45 100       261 return $res[0] unless $summarize;
283 41         132 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 178     178 1 159757 my ( $self, $result ) = @_;
338              
339 178         655 $self->_set_default_fields($result, qw(id label runbook tags));
340              
341 178   100     1002 return $self->_summarize( $result, $result->{id} // 0 );
342             }
343              
344             sub _set_default_fields {
345 209     209   601 my ($self, $target, @fields) = @_;
346 209 100       781 if ( ref $self ) {
347 71 100       137 $target->{$_} = ($_ eq 'tags' ? [ $self->$_ ] : $self->$_) for (
348             grep {
349 191 100       723 !exists($target->{$_}) &&
    100          
350             ($_ eq 'tags' ? scalar($self->$_) : defined($self->$_))
351             }
352             @fields
353             );
354             }
355             }
356              
357             sub _summarize {
358 284     284   609 my ($self, $result, $id) = @_;
359              
360             # Indexes correspond to Nagios Plugin Return Codes
361             # https://assets.nagios.com/downloads/nagioscore/docs/nagioscore/3/en/pluginapi.html
362 284         493 state $forward = [ qw( OK WARNING CRITICAL UNKNOWN ) ];
363              
364             # The order of preference to inherit from a child. The highest priority
365             # has the lowest number.
366 284         484 state $statuses = { map { state $i = 1; $_ => $i++ } qw(
  8         11  
  8         31  
367             CRITICAL
368             WARNING
369             UNKNOWN
370             OK
371             ) };
372              
373 284   100     947 my $status = uc( $result->{status} || '' );
374 284 100       700 $status = '' unless exists $statuses->{$status};
375              
376 284         462 my @results;
377 284 100       645 if ( exists $result->{results} ) {
378 45 100 100     207 if ( ( ref $result->{results} || '' ) eq 'ARRAY' ) {
379 41         65 @results = @{ $result->{results} };
  41         108  
380              
381             # Merge if there is only a single result.
382 41 100 100     133 if ( @results == 1 and $self->collapse_single_result ) {
383 5         9 my ($r) = @{ delete $result->{results} };
  5         14  
384 5         8 %{$result} = ( %{$result}, %{$r} );
  5         14  
  5         14  
  5         13  
385              
386             # Now that we've merged, need to redo everything again
387 5         62 return $self->_summarize($result, $id);
388             }
389             }
390             else {
391             my $disp
392             = defined $result->{results}
393 4 100       15 ? "invalid results '$result->{results}'"
394             : 'undefined results';
395 4         529 carp("Result $id has $disp");
396             }
397             }
398              
399 279         537 my %seen_ids;
400 279         857 foreach my $i ( 0 .. $#results ) {
401 101         173 my $r = $results[$i];
402 101   100     630 $self->_summarize( $r, "$id-" . ( $r->{id} // $i ) );
403              
404             # If this result has an ID we have seen already, append a number
405 101 100 100     465 if ( exists $r->{id} and my $i = $seen_ids{ $r->{id} // '' }++ ) {
      100        
406 10 100 100     52 $r->{id} .= defined $r->{id} && length $r->{id} ? "_$i" : $i;
407             }
408              
409 101 50       248 if ( defined( my $s = $r->{status} ) ) {
410 101         218 $s = uc $s;
411 101 100       346 $s = $forward->[$s] if $s =~ /^[0-3]$/;
412              
413             $status = $s
414             if exists $statuses->{$s}
415 101 100 100     605 and $statuses->{$s} < ( $statuses->{$status} // 5 );
      100        
416             }
417             }
418              
419             # If we've found a valid status in our children,
420             # use that if we don't have our own.
421             # Removing the // here will force "worse" status inheritance
422 279 100 66     1044 $result->{status} //= $status if $status;
423              
424 279         439 my @errors;
425              
426 279 100       635 if ( exists $result->{id} ) {
427 79         135 my $rid = $result->{id};
428 79 100 100     473 unless ( defined $rid and $rid =~ /^[a-z0-9_]+$/ ) {
429 14 100       56 push @errors, defined $rid ? "invalid id '$rid'" : 'undefined id';
430             }
431             }
432              
433 279 100       729 if ( exists $result->{timestamp} ) {
434 125         253 my $ts = $result->{timestamp};
435 125 100 66     1587 unless ( defined $ts and $ts =~ /$rfc3339_timestamp/ ) {
436 121 50       448 my $disp_timestamp
437             = defined $ts
438             ? "invalid timestamp '$ts'"
439             : 'undefined timestamp';
440 121         340 push @errors, "$disp_timestamp";
441             }
442             }
443              
444 279 100 50     1489 if ( not exists $result->{status} ) {
    100          
    100          
445 5         13 push @errors, "missing status";
446             }
447             elsif ( not defined $result->{status} ) {
448 1         3 push @errors, "undefined status";
449             }
450             elsif ( not exists $statuses->{ uc( $result->{status} // '' ) } ) {
451 11         31 push @errors, "invalid status '$result->{status}'";
452             }
453              
454             $result->{status} = 'UNKNOWN'
455 279 100 100     1091 unless defined $result->{status} and length $result->{status};
456              
457 279 100       641 if (@errors) {
458 152         22580 carp("Result $id has $_") for @errors;
459             $result->{status} = 'UNKNOWN'
460             if $result->{status}
461             and $statuses->{ $result->{status} }
462 152 100 100     2403 and $statuses->{UNKNOWN} < $statuses->{ $result->{status} };
      100        
463 152         468 $result->{info} = join "\n", grep {$_} $result->{info}, @errors;
  304         935  
464             }
465              
466 279         2041 return $result;
467             }
468              
469             1;
470              
471             __END__