File Coverage

blib/lib/HealthCheck/Diagnostic/WebRequest.pm
Criterion Covered Total %
statement 104 109 95.4
branch 41 58 70.6
condition 28 42 66.6
subroutine 16 16 100.0
pod 7 7 100.0
total 196 232 84.4


line stmt bran cond sub pod time code
1             package HealthCheck::Diagnostic::WebRequest;
2 4     4   1940816 use parent 'HealthCheck::Diagnostic';
  4         8  
  4         33  
3              
4             # ABSTRACT: Make HTTP/HTTPS requests to web servers to check connectivity
5 4     4   14405 use version;
  4         8  
  4         24  
6             our $VERSION = 'v1.4.4'; # VERSION
7              
8 4     4   302 use strict;
  4         9  
  4         65  
9 4     4   28 use warnings;
  4         9  
  4         158  
10              
11 4     4   17 use Carp;
  4         8  
  4         1224  
12 4     4   2719 use LWP::UserAgent;
  4         210962  
  4         180  
13 4     4   41 use HTTP::Request;
  4         8  
  4         275  
14 4     4   30 use Scalar::Util 'blessed';
  4         8  
  4         249  
15 4     4   22 use Time::HiRes 'gettimeofday';
  4         14  
  4         39  
16              
17             sub new {
18 21     21 1 655537 my ($class, @params) = @_;
19              
20             my %params = @params == 1 && ( ref $params[0] || '' ) eq 'HASH'
21 21 50 33     223 ? %{ $params[0] } : @params;
  0         0  
22              
23             my @bad_params = grep {
24 21         94 !/^( content_regex
  38         232  
25             | id
26             | label
27             | no_follow_redirects
28             | options
29             | request
30             | response_time_threshold
31             | status_code
32             | status_code_eval
33             | tags
34             | timeout
35             | ua
36             | url
37             )$/x
38             } keys %params;
39              
40 21 50       57 carp("Invalid parameter: " . join(", ", @bad_params)) if @bad_params;
41              
42             croak "The 'request' and 'url' parameters are mutually exclusive!"
43 21 50 33     123 if $params{url} && $params{request};
44 21 50       71 if ($params{url}) {
    0          
45             # Validation for url can be added here
46 21         129 $params{request} = HTTP::Request->new('GET', $params{url});
47             }
48             elsif ($params{request}) {
49 0 0 0     0 croak "request must be an HTTP::Request" unless blessed $params{request} && $params{request}->isa('HTTP::Request');
50             }
51             else{
52 0         0 croak "Either url or request is required";
53             }
54              
55 21 50       26241 if ($params{ua}) {
56 0 0 0     0 croak "The 'ua' parameter must be of type LWP::UserAgent if provided" unless blessed $params{ua} && $params{ua}->isa('LWP::UserAgent');
57 0 0       0 carp "no_follow_redirects does not do anything when 'ua' is provided" if $params{no_follow_redirects};
58             }
59              
60             # Process and serialize the status code checker
61 21   100     139 $params{status_code} ||= '200';
62 21         44 my (@and, @or);
63 21         267 foreach my $part (split qr{\s*,\s*}, $params{status_code}) {
64             # Strict validation of each part, since we're throwing these into an eval
65 23         183 my ($op, $code) = $part =~ m{\A\s*(>=|>|<=|<|!=|!)?\s*(\d{3})\z};
66              
67 23 50       69 croak "The 'status_code' condition '$part' is not in the correct format!"
68             unless defined $code;
69 23 100 100     132 $op = '!=' if defined $op && $op eq '!';
70              
71 23 100       54 unless ($op) { push @or, '$_ == '.$code; }
  16         61  
72 7         31 else { push @and, '$_ '."$op $code"; }
73             }
74 21 100       101 push @or, '('.join(' && ', @and).')' if @and; # merge @and as one big condition into @or
75 21         80 $params{status_code_eval} = join ' || ', @or;
76              
77 21   100     128 $params{options} //= {};
78 21   50     186 $params{options}{agent} //= LWP::UserAgent->_agent .
      66        
79             " HealthCheck-Diagnostic-WebRequest/" . ( $class->VERSION || '0' );
80 21   100     497 $params{options}{timeout} //= 7; # Decided by committee
81 21 50       56 unless ($params{ua}) {
82 21   33     74 $params{ua} //= LWP::UserAgent->new( %{$params{options}} );
  21         114  
83 21 50       12217 $params{ua}->requests_redirectable([]) if $params{'no_follow_redirects'};
84             }
85              
86 21         159 return $class->SUPER::new(
87             label => 'web_request',
88             %params,
89             );
90             }
91              
92             sub check {
93 27     27 1 13143 my ($self, @args) = @_;
94              
95 27 100       352 croak("check cannot be called as a class method")
96             unless ref $self;
97 26         99 return $self->SUPER::check(@args);
98             }
99              
100             sub run {
101 26     26 1 575 my ( $self, %params ) = @_;
102              
103 26         44 my ($response, $elapsed_time);
104             {
105 26         48 my $t1 = gettimeofday;
  26         100  
106 26         504 $response = $self->send_request;
107 26         45631 $elapsed_time = gettimeofday - $t1;
108             }
109              
110 26         534 my @results;
111 26         81 push @results, $self->check_status( $response );
112 26         90 push @results, $self->check_response_time( $elapsed_time );
113             push @results, $self->check_content( $response )
114 26 100       132 if $results[0]->{status} eq 'OK';
115              
116 26         98 my $info = join '; ', grep { length } map { $_->{info} } @results;
  56         158  
  56         139  
117              
118 26         377 return { info => $info, results => \@results };
119             }
120              
121             sub check_status {
122 26     26 1 72 my ( $self, $response ) = @_;
123 26         40 my $status;
124              
125 26   100     74 my $client_warning = $response->header('Client-Warning') // '';
126 26   100     1664 my $proxy_error = $response->header('X-Squid-Error') // '';
127              
128             # Eval the status checker
129 26         1418 my $success;
130             {
131 26         41 local $_ = $response->code;
  26         87  
132 26         2691 $success = eval $self->{status_code_eval};
133             }
134              
135             # An unfortunate post-constructor die, but this would be a validation bug (ie: our fault)
136 26 50       136 die "Status code checker eval '".$self->{status_code_eval}."' failed: $@" if $@;
137              
138 26 100       100 $status = $success ? 'OK' : 'CRITICAL';
139              
140             # Proxy error is an automatic failure
141 26 100       56 $status = 'CRITICAL' if $proxy_error;
142              
143             my $info = sprintf( "Requested %s and got%s status code %s",
144             $self->{request}->uri,
145 26 100       121 $status eq 'OK' ? ' expected' : '',
146             $response->code,
147             );
148 26 100       853 $info .= " from proxy with error '$proxy_error'" if $proxy_error;
149 26 100 100     134 $info .= ", expected ".$self->{status_code} unless $status eq 'OK' || $proxy_error;
150              
151             # If LWP returned 'Internal response', the status code doesn't actually mean anything
152 26 100 100     105 if ($client_warning && $client_warning eq 'Internal response') {
153 2         7 $status = 'CRITICAL';
154 2         8 $info = "User Agent returned: ".$response->message;
155             }
156              
157 26         181 return { status => $status, info => $info };
158             }
159              
160             sub check_content {
161 16     16 1 33 my ( $self, $response ) = @_;
162              
163 16 100       48 return unless $self->{content_regex};
164              
165 4         10 my $regex = $self->{content_regex};
166 4         23 my $content = $response->content;
167 4 100       100 my $status = $content =~ /$regex/ ? 'OK' : 'CRITICAL';
168 4 100       12 my $successful = $status eq 'OK' ? 'matches' : 'does not match';
169              
170             return {
171 4         22 status => $status,
172             info => "Response content $successful /$regex/",
173             };
174             }
175              
176             sub check_response_time {
177 26     26 1 61 my ( $self, $elapsed_time ) = @_;
178              
179 26         56 my $response_time_threshold = $self->{response_time_threshold};
180 26         45 my $status = 'OK';
181 26 100 100     78 $status = 'WARNING' if defined $response_time_threshold && $elapsed_time > $response_time_threshold;
182              
183             return {
184 26 100       144 status => $status,
185             info => "Request took $elapsed_time second" . ( $elapsed_time == 1 ? '' : 's' ),
186             };
187             }
188              
189             sub send_request {
190 26     26 1 59 my ( $self ) = @_;
191              
192 26         123 return $self->{ua}->request( $self->{request} );
193             }
194              
195             1;
196              
197             __END__