File Coverage

blib/lib/HealthCheck/Diagnostic/WebRequest.pm
Criterion Covered Total %
statement 85 86 98.8
branch 34 42 80.9
condition 26 42 61.9
subroutine 13 13 100.0
pod 3 5 60.0
total 161 188 85.6


line stmt bran cond sub pod time code
1             package HealthCheck::Diagnostic::WebRequest;
2 1     1   112512 use parent 'HealthCheck::Diagnostic';
  1         309  
  1         7  
3              
4             # ABSTRACT: Make HTTP/HTTPS requests to web servers to check connectivity
5 1     1   7320 use version;
  1         2  
  1         4  
6             our $VERSION = 'v1.4.0'; # VERSION
7              
8 1     1   74 use strict;
  1         3  
  1         18  
9 1     1   4 use warnings;
  1         2  
  1         23  
10              
11 1     1   5 use Carp;
  1         3  
  1         57  
12 1     1   816 use LWP::UserAgent;
  1         48391  
  1         34  
13 1     1   13 use HTTP::Request;
  1         2  
  1         27  
14 1     1   5 use Scalar::Util 'blessed';
  1         2  
  1         1126  
15              
16             sub new {
17 15     15 1 56624 my ($class, @params) = @_;
18              
19             my %params = @params == 1 && ( ref $params[0] || '' ) eq 'HASH'
20 15 50 33     91 ? %{ $params[0] } : @params;
  0         0  
21              
22             my @bad_params = grep {
23 15         66 !/^( content_regex
  27         144  
24             | id
25             | label
26             | no_follow_redirects
27             | options
28             | request
29             | status_code
30             | status_code_eval
31             | tags
32             | timeout
33             | url
34             )$/x
35             } keys %params;
36              
37 15 50       45 carp("Invalid parameter: " . join(", ", @bad_params)) if @bad_params;
38              
39             die "No url or HTTP::Request specified!" unless ($params{url} ||
40             ($params{request} && blessed $params{request} &&
41 15 0 0     41 $params{request}->isa('HTTP::Request')));
      0        
      33        
42             die "The 'request' and 'url' options are mutually exclusive!"
43 15 50 33     75 if $params{url} && $params{request};
44              
45             # Process and serialize the status code checker
46 15   100     59 $params{status_code} ||= '200';
47 15         30 my (@and, @or);
48 15         109 foreach my $part (split qr{\s*,\s*}, $params{status_code}) {
49             # Strict validation of each part, since we're throwing these into an eval
50 17         111 my ($op, $code) = $part =~ m{\A\s*(>=|>|<=|<|!=|!)?\s*(\d{3})\z};
51              
52 17 50       54 die "The 'status_code' condition '$part' is not in the correct format!"
53             unless defined $code;
54 17 100 100     60 $op = '!=' if defined $op && $op eq '!';
55              
56 17 100       38 unless ($op) { push @or, '$_ == '.$code; }
  10         32  
57 7         23 else { push @and, '$_ '."$op $code"; }
58             }
59 15 100       64 push @or, '('.join(' && ', @and).')' if @and; # merge @and as one big condition into @or
60 15         49 $params{status_code_eval} = join ' || ', @or;
61              
62 15   33     97 $params{request} //= HTTP::Request->new('GET', $params{url});
63 15   100     10029 $params{options} //= {};
64 15   50     90 $params{options}{agent} //= LWP::UserAgent->_agent .
      66        
65             " HealthCheck-Diagnostic-WebRequest/" . ( $class->VERSION || '0' );
66 15   100     271 $params{options}{timeout} //= 7; # Decided by committee
67              
68 15         83 return $class->SUPER::new(
69             label => 'web_request',
70             %params,
71             );
72             }
73              
74             sub check {
75 21     21 1 8041 my ($self, @args) = @_;
76              
77 21 100       288 croak("check cannot be called as a class method")
78             unless ref $self;
79 20         70 return $self->SUPER::check(@args);
80             }
81              
82             sub run {
83 20     20 1 437 my ( $self, %params ) = @_;
84 20         36 my $ua = LWP::UserAgent->new( %{$self->{options}} );
  20         97  
85              
86 20 50       7121 $ua->requests_redirectable([]) if $self->{'no_follow_redirects'};
87              
88 20         57 my $response = $ua->request( $self->{request} );
89              
90 20         27697 my @results = $self->check_status( $response );
91             push @results, $self->check_content( $response )
92 20 100       71 if $results[0]->{status} eq 'OK';
93              
94 20         45 my $info = join '; ', map { $_->{info} } @results;
  24         83  
95              
96 20         401 return { info => $info, results => \@results };
97             }
98              
99             sub check_status {
100 20     20 0 45 my ( $self, $response ) = @_;
101 20         33 my $status;
102              
103 20   100     47 my $client_warning = $response->header('Client-Warning') // '';
104 20   100     1026 my $proxy_error = $response->header('X-Squid-Error') // '';
105              
106             # Eval the status checker
107 20         972 my $success;
108             {
109 20         33 local $_ = $response->code;
  20         49  
110 20         1667 $success = eval $self->{status_code_eval};
111             }
112              
113             # An unfortunate post-constructor die, but this would be a validation bug (ie: our fault)
114 20 50       87 die "Status code checker eval '".$self->{status_code_eval}."' failed: $@" if $@;
115              
116 20 100       56 $status = $success ? 'OK' : 'CRITICAL';
117              
118             # Proxy error is an automatic failure
119 20 100       40 $status = 'CRITICAL' if $proxy_error;
120              
121             my $info = sprintf( "Requested %s and got%s status code %s",
122             $self->{request}->uri,
123 20 100       79 $status eq 'OK' ? ' expected' : '',
124             $response->code,
125             );
126 20 100       579 $info .= " from proxy with error '$proxy_error'" if $proxy_error;
127 20 100 100     91 $info .= ", expected ".$self->{status_code} unless $status eq 'OK' || $proxy_error;
128              
129             # If LWP returned 'Internal response', the status code doesn't actually mean anything
130 20 100 100     56 if ($client_warning && $client_warning eq 'Internal response') {
131 2         5 $status = 'CRITICAL';
132 2         8 $info = "User Agent returned: ".$response->message;
133             }
134              
135 20         113 return { status => $status, info => $info };
136             }
137              
138             sub check_content {
139 10     10 0 23 my ( $self, $response ) = @_;
140              
141 10 100       29 return unless $self->{content_regex};
142              
143 4         9 my $regex = $self->{content_regex};
144 4         14 my $content = $response->content;
145 4 100       74 my $status = $content =~ /$regex/ ? 'OK' : 'CRITICAL';
146 4 100       12 my $successful = $status eq 'OK' ? 'matches' : 'does not match';
147              
148             return {
149 4         22 status => $status,
150             info => "Response content $successful /$regex/",
151             };
152             }
153              
154             1;
155              
156             __END__