File Coverage

blib/lib/HealthCheck.pm
Criterion Covered Total %
statement 145 145 100.0
branch 74 80 92.5
condition 28 34 82.3
subroutine 20 20 100.0
pod 6 6 100.0
total 273 285 95.7


line stmt bran cond sub pod time code
1             package HealthCheck;
2 2     2   433095 use parent 'HealthCheck::Diagnostic';
  2         354  
  2         18  
3              
4             # ABSTRACT: A health check for your code
5 2     2   105 use version;
  2         3  
  2         9  
6             our $VERSION = 'v1.9.2'; # VERSION
7              
8 2     2   231 use 5.010;
  2         7  
9 2     2   8 use strict;
  2         4  
  2         38  
10 2     2   9 use warnings;
  2         3  
  2         104  
11              
12 2     2   9 use Carp;
  2         4  
  2         143  
13              
14 2     2   1253 use Hash::Util::FieldHash;
  2         4061  
  2         123  
15 2     2   41 use List::Util qw(any uniq);
  2         4  
  2         4526  
16              
17             # Create a place outside of $self to store the checks
18             # as everything in the self hashref will be copied into
19             # the result.
20             Hash::Util::FieldHash::fieldhash my %registered_checks;
21              
22             #pod =head1 SYNOPSIS
23             #pod
24             #pod use HealthCheck;
25             #pod
26             #pod # a check can return a hashref containing anything at all,
27             #pod # however some values are special.
28             #pod # See the HealthCheck Standard for details.
29             #pod sub my_check {
30             #pod return {
31             #pod anything => "at all",
32             #pod id => "my_check",
33             #pod status => 'WARNING',
34             #pod };
35             #pod }
36             #pod
37             #pod my $checker = HealthCheck->new(
38             #pod id => 'main_checker',
39             #pod label => 'Main Health Check',
40             #pod runbook => 'https://grantstreetgroup.github.io/HealthCheck.html',
41             #pod tags => [qw( fast cheap )],
42             #pod checks => [
43             #pod sub { return { id => 'coderef', status => 'OK' } },
44             #pod 'my_check', # Name of a method on caller
45             #pod ],
46             #pod );
47             #pod
48             #pod my $other_checker = HealthCheck->new(
49             #pod id => 'my_health_check',
50             #pod label => "My Health Check",
51             #pod runbook => 'https://grantstreetgroup.github.io/HealthCheck.html',
52             #pod tags => [qw( cheap easy )],
53             #pod other => "Other details to pass to the check call",
54             #pod )->register(
55             #pod 'My::Checker', # Name of a loaded class that ->can("check")
56             #pod My::Checker->new, # Object that ->can("check")
57             #pod );
58             #pod
59             #pod # It's possible to add ids, labels, and tags to your checks
60             #pod # and they will be copied to the Result.
61             #pod $other_checker->register( My::Checker->new(
62             #pod id => 'my_checker',
63             #pod label => 'My Checker',
64             #pod runbook => 'https://grantstreetgroup.github.io/HealthCheck.html',
65             #pod tags => [qw( cheap copied_to_the_result )],
66             #pod ) );
67             #pod
68             #pod # You can add HealthCheck instances as checks
69             #pod # You could add a check to itself to create an infinite loop of checks.
70             #pod $checker->register( $other_checker );
71             #pod
72             #pod # A hashref of the check config
73             #pod # This whole hashref is passed as an argument
74             #pod # to My::Checker->another_check
75             #pod $checker->register( {
76             #pod invocant => 'My::Checker', # to call the "check" on
77             #pod check => 'another_check', # name of the check method
78             #pod runbook => 'https://grantstreetgroup.github.io/HealthCheck.html',
79             #pod tags => [qw( fast easy )],
80             #pod more_params => 'anything',
81             #pod } );
82             #pod
83             #pod my @tags = $checker->tags; # returns fast, cheap
84             #pod
85             #pod my %result = %{ $checker->check( tags => ['cheap'] ) };
86             #pod # OR run the opposite checks
87             #pod %result = %{ $checker->check( tags => ['!cheap'] ) };
88             #pod
89             #pod # A checker class or object just needs to have either
90             #pod # a check method, which is used by default,
91             #pod # or another method as specified in a hash config.
92             #pod package My::Checker;
93             #pod
94             #pod # Optionally subclass HealthCheck::Diagnostic
95             #pod use parent 'HealthCheck::Diagnostic';
96             #pod
97             #pod # and provide a 'run' method, the Diagnostic base class will
98             #pod # pass your results through the 'summarize' helper that
99             #pod # will add warnings about invalid values as well as
100             #pod # summarizing multiple results.
101             #pod sub run {
102             #pod return {
103             #pod id => ( ref $_[0] ? "object_method" : "class_method" ),
104             #pod status => "WARNING",
105             #pod };
106             #pod }
107             #pod
108             #pod # Any checks *must* return a valid "Health Check Result" hashref.
109             #pod
110             #pod # You can add your own check that doesn't call 'summarize'
111             #pod # or, overload the 'check' helper in the parent class.
112             #pod sub another_check {
113             #pod my ($self, %params) = @_;
114             #pod return {
115             #pod id => 'another_check',
116             #pod label => 'A Super custom check',
117             #pod runbook => 'https://grantstreetgroup.github.io/HealthCheck.html',
118             #pod status => ( $params{more_params} eq 'fine' ? "OK" : "CRITICAL" ),
119             #pod };
120             #pod }
121             #pod
122             #pod C<%result> will be from the subset of checks run due to the tags.
123             #pod
124             #pod $checker->check(tags => ['cheap']);
125             #pod
126             #pod id => "main_checker",
127             #pod label => "Main Health Check",
128             #pod runbook => 'https://grantstreetgroup.github.io/HealthCheck.html',
129             #pod tags => [ "fast", "cheap" ],
130             #pod status => "WARNING",
131             #pod results => [
132             #pod { id => "coderef",
133             #pod status => "OK",
134             #pod tags => [ "fast", "cheap" ] # inherited
135             #pod },
136             #pod { anything => "at all",
137             #pod id => "my_check",
138             #pod status => "WARNING",
139             #pod tags => [ "fast", "cheap" ] # inherited
140             #pod },
141             #pod { id => "my_health_check",
142             #pod label => "My Health Check",
143             #pod tags => [ "cheap", "easy" ],
144             #pod status => "WARNING",
145             #pod results => [
146             #pod { id => "class_method",
147             #pod tags => [ "cheap", "easy" ],
148             #pod status => "WARNING",
149             #pod },
150             #pod { id => "object_method",
151             #pod tags => [ "cheap", "easy" ],
152             #pod status => "WARNING",
153             #pod },
154             #pod { id => "object_method_1",
155             #pod label => "My Checker",
156             #pod tags => [ "cheap", "copied_to_the_result" ],
157             #pod status => "WARNING",
158             #pod }
159             #pod ],
160             #pod }
161             #pod ],
162             #pod
163             #pod There is also runtime support,
164             #pod which can be enabled by adding a truthy C param to the C.
165             #pod
166             #pod $checker->check( tags => [ 'easy', '!fast' ], runtime => 1 );
167             #pod
168             #pod id => "my_health_check",
169             #pod label => "My Health Check",
170             #pod runtime => "0.000",
171             #pod runbook => 'https://grantstreetgroup.github.io/HealthCheck.html',
172             #pod tags => [ "cheap", "easy" ],
173             #pod status => "WARNING",
174             #pod results => [
175             #pod { id => "class_method",
176             #pod runtime => "0.000",
177             #pod tags => [ "cheap", "easy" ],
178             #pod status => "WARNING",
179             #pod },
180             #pod { id => "object_method",
181             #pod runtime => "0.000",
182             #pod tags => [ "cheap", "easy" ],
183             #pod status => "WARNING",
184             #pod }
185             #pod ],
186             #pod
187             #pod =head1 DESCRIPTION
188             #pod
189             #pod Allows you to create callbacks that check the health of your application
190             #pod and return a status result.
191             #pod
192             #pod There are several things this is trying to enable:
193             #pod
194             #pod =over
195             #pod
196             #pod =item *
197             #pod
198             #pod A fast HTTP endpoint that can be used to verify that a web app can
199             #pod serve traffic.
200             #pod To this end, it may be useful to use the runtime support option,
201             #pod available in L.
202             #pod
203             #pod =item *
204             #pod A more complete check that verifies all the things work after a deployment.
205             #pod
206             #pod =item *
207             #pod
208             #pod The ability for a script, such as a cronjob, to verify that it's dependencies
209             #pod are available before starting work.
210             #pod
211             #pod =item *
212             #pod
213             #pod Different sorts of monitoring checks that are defined in your codebase.
214             #pod
215             #pod =back
216             #pod
217             #pod Results returned by these checks should correspond to the GSG
218             #pod L.
219             #pod
220             #pod You may want to use L to simplify writing your
221             #pod check slightly.
222             #pod
223             #pod =head1 METHODS
224             #pod
225             #pod =head2 new
226             #pod
227             #pod my $checker = HealthCheck->new( id => 'my_checker' );
228             #pod
229             #pod =head3 ATTRIBUTES
230             #pod
231             #pod =over
232             #pod
233             #pod =item checks
234             #pod
235             #pod An arrayref that is passed to L to initialize checks.
236             #pod
237             #pod =item tags
238             #pod
239             #pod An arrayref used as the default set of tags for any checks that don't
240             #pod override them.
241             #pod
242             #pod =back
243             #pod
244             #pod Any other parameters are included in the "Result" hashref returned.
245             #pod
246             #pod Some recommended things to include are:
247             #pod
248             #pod =over
249             #pod
250             #pod =item id
251             #pod
252             #pod The unique id for this check.
253             #pod
254             #pod =item label
255             #pod
256             #pod A human readable name for this check.
257             #pod
258             #pod =item runbook
259             #pod
260             #pod A runbook link to help troubleshooting if the status is not OK.
261             #pod
262             #pod =back
263             #pod
264             #pod =cut
265              
266             sub new {
267 36     36 1 28774 my ( $class, %params ) = @_;
268 36         66 my $checks = delete $params{checks};
269 36         81 my $self = bless {%params}, $class;
270 36 100       183 return $checks ? $self->register($checks) : $self;
271             }
272              
273             #pod =head2 register
274             #pod
275             #pod $checker->register({
276             #pod invocant => $class_or_object,
277             #pod check => $method_on_invocant_or_coderef,
278             #pod more => "any other params are passed to the check",
279             #pod });
280             #pod
281             #pod Takes a list or arrayref of check definitions to be added to the object.
282             #pod
283             #pod Each registered check must return a valid GSG Health Check response,
284             #pod either as a hashref or an even-sized list.
285             #pod See the GSG Health Check Standard (linked in L)
286             #pod for the fields that checks should return.
287             #pod
288             #pod Rather than having to always pass in the full hashref definition,
289             #pod several common cases are detected and used to fill out the check.
290             #pod
291             #pod =over
292             #pod
293             #pod =item coderef
294             #pod
295             #pod If passed a coderef, this will be called as the C without an C.
296             #pod
297             #pod =item object
298             #pod
299             #pod If a blessed object is passed in
300             #pod and it has a C method, use that for the C,
301             #pod otherwise throw an exception.
302             #pod
303             #pod =item string
304             #pod
305             #pod If a string is passed in,
306             #pod check if it is the name of a loaded class that has a C method,
307             #pod and if so use it as the C with the method as the C.
308             #pod Otherwise if our L has a method with this name,
309             #pod the L becomes the C and this becomes the C,
310             #pod otherwise throws an exception.
311             #pod
312             #pod =item full hashref of params
313             #pod
314             #pod The full hashref can consist of a C key that the above heuristics
315             #pod are applied,
316             #pod or include an C key that is used as either
317             #pod an C or C.
318             #pod With the C specified, the now optional C key
319             #pod defaults to "check" and is used as the method to call on C.
320             #pod
321             #pod All attributes other than C and C are passed to the check.
322             #pod
323             #pod =back
324             #pod
325             #pod =cut
326              
327             sub register {
328 29     29 1 293613 my ($self, @checks) = @_;
329 29 100       337 croak("register cannot be called as a class method") unless ref $self;
330 28 50       54 return $self unless @checks;
331 28         51 my $class = ref $self;
332              
333 28 100 100     160 @checks = @{ $checks[0] }
  12   66     26  
334             if @checks == 1 and ( ref $checks[0] || '' ) eq 'ARRAY';
335              
336             # If the check that was passed in is just the name of a method
337             # we are going to use our caller as the invocant.
338 28         75 my $caller;
339             my $find_caller = sub {
340 7     7   11 my ( $i, $c ) = ( 1, undef );
341 7         11 do { ($c) = caller( $i++ ) } while $c->isa(__PACKAGE__);
  10         74  
342 7         22 $c;
343 28         114 };
344              
345 28         53 foreach (@checks) {
346 45   100     100 my $type = ref $_ || '';
347             my %c
348 45 50       139 = $type eq 'HASH' ? ( %{$_} )
  21 100       60  
349             : $type eq 'ARRAY' ? ( check => $class->register($_) )
350             : ( check => $_ );
351              
352 45 100       470 croak("check parameter required") unless $c{check};
353              
354             # If it's not a coderef,
355             # it must be the name of a method to call on an invocant.
356 42 100 100     122 unless ( ( ref $c{check} || '' ) eq 'CODE' ) {
357              
358             # If they passed in an object or a class that can('check')
359             # then we want to set that as the invocant so the check
360             # runner does the right thing.
361 13 100 66     53 if ( $c{check} and not $c{invocant} and do {
      100        
362 11         18 local $@;
363 11         37 eval { $c{check}->can('check') };
  11         92  
364             } )
365             {
366 4         26 $c{invocant} = $c{check};
367 4         7 $c{check} = 'check';
368             }
369              
370             # If they just passed in a method name,
371             # we can see if the caller has that method.
372 13 100       31 unless ($c{invocant}) {
373 7   33     25 $caller ||= $find_caller->();
374              
375 7 100       33 if ($caller->can($c{check}) ) {
376 4         7 $c{invocant} = $caller;
377             }
378             else {
379 3         449 croak("Can't determine what to do with '$c{check}'");
380             }
381             }
382              
383             croak("'$c{invocant}' cannot '$c{check}'")
384 10 100       1511 unless $c{invocant}->can( $c{check} );
385             }
386              
387 37         52 push @{ $registered_checks{$self} }, \%c;
  37         203  
388             }
389              
390 20         149 return $self;
391             }
392              
393             #pod =head2 check
394             #pod
395             #pod my %results = %{ $checker->check(%params) }
396             #pod
397             #pod Calls all of the registered checks and returns a hashref of the results of
398             #pod processing the checks passed through L.
399             #pod Passes the L as an even-sized list to the check,
400             #pod without the C or C keys.
401             #pod This hashref is shallow merged with and duplicate keys overridden by
402             #pod the C<%params> passed in.
403             #pod
404             #pod If there is both an C and C in the params,
405             #pod it the C is called as a method on the C,
406             #pod otherwise C is used as a callback coderef.
407             #pod
408             #pod If only a single check is registered,
409             #pod the results from that check are merged with, and will override
410             #pod the L set on the object instead of being put in
411             #pod a C arrayref.
412             #pod
413             #pod Throws an exception if no checks have been registered.
414             #pod
415             #pod =head3 run
416             #pod
417             #pod Main implementation of the checker is here.
418             #pod
419             #pod Passes C<< summarize_result => 0 >> to each registered check
420             #pod unless overridden to avoid running C multiple times.
421             #pod See L.
422             #pod
423             #pod =cut
424              
425             sub check {
426 29     29 1 3026 my ( $self, @params ) = @_;
427 29 100       2765 croak("check cannot be called as a class method") unless ref $self;
428 28 100       37 croak("No registered checks") unless @{ $registered_checks{$self} || [] };
  28 100       226  
429 27         94 $self->SUPER::check(@params);
430             }
431              
432             #pod =head2 get_registered_tags
433             #pod
434             #pod Read-only accessor that returns the list of 'top-level' tags registered with
435             #pod this object. Sub-check tags are not included - only those which will result in
436             #pod checks being run when passed to L on the given object.
437             #pod
438             #pod =cut
439              
440             sub get_registered_tags {
441 3     3 1 22 my ($self) = @_;
442              
443 3 50       6 my @checks = @{ $registered_checks{$self} || [] };
  3         17  
444 3         5 my @tags;
445 3         8 for my $check (@checks) {
446 15         31 $self->_set_check_response_defaults($check);
447 15 100       19 push @tags, @{ $check->{_respond}{tags} || [] };
  15         43  
448             }
449 3         7 push @tags, $self->tags;
450              
451 3         54 return uniq sort @tags;
452             }
453              
454             sub run {
455 27     27 1 52 my ($self, %params) = @_;
456              
457             # If we are going to summarize things, no need for our children to
458 27 100       76 $params{summarize_result} = 0 unless exists $params{summarize_result};
459              
460             my @results = $self->_run_checks(
461             [
462 69         150 grep { $self->should_run( $_, %params ) }
463 27 50       40 @{ $registered_checks{$self} || [] }
  27         79  
464             ],
465             \%params,
466             );
467              
468 27 100       82 return unless @results; # don't return undef, instead an empty list
469 25 50       37 return $results[0] if @{ $registered_checks{$self} || [] } == 1;
  25 100       99  
470 11         40 return { results => \@results };
471             }
472              
473             sub _run_checks {
474 27     27   48 my ( $self, $checks, $params ) = @_;
475              
476 27         49 return map { $self->_run_check( $_, $params ) } @$checks;
  43         98  
477             }
478              
479             sub _run_check {
480 43     43   74 my ( $self, $check, $params ) = @_;
481              
482 43         83 my %c = %{ $check };
  43         134  
483 43         106 $self->_set_check_response_defaults(\%c);
484 43         71 my $defaults = delete $c{_respond};
485 43   100     109 my $i = delete $c{invocant} || '';
486 43   50     112 my $m = delete $c{check} || '';
487              
488 43         80 my @r;
489             # Exceptions will probably not contain child health check's metadata,
490             # as HealthCheck::Diagnostic->summarize would normally populate these
491             # and was not called.
492             # This could theoretically be a pain for prodsupport. If we find this
493             # happening frequently, we should reassess our decision not to attempt
494             # to call summarize here
495             # (for fear of exception-catching magic and rabbitholes).
496             {
497 43         70 local $@;
  43         55  
498 43 100       66 @r = eval { $i ? $i->$m( %c, %$params ) : $m->( %c, %$params ) };
  43         155  
499 43 100 66     299 @r = { status => 'CRITICAL', info => $@ } if $@ and not @r;
500             }
501              
502             @r
503             = @r == 1 && ref $r[0] eq 'HASH' ? $r[0]
504             : @r % 2 == 0 ? {@r}
505 43 100 100     172 : do {
    100          
506 2 100       8 my $c = $i ? "$i->$m" : "$m";
507 2         302 carp("Invalid return from $c (@r)");
508 2         15 ();
509             };
510              
511 43 100       107 if (@r) { @r = +{ %$defaults, %{ $r[0] } } }
  41         68  
  41         123  
512              
513 43         167 return @r;
514             }
515              
516             sub _set_check_response_defaults {
517 136     136   218 my ($self, $c) = @_;
518 136 100       264 return if exists $c->{_respond};
519              
520 47         59 my %defaults;
521 47         76 FIELD: for my $field ( qw(id label runbook tags) ) {
522 188 100       331 if (exists $c->{$field}) {
523 29         51 $defaults{$field} = $c->{$field};
524 29         49 next FIELD;
525             }
526              
527 159 100 100     426 if ( $c->{invocant} && $c->{invocant}->can($field) ) {
528 20         42 my $val;
529 20 100       33 if ( $field eq 'tags' ) {
530 4 100       10 if (my @tags = $c->{invocant}->$field) {
531 2         5 $val = [@tags];
532             }
533             }
534             else {
535 16         32 $val = $c->{invocant}->$field;
536             }
537              
538 20 100       37 if (defined $val) {
539 6         11 $defaults{$field} = $val;
540 6         11 next FIELD;
541             }
542             }
543              
544             # we only copy tags from the checker to the sub-checks,
545             # and only if they don't exist.
546 153 100       348 $self->_set_default_fields(\%defaults, $field)
547             if $field eq 'tags';
548             }
549              
550             # deref the tags, just in case someone decides to adjust them later.
551 47 100       96 $defaults{tags} = [ @{ $defaults{tags} } ] if $defaults{tags};
  23         48  
552              
553 47         93 $c->{_respond} = \%defaults;
554             }
555              
556              
557             #pod =head1 INTERNALS
558             #pod
559             #pod These methods may be useful for subclassing,
560             #pod but are not intended for general use.
561             #pod
562             #pod =head2 should_run
563             #pod
564             #pod my $bool = $checker->should_run( \%check, tags => ['apple', '!banana'] );
565             #pod
566             #pod Takes a check definition hash and paramters and returns true
567             #pod if the check should be run.
568             #pod Used by L to determine which checks to run.
569             #pod
570             #pod Supported parameters:
571             #pod
572             #pod =over
573             #pod
574             #pod =item tags
575             #pod
576             #pod Tags can be either "positive" or "negative". A negative tag is indicated by a
577             #pod leading C.
578             #pod A check is run if its tags match any of the passed in positive tags and none
579             #pod of the negative ones.
580             #pod If no tags are passed in, all checks will be run.
581             #pod
582             #pod If the C C and there are no tags in the
583             #pod L then the return value of that method is used.
584             #pod
585             #pod If a check has no tags defined, will use the default tags defined
586             #pod when the object was created.
587             #pod
588             #pod =back
589             #pod
590             #pod =cut
591              
592             sub _has_tags {
593 78     78   133 my ($self, $check, @want_tags) = @_;
594              
595 78         158 $self->_set_check_response_defaults($check);
596              
597             # Look at what the check responds to, not what was initially specified
598             # (in case tags are inherited)
599 78 50       94 my %have_tags = map { $_ => 1 } @{ $check->{_respond}{tags} || [] };
  125         278  
  78         165  
600              
601 78     83   243 return any { $have_tags{$_} } @want_tags;
  83         378  
602             }
603              
604             sub should_run {
605 117     117 1 293 my ( $self, $check, %params ) = @_;
606              
607 117         166 my (@positive_tags, @negative_tags);
608 117         151 for my $tag ( @{ $params{tags} } ) {
  117         195  
609 86 100       153 if ( $tag =~ /^!/ ) {
610 12         26 push @negative_tags, substr($tag, 1);
611             }
612             else {
613 74         120 push @positive_tags, $tag;
614             }
615             }
616              
617 117 100 100     263 return 0 if @negative_tags && $self->_has_tags($check, @negative_tags);
618 114 100       305 return 1 unless @positive_tags;
619 66         112 return $self->_has_tags($check, @positive_tags);
620             }
621              
622             1;
623              
624             __END__