File Coverage

blib/lib/HealthCheck.pm
Criterion Covered Total %
statement 131 131 100.0
branch 71 76 93.4
condition 28 34 82.3
subroutine 17 17 100.0
pod 5 5 100.0
total 252 263 95.8


line stmt bran cond sub pod time code
1             package HealthCheck;
2 1     1   223322 use parent 'HealthCheck::Diagnostic';
  1         307  
  1         5  
3              
4             # ABSTRACT: A health check for your code
5 1     1   45 use version;
  1         2  
  1         5  
6             our $VERSION = 'v1.8.0'; # VERSION
7              
8 1     1   80 use 5.010;
  1         3  
9 1     1   5 use strict;
  1         1  
  1         18  
10 1     1   4 use warnings;
  1         2  
  1         31  
11              
12 1     1   9 use Carp;
  1         1  
  1         56  
13              
14 1     1   1685 use Hash::Util::FieldHash;
  1         976  
  1         51  
15 1     1   7 use List::Util qw(any);
  1         2  
  1         1510  
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 35     35 1 28387 my ( $class, %params ) = @_;
268 35         91 my $checks = delete $params{checks};
269 35         92 my $self = bless {%params}, $class;
270 35 100       175 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 6344 my ($self, @checks) = @_;
329 29 100       266 croak("register cannot be called as a class method") unless ref $self;
330 28 50       70 return $self unless @checks;
331 28         57 my $class = ref $self;
332              
333 28 100 100     164 @checks = @{ $checks[0] }
  12   66     32  
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         45 my $caller;
339             my $find_caller = sub {
340 7     7   18 my ( $i, $c ) = ( 1, undef );
341 7         7 do { ($c) = caller( $i++ ) } while $c->isa(__PACKAGE__);
  10         96  
342 7         24 $c;
343 28         112 };
344              
345 28         57 foreach (@checks) {
346 44   100     132 my $type = ref $_ || '';
347             my %c
348 44 50       123 = $type eq 'HASH' ? ( %{$_} )
  20 100       64  
349             : $type eq 'ARRAY' ? ( check => $class->register($_) )
350             : ( check => $_ );
351              
352 44 100       413 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 41 100 100     120 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     57 if ( $c{check} and not $c{invocant} and do {
      100        
362 11         19 local $@;
363 11         15 eval { $c{check}->can('check') };
  11         88  
364             } )
365             {
366 4         8 $c{invocant} = $c{check};
367 4         9 $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       29 unless ($c{invocant}) {
373 7   33     45 $caller ||= $find_caller->();
374              
375 7 100       31 if ($caller->can($c{check}) ) {
376 4         9 $c{invocant} = $caller;
377             }
378             else {
379 3         283 croak("Can't determine what to do with '$c{check}'");
380             }
381             }
382              
383             croak("'$c{invocant}' cannot '$c{check}'")
384 10 100       213 unless $c{invocant}->can( $c{check} );
385             }
386              
387 36         43 push @{ $registered_checks{$self} }, \%c;
  36         208  
388             }
389              
390 20         138 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 27     27 1 1795 my ( $self, @params ) = @_;
427 27 100       193 croak("check cannot be called as a class method") unless ref $self;
428 26 100       39 croak("No registered checks") unless @{ $registered_checks{$self} || [] };
  26 100       184  
