File Coverage

blib/lib/HealthCheck/Diagnostic/WebRequest.pm
Criterion Covered Total %
statement 86 87 98.8
branch 34 42 80.9
condition 26 42 61.9
subroutine 13 13 100.0
pod 3 5 60.0
total 162 189 85.7


line stmt bran cond sub pod time code
1             package HealthCheck::Diagnostic::WebRequest;
2 1     1   113827 use parent 'HealthCheck::Diagnostic';
  1         322  
  1         6  
3              
4             # ABSTRACT: Make HTTP/HTTPS requests to web servers to check connectivity
5 1     1   6908 use version;
  1         2  
  1         5  
6             our $VERSION = 'v1.4.2'; # VERSION
7              
8 1     1   74 use strict;
  1         2  
  1         18  
9 1     1   5 use warnings;
  1         2  
  1         24  
10              
11 1     1   5 use Carp;
  1         2  
  1         48  
12 1     1   748 use LWP::UserAgent;
  1         46590  
  1         38  
13 1     1   8 use HTTP::Request;
  1         3  
  1         25  
14 1     1   6 use Scalar::Util 'blessed';
  1         3  
  1         1127  
15              
16             sub new {
17 15     15 1 56619 my ($class, @params) = @_;
18              
19             my %params = @params == 1 && ( ref $params[0] || '' ) eq 'HASH'
20 15 50 33     89 ? %{ $params[0] } : @params;
  0         0  
21              
22             my @bad_params = grep {
23 15         47 !/^( content_regex
  27         157  
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       44 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     43 $params{request}->isa('HTTP::Request')));
      0        
      33        
42             die "The 'request' and 'url' options are mutually exclusive!"
43 15 50 33     68 if $params{url} && $params{request};
44              
45             # Process and serialize the status code checker
46 15   100     63 $params{status_code} ||= '200';
47 15         25 my (@and, @or);
48 15         112 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         100 my ($op, $code) = $part =~ m{\A\s*(>=|>|<=|<|!=|!)?\s*(\d{3})\z};
51              
52 17 50       53 die "The 'status_code' condition '$part' is not in the correct format!"
53             unless defined $code;
54 17 100 100     59 $op = '!=' if defined $op && $op eq '!';
55              
56 17 100       38 unless ($op) { push @or, '$_ == '.$code; }
  10         38  
57 7         25 else { push @and, '$_ '."$op $code"; }
58             }
59 15 100       62 push @or, '('.join(' && ', @and).')' if @and; # merge @and as one big condition into @or
60 15         46 $params{status_code_eval} = join ' || ', @or;
61              
62 15   33     91 $params{request} //= HTTP::Request->new('GET', $params{url});
63 15   100     9424 $params{options} //= {};
64 15   50     81 $params{options}{agent} //= LWP::UserAgent->_agent .
      66        
65             " HealthCheck-Diagnostic-WebRequest/" . ( $class->VERSION || '0' );
66 15   100     280 $params{options}{timeout} //= 7; # Decided by committee
67              
68 15         77 return $class->SUPER::new(
69             label => 'web_request',
70             %params,
71             );
72             }
73              
74             sub check {
75 21     21 1 8631 my ($self, @args) = @_;
76              
77 21 100       284 croak("check cannot be called as a class method")
78             unless ref $self;
79 20         64 return $self->SUPER::check(@args);
80             }
81              
82             sub run {
83 20     20 1 434 my ( $self, %params ) = @_;
84 20         37 my $ua = LWP::UserAgent->new( %{$self->{options}} );
  20         94  
85              
86 20 50       7084 $ua->requests_redirectable([]) if $self->{'no_follow_redirects'};
87              
88 20         61 my $response = $ua->request( $self->{request} );
89              
90 20         27478 my @results = $self->check_status( $response );
91             push @results, $self->check_content( $response )
92 20 100       73 if $results[0]->{status} eq 'OK';
93              
94 20         44 my $info = join '; ', grep { length } map { $_->{info} } @results;
  24         76  
  24         63  
95              
96 20         377 return { info => $info, results => \@results };
97             }
98              
99             sub check_status {
100 20     20 0 44 my ( $self, $response ) = @_;
101 20         37 my $status;
102              
103 20   100     53 my $client_warning = $response->header('Client-Warning') // '';
104 20   100     1067 my $proxy_error = $response->header('X-Squid-Error') // '';
105              
106             # Eval the status checker
107 20         999 my $success;
108             {
109 20         36 local $_ = $response->code;
  20         48  
110 20         1582 $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       91 die "Status code checker eval '".$self->{status_code_eval}."' failed: $@" if $@;
115              
116 20 100       58 $status = $success ? 'OK' : 'CRITICAL';
117              
118             # Proxy error is an automatic failure
119 20 100       46 $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       77 $status eq 'OK' ? ' expected' : '',
124             $response->code,
125             );
126 20 100       575 $info .= " from proxy with error '$proxy_error'" if $proxy_error;
127 20 100 100     94 $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     60 if ($client_warning && $client_warning eq 'Internal response') {
131 2         5 $status = 'CRITICAL';
132 2         6 $info = "User Agent returned: ".$response->message;
133             }
134              
135 20         116 return { status => $status, info => $info };
136             }
137              
138             sub check_content {
139 10     10 0 25 my ( $self, $response ) = @_;
140              
141 10 100       29 return unless $self->{content_regex};
142              
143 4         7 my $regex = $self->{content_regex};
144 4         16 my $content = $response->content;
145 4 100       73 my $status = $content =~ /$regex/ ? 'OK' : 'CRITICAL';
146 4 100       11 my $successful = $status eq 'OK' ? 'matches' : 'does not match';
147              
148             return {
149 4         20 status => $status,
150             info => "Response content $successful /$regex/",
151             };
152             }
153              
154             1;
155              
156             __END__