File Coverage

blib/lib/HealthCheck/Diagnostic.pm
Criterion Covered Total %
statement 109 111 98.2
branch 72 78 92.3
condition 51 59 86.4
subroutine 15 15 100.0
pod 7 7 100.0
total 254 270 94.0


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   226412 use version;
  2         3950  
  2         11  
5             our $VERSION = 'v1.7.0'; # VERSION
6              
7 2     2   210 use 5.010;
  2         13  
8 2     2   11 use strict;
  2         4  
  2         38  
9 2     2   10 use warnings;
  2         4  
  2         46  
10              
11 2     2   9 use Carp;
  2         4  
  2         117  
12 2     2   1108 use Time::HiRes qw< gettimeofday tv_interval >;
  2         2979  
  2         10  
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 results => [ {
110             #pod label => "Sub Label",
111             #pod status => "OK",
112             #pod } ]
113             #pod }
114             #pod
115             #pod Collapses to:
116             #pod
117             #pod { id => "my_id",
118             #pod label => "Sub Label",
119             #pod status => "OK",
120             #pod }
121             #pod
122             #pod
123             #pod =item tags
124             #pod
125             #pod An arrayref used as the default set of tags for any checks that don't
126             #pod override them.
127             #pod
128             #pod =back
129             #pod
130             #pod Any other parameters are included in the "Result" hashref returned.
131             #pod
132             #pod Some recommended things to include are:
133             #pod
134             #pod =over
135             #pod
136             #pod =item id
137             #pod
138             #pod The unique id for this check.
139             #pod
140             #pod =item label
141             #pod
142             #pod A human readable name for this check.
143             #pod
144             #pod =back
145             #pod
146             #pod =cut
147              
148             sub new {
149 13     13 1 13297 my ($class, @params) = @_;
150              
151             # Allow either a hashref or even-sized list of params
152             my %params = @params == 1 && ( ref $params[0] || '' ) eq 'HASH'
153 13 50 33     62 ? %{ $params[0] } : @params;
  0         0  
154              
155 13         87 bless \%params, $class;
156             }
157              
158             #pod =head2 collapse_single_result
159             #pod
160             #pod Read only accessor for the C attribute.
161             #pod
162             #pod =cut
163              
164             sub collapse_single_result {
165 16 100   16 1 41 return unless ref $_[0]; return shift->{collapse_single_result};
  13         42  
166             }
167              
168             #pod =head2 tags
169             #pod
170             #pod Read only accessor that returns the list of tags registered with this object.
171             #pod
172             #pod =cut
173              
174 8 100   8 1 27 sub tags { return unless ref $_[0]; @{ shift->{tags} || [] } }
  7 100       9  
  7         58  
175              
176             #pod =head2 id
177             #pod
178             #pod Read only accessor that returns the id registered with this object.
179             #pod
180             #pod =cut
181              
182 5 50   5 1 14 sub id { return unless ref $_[0]; return shift->{id} }
  5         12  
183              
184             #pod =head2 label
185             #pod
186             #pod Read only accessor that returns the label registered with this object.
187             #pod
188             #pod =cut
189              
190 5 50   5 1 12 sub label { return unless ref $_[0]; return shift->{label} }
  5         11  
191              
192             #pod =head2 check
193             #pod
194             #pod my %results = %{ $diagnostic->check(%params) }
195             #pod
196             #pod This method is what is normally called by the L runner,
197             #pod but this version expects you to implement a L method for the
198             #pod body of your diagnostic.
199             #pod This thin wrapper
200             #pod makes sure C<%params> is an even-sided list (possibly unpacking a hashref)
201             #pod before passing it to L,
202             #pod trapping any exceptions,
203             #pod and passing the return value through L unless a falsy
204             #pod C parameter is passed.
205             #pod
206             #pod This could be used to validate parameters or to modify the the return value
207             #pod in some way.
208             #pod
209             #pod sub check {
210             #pod my ( $self, @params ) = @_;
211             #pod
212             #pod # Require check as an instance method
213             #pod croak("check cannot be called as a class method") unless ref $self;
214             #pod
215             #pod # Allow either a hashref or even-sized list of params
216             #pod my %params = @params == 1 && ( ref $params[0] || '' ) eq 'HASH'
217             #pod ? %{ $params[0] } : @params;
218             #pod
219             #pod # Validate any required parameters and that they look right.
220             #pod my $required_param = $params{required} || $self->{required};
221             #pod return {
222             #pod status => 'UNKNOWN',
223             #pod info => 'The "required" parameter is required',
224             #pod } unless $required_param and ref $required_param == 'HASH';
225             #pod
226             #pod # Calls $self->run and then passes the result through $self->summarize
227             #pod my $res = $self->SUPER::check( %params, required => $required_param );
228             #pod
229             #pod # Modify the result after it has been summarized
230             #pod delete $res->{required};
231             #pod
232             #pod # and return it
233             #pod return $res;
234             #pod }
235             #pod
236             #pod =cut
237              
238             sub check {
239 45     45 1 5685 my ( $class_or_self, @params ) = @_;
240              
241             # Allow either a hashref or even-sized list of params
242             my %params = @params == 1 && ( ref $params[0] || '' ) eq 'HASH'
243 45 50 33     175 ? %{ $params[0] } : @params;
  0         0  
244              
245 45   66     131 my $class = ref $class_or_self || $class_or_self;
246 45 100       453 croak("$class does not implement a 'run' method")
247             unless $class_or_self->can('run');
248              
249             my $summarize
250             = exists $params{summarize_result}
251             ? $params{summarize_result}
252 43 100       112 : 1;
253              
254 43         62 local $@;
255 43 100       93 my $start = $params{runtime} ? [ gettimeofday ] : undef;
256 43         80 my @res = eval { local $SIG{__DIE__}; $class_or_self->run(%params) };
  43         131  
  43         137  
257 43 100       193 @res = { status => 'CRITICAL', info => "$@" } if $@;
258              
259 43 100 100     228 if ( @res == 1 && ( ref $res[0] || '' ) eq 'HASH' ) { } # noop, OK
    100 100        
260 12         43 elsif ( @res % 2 == 0 ) { @res = {@res}; }
261             else {
262 4         403 carp("Invalid return from $class\->run (@res)");
263 4         87 @res = { status => 'UNKNOWN' };
264             }
265              
266 43 100       104 $res[0]->{runtime} = sprintf "%.03f", tv_interval($start) if $start;
267              
268 43 100       191 return $res[0] unless $summarize;
269 39         102 return $class_or_self->summarize(@res);
270             }
271              
272             #pod =head2 summarize
273             #pod
274             #pod %result = %{ $diagnostic->summarize( \%result ) };
275             #pod
276             #pod Validates, pre-formats, and returns the C so that it is easily
277             #pod usable by HealthCheck.
278             #pod
279             #pod The attributes C, C
280             #pod get copied from the C<$diagnostic> into the C
281             #pod if they exist in the former and not in the latter.
282             #pod
283             #pod The C and C are summarized when we have multiple
284             #pod C in the C. All of the C values get appended
285             #pod together. One C value is selected from the list of C
286             #pod values.
287             #pod
288             #pod Used by L.
289             #pod
290             #pod Carps a warning if validation fails on several keys, and sets the
291             #pod C from C to C.
292             #pod
293             #pod =over
294             #pod
295             #pod =item status
296             #pod
297             #pod Expects it to be one of C, C, C, or C.
298             #pod
299             #pod Also carps if it does not exist.
300             #pod
301             #pod =item results
302             #pod
303             #pod Complains if it is not an arrayref.
304             #pod
305             #pod =item id
306             #pod
307             #pod Complains if the id contains anything but
308             #pod lowercase ascii letters, numbers, and underscores.
309             #pod
310             #pod =item timestamp
311             #pod
312             #pod Expected to look like an
313             #pod L
314             #pod which is a more strict subset of an ISO8601 timestamp.
315             #pod
316             #pod =back
317             #pod
318             #pod Modifies the passed in hashref in-place.
319             #pod
320             #pod =cut
321              
322             sub summarize {
323 176     176 1 112471 my ( $self, $result ) = @_;
324              
325 176         492 $self->_set_default_fields($result, qw(id label tags));
326              
327 176   100     780 return $self->_summarize( $result, $result->{id} // 0 );
328             }
329              
330             sub _set_default_fields {
331 206     206   548 my ($self, $target, @fields) = @_;
332 206 100       591 if ( ref $self ) {
333             $target->{$_} = $self->{$_}
334 68         126 for grep { not exists $target->{$_} }
  48         160  
335 144         397 grep { exists $self->{$_} } @fields;
336             }
337             }
338              
339             sub _summarize {
340 280     280   567 my ($self, $result, $id) = @_;
341              
342             # Indexes correspond to Nagios Plugin Return Codes
343             # https://assets.nagios.com/downloads/nagioscore/docs/nagioscore/3/en/pluginapi.html
344 280         415 state $forward = [ qw( OK WARNING CRITICAL UNKNOWN ) ];
345              
346             # The order of preference to inherit from a child. The highest priority
347             # has the lowest number.
348 280         405 state $statuses = { map { state $i = 1; $_ => $i++ } qw(
  8         13  
  8         40  
349             CRITICAL
350             WARNING
351             UNKNOWN
352             OK
353             ) };
354              
355 280   100     727 my $status = uc( $result->{status} || '' );
356 280 100       573 $status = '' unless exists $statuses->{$status};
357              
358 280         382 my @results;
359 280 100       506 if ( exists $result->{results} ) {
360 44 100 100     135 if ( ( ref $result->{results} || '' ) eq 'ARRAY' ) {
361 40         60 @results = @{ $result->{results} };
  40         96  
362              
363             # Merge if there is only a single result.
364 40 100 100     121 if ( @results == 1 and $self->collapse_single_result ) {
365 5         9 my ($r) = @{ delete $result->{results} };
  5         13  
366 5         8 %{$result} = ( %{$result}, %{$r} );
  5         14  
  5         13  
  5         13  
367              
368             # Now that we've merged, need to redo everything again
369 5         41 return $self->_summarize($result, $id);
370             }
371             }
372             else {
373             my $disp
374             = defined $result->{results}
375 4 100       14 ? "invalid results '$result->{results}'"
376             : 'undefined results';
377 4         361 carp("Result $id has $disp");
378             }
379             }
380              
381 275         495 my %seen_ids;
382 275         655 foreach my $i ( 0 .. $#results ) {
383 99         156 my $r = $results[$i];
384 99   100     497 $self->_summarize( $r, "$id-" . ( $r->{id} // $i ) );
385              
386             # If this result has an ID we have seen already, append a number
387 99 100 100     408 if ( exists $r->{id} and my $i = $seen_ids{ $r->{id} // '' }++ ) {
      100        
388 10 100 100     40 $r->{id} .= defined $r->{id} && length $r->{id} ? "_$i" : $i;
389             }
390              
391 99 50       214 if ( defined( my $s = $r->{status} ) ) {
392 99         163 $s = uc $s;
393 99 100       262 $s = $forward->[$s] if $s =~ /^[0-3]$/;
394              
395             $status = $s
396             if exists $statuses->{$s}
397 99 100 100     430 and $statuses->{$s} < ( $statuses->{$status} // 5 );
      100        
398             }
399             }
400              
401             # If we've found a valid status in our children,
402             # use that if we don't have our own.
403             # Removing the // here will force "worse" status inheritance
404 275 100 66     777 $result->{status} //= $status if $status;
405              
406 275         381 my @errors;
407              
408 275 100       475 if ( exists $result->{id} ) {
409 75         119 my $rid = $result->{id};
410 75 100 100     388 unless ( defined $rid and $rid =~ /^[a-z0-9_]+$/ ) {
411 14 100       44 push @errors, defined $rid ? "invalid id '$rid'" : 'undefined id';
412             }
413             }
414              
415 275 100       546 if ( exists $result->{timestamp} ) {
416 125         203 my $ts = $result->{timestamp};
417 125 100 66     1071 unless ( defined $ts and $ts =~ /$rfc3339_timestamp/ ) {
418 121 50       356 my $disp_timestamp
419             = defined $ts
420             ? "invalid timestamp '$ts'"
421             : 'undefined timestamp';
422 121         285 push @errors, "$disp_timestamp";
423             }
424             }
425              
426 275 100 50     1053 if ( not exists $result->{status} ) {
    100          
    100          
427 4         13 push @errors, "missing status";
428             }
429             elsif ( not defined $result->{status} ) {
430 1         4 push @errors, "undefined status";
431             }
432             elsif ( not exists $statuses->{ uc( $result->{status} // '' ) } ) {
433 11         29 push @errors, "invalid status '$result->{status}'";
434             }
435              
436             $result->{status} = 'UNKNOWN'
437 275 100 100     947 unless defined $result->{status} and length $result->{status};
438              
439 275 100       559 if (@errors) {
440 151         14543 carp("Result $id has $_") for @errors;
441             $result->{status} = 'UNKNOWN'
442             if $result->{status}
443             and $statuses->{ $result->{status} }
444 151 100 100     5293 and $statuses->{UNKNOWN} < $statuses->{ $result->{status} };
      100        
445 151         344 $result->{info} = join "\n", grep {$_} $result->{info}, @errors;
  302         650  
446             }
447              
448 275         1409 return $result;
449             }
450              
451             1;
452              
453             __END__