429 25         98 $self->SUPER::check(@params);
430             }
431              
432             sub run {
433 25     25 1 54 my ($self, %params) = @_;
434              
435             # If we are going to summarize things, no need for our children to
436 25 100       78 $params{summarize_result} = 0 unless exists $params{summarize_result};
437              
438             my @results = map {
439 41         55 my %c = %{$_};
  41         115  
440 41         113 $self->_set_check_response_defaults(\%c);
441 41         77 my $defaults = delete $c{_respond};
442 41   100     114 my $i = delete $c{invocant} || '';
443 41   50     86 my $m = delete $c{check} || '';
444              
445 41         51 my @r;
446             # Exceptions will probably not contain child health check's metadata,
447             # as HealthCheck::Diagnostic->summarize would normally populate these
448             # and was not called.
449             # This could theoretically be a pain for prodsupport. If we find this
450             # happening frequently, we should reassess our decision not to attempt
451             # to call summarize here
452             # (for fear of exception-catching magic and rabbitholes).
453             {
454 41         56 local $@;
  41         57  
455 41 100       58 @r = eval { $i ? $i->$m( %c, %params ) : $m->( %c, %params ) };
  41         152  
456 41 100 66     254 @r = { status => 'CRITICAL', info => $@ } if $@ and not @r;
457             }
458              
459             @r
460             = @r == 1 && ref $r[0] eq 'HASH' ? $r[0]
461             : @r % 2 == 0 ? {@r}
462 41 100 100     178 : do {
    100          
463 2 100       8 my $c = $i ? "$i->$m" : "$m";
464 2         200 carp("Invalid return from $c (@r)");
465 2         59 ();
466             };
467              
468 41 100       90 if (@r) { @r = +{ %$defaults, %{ $r[0] } } }
  39         78  
  39         119  
469              
470 41         141 @r;
471             } grep {
472 54         129 $self->should_run( $_, %params );
473 25 50       36 } @{ $registered_checks{$self} || [] };
  25         89  
474              
475 25 100       69 return unless @results; # don't return undef, instead an empty list
476 24 50       35 return $results[0] if @{ $registered_checks{$self} || [] } == 1;
  24 100       99  
477 10         42 return { results => \@results };
478             }
479              
480             sub _set_check_response_defaults {
481 105     105   156 my ($self, $c) = @_;
482 105 100       240 return if exists $c->{_respond};
483              
484 48         61 my %defaults;
485 48         77 FIELD: for my $field ( qw(id label runbook tags) ) {
486 192 100       329 if (exists $c->{$field}) {
487 31         52 $defaults{$field} = $c->{$field};
488 31         118 next FIELD;
489             }
490              
491 161 100 100     449 if ( $c->{invocant} && $c->{invocant}->can($field) ) {
492 22         30 my $val;
493 22 100       40 if ( $field eq 'tags' ) {
494 5 100       14 if (my @tags = $c->{invocant}->$field) {
495 3         8 $val = [@tags];
496             }
497             }
498             else {
499 17         40 $val = $c->{invocant}->$field;
500             }
501              
502 22 100       45 if (defined $val) {
503 9         14 $defaults{$field} = $val;
504 9         17 next FIELD;
505             }
506             }
507              
508             # we only copy tags from the checker to the sub-checks,
509             # and only if they don't exist.
510 152 100       347 $self->_set_default_fields(\%defaults, $field)
511             if $field eq 'tags';
512             }
513              
514             # deref the tags, just in case someone decides to adjust them later.
515 48 100       107 $defaults{tags} = [ @{ $defaults{tags} } ] if $defaults{tags};
  26         66  
516              
517 48         115 $c->{_respond} = \%defaults;
518             }
519              
520              
521             #pod =head1 INTERNALS
522             #pod
523             #pod These methods may be useful for subclassing,
524             #pod but are not intended for general use.
525             #pod
526             #pod =head2 should_run
527             #pod
528             #pod my $bool = $checker->should_run( \%check, tags => ['apple', '!banana'] );
529             #pod
530             #pod Takes a check definition hash and paramters and returns true
531             #pod if the check should be run.
532             #pod Used by L to determine which checks to run.
533             #pod
534             #pod Supported parameters:
535             #pod
536             #pod =over
537             #pod
538             #pod =item tags
539             #pod
540             #pod Tags can be either "positive" or "negative". A negative tag is indicated by a
541             #pod leading C.
542             #pod A check is run if its tags match any of the passed in positive tags and none
543             #pod of the negative ones.
544             #pod If no tags are passed in, all checks will be run.
545             #pod
546             #pod If the C C and there are no tags in the
547             #pod L then the return value of that method is used.
548             #pod
549             #pod If a check has no tags defined, will use the default tags defined
550             #pod when the object was created.
551             #pod
552             #pod =back
553             #pod
554             #pod =cut
555              
556             sub _has_tags {
557 64     64   114 my ($self, $check, @want_tags) = @_;
558              
559 64         139 $self->_set_check_response_defaults($check);
560              
561             # Look at what the check responds to, not what was initially specified
562             # (in case tags are inherited)
563 64 50       84 my %have_tags = map { $_ => 1 } @{ $check->{_respond}{tags} || [] };
  99         237  
  64         137  
564              
565 64     69   219 return any { $have_tags{$_} } @want_tags;
  69         327  
566             }
567              
568             sub should_run {
569 102     102 1 328 my ( $self, $check, %params ) = @_;
570              
571 102         147 my (@positive_tags, @negative_tags);
572 102         123 for my $tag ( @{ $params{tags} } ) {
  102         185  
573 72 100       149 if ( $tag =~ /^!/ ) {
574 12         29 push @negative_tags, substr($tag, 1);
575             }
576             else {
577 60         105 push @positive_tags, $tag;
578             }
579             }
580              
581 102 100 100     224 return 0 if @negative_tags && $self->_has_tags($check, @negative_tags);
582 99 100       271 return 1 unless @positive_tags;
583 52         96 return $self->_has_tags($check, @positive_tags);
584             }
585              
586             1;
587              
588             __END__