File Coverage

blib/lib/WWW/Splunk/API.pm
Criterion Covered Total %
statement 18 102 17.6
branch 0 54 0.0
condition 0 21 0.0
subroutine 6 15 40.0
pod 7 7 100.0
total 31 199 15.5


line stmt bran cond sub pod time code
1             =encoding utf8
2              
3             =head1 NAME
4              
5             WWW::Splunk::API - Splunk REST client
6              
7             =head1 DESCRIPTION
8              
9             L is a low-level interface to Splunk
10             log search engine. It deals with HTTP communication as well as
11             working around certain interface glitches.
12              
13             See L
14             for API definition.
15              
16             This module is designed to be Splunk API version agnostic.
17              
18             =cut
19              
20             package WWW::Splunk::API;
21              
22 4     4   112163 use LWP::UserAgent;
  4         245706  
  4         158  
23 4     4   2077 use HTTP::Request::Common;
  4         7994  
  4         308  
24 4     4   1703 use WWW::Splunk::XMLParser;
  4         23  
  4         232  
25 4     4   29 use Carp;
  4         13  
  4         232  
26              
27 4     4   21 use strict;
  4         6  
  4         82  
28 4     4   15 use warnings;
  4         8  
  4         6600  
29              
30             our $VERSION = '2.09';
31              
32             =head2 B (F)
33              
34             A constructor.
35              
36             my $splunk = WWW::Splunk::API->new({
37             host => $host,
38             port => $port,
39             login => $login,
40             password => $password,
41             unsafe_ssl => 0,
42             verbose => 0,
43             });
44              
45             Default values are:
46             - port - 8089
47             - host - localhost
48             - url - https://$host:$port
49             - verbose - 0
50             - unsafe_ssl - 0
51             - service_prefix - /services
52             - search - 1
53              
54             =cut
55              
56             sub new {
57 0     0 1   my ($class, $self) = @_;
58              
59 0   0       $self->{port} ||= 8089;
60 0   0       $self->{host} ||= 'localhost';
61 0   0       $self->{url} ||= 'https://'.$self->{host}.':'.$self->{port};
62 0   0       $self->{verbose} ||= 0;
63 0   0       $self->{service_prefix} ||= '/services';
64 0   0       $self->{search} ||= 1;
65              
66             # Set up user agent unless an existing one was passed
67 0 0         unless ($self->{agent}) {
68             $self->{agent} = LWP::UserAgent->new(
69 0           ssl_opts => {verify_hostname => (not $self->{unsafe_ssl})},
70             );
71 0           $self->{agent}->cookie_jar ({});
72             $self->{agent}->credentials(
73             delete ($self->{host}).':'.(delete $self->{port}),
74             '/splunk',
75             delete $self->{login},
76             delete $self->{password},
77 0 0         ) if exists $self->{login};
78 0           $self->{agent}->agent("$class/$VERSION ");
79             }
80              
81 0           return bless $self, $class;
82             }
83              
84             =head2 B (F)
85              
86             Wrapper around HTTP::Request::Common::DELETE().
87              
88             =cut
89              
90             sub delete {
91 0     0 1   my ($self, @args) = @_;
92              
93 0 0         print "DELETE" if $self->{verbose};
94 0           $self->request(\&DELETE, @args);
95             }
96              
97             =head2 B (F)
98              
99             Wrapper around HTTP::Request::Common::POST().
100              
101             =cut
102              
103             sub post {
104 0     0 1   my ($self, @args) = @_;
105              
106 0 0         print "POST" if $self->{verbose};
107 0           $self->request(\&POST, @args);
108             }
109              
110             =head2 B (F)
111              
112             Wrapper around HTTP::Request::Common::GET().
113              
114             =cut
115              
116             sub get {
117 0     0 1   my ($self, @args) = @_;
118              
119 0 0         print "GET" if $self->{verbose};
120 0           $self->request(\&GET, @args);
121             }
122              
123             =head2 B (F)
124              
125             Wrapper around HTTP::Request::Common::HEAD().
126             Not used anywhere in splunk API
127              
128             =cut
129              
130             sub head {
131 0     0 1   my ($self, @args) = @_;
132              
133 0 0         print "HEAD" if $self->{verbose};
134 0           $self->request(\&HEAD, @args);
135             }
136              
137             =head2 B (F)
138              
139             Wrapper around HTTP::Request::Common::PUT().
140             Not used anywhere in splunk API
141              
142             =cut
143              
144             sub put {
145 0     0 1   my ($self, @args) = @_;
146              
147 0 0         print "PUT" if $self->{verbose};
148 0           $self->request(\&PUT, @args);
149             }
150              
151             =head2 B (F, F, [F], [F])
152              
153             Request a Splunk api and deal with the results.
154              
155             Method can be either a L instance (see L
156             for useful ones), or a plain string, such as "GET" or "DELETE."
157              
158             Optional F is has reference gets serialized into a request body for POST
159             request. Use I in case you don't have any data to send, but need to
160             specify a callback function in subsequent argument.
161              
162             Call-back function can be specified for a single special case, where a XML stream
163             of elements is expected.
164              
165             =cut
166              
167             sub request {
168 0     0 1   my ($self, $method, $location, $data, $callback) = @_;
169              
170 0           my $url = $self->{url}.$self->{service_prefix}.$location;
171 0 0         if ($self->{verbose}) {
172 0           print " $url\n";
173 0 0         if (defined $data) {
174 0           foreach my $key (sort keys %$data) {
175 0           my $value = $data->{$key};
176 0           $value =~ s/\n/ /msg;
177 0           print "- $key => $value\n";
178             }
179             }
180             }
181              
182             # Construct the request
183 0           my $request;
184 0 0 0       if (ref $method and ref $method eq 'CODE') {
185             # Most likely a HTTP::Request::Common
186 0 0         if (! defined $data) {
187 0           $request = $method->($url);
188             } else {
189 0           $request = $method->($url, $data);
190             }
191             } else {
192             # A method string
193 0           $request = HTTP::Request->($method, $url);
194             }
195              
196 0           my $content_type = '';
197 0           my $buffer;
198              
199 0           $self->{agent}->remove_handler('response_header');
200             $self->{agent}->add_handler(response_header => sub {
201 0     0     my ($response, $ua, $h) = @_;
202              
203             # Do not think of async processing of error responses
204 0 0         return 0 unless $response->is_success;
205              
206 0   0       my $content_type_header = $response->header('Content-Type') // '';
207 0 0         if ($content_type_header =~ /^([^\s;]+)/) {
    0          
208 0           $content_type = $1;
209             } elsif ($response->code ne 204) {
210             # Sometimes splunk return HTTP 204 NO CONTENT during poll_search() call,
211             # Content-Type header is empty in this case. We must not croak in this case.
212 0           croak "Missing or invalid Content-Type: $content_type_header";
213             }
214              
215 0 0         if ($callback) {
216 0           $response->{default_add_content} = 0;
217 0           $buffer = "";
218             }
219 0           });
220              
221 0           $self->{agent}->remove_handler('response_data');
222             $self->{agent}->add_handler(response_data => sub {
223 0     0     my ($response, $ua, $h, $data) = @_;
224              
225 0 0         return 1 unless defined $buffer;
226 0           $buffer .= $data;
227 0           foreach (split /<\/results>\K/, $buffer) {
228 0 0         unless (/<\/results>$/) {
229 0           $buffer = $_;
230 0           last;
231             }
232              
233 0           my $xml = XML::LibXML->load_xml(string => $_);
234 0           $callback->(WWW::Splunk::XMLParser::parse($xml));
235             }
236              
237 0           return 1;
238 0 0         }) if $callback;
239              
240             # Run it
241 0           my $response = $self->{agent}->request($request);
242 0 0         croak $response->header ('X-Died') if $response->header ('X-Died');
243              
244             # Deal with HTTP errors
245 0 0         unless ($response->is_success) {
246 0 0         my $content = WWW::Splunk::XMLParser::parse ($response->content)
247             if $response->header ('Content-Type') =~ /xml/;
248 0           my $error = "HTTP Error: ".$response->status_line;
249             $error .= sprintf "\n%s: %s",
250             $content->findvalue ('/response/messages/msg/@type'),
251             $content->findvalue ('/response/messages/msg')
252 0 0 0       if eval { $content->isa ('XML::LibXML::Document') }
  0            
253             and $content->documentElement->nodeName eq 'response';
254 0           croak $error;
255             }
256              
257             # We've gotten the response already
258 0 0         return if $callback;
259              
260             # Parse content from synchronous responses
261             # TODO: use callback and m_media_type matchspecs
262 0 0         if ($content_type eq 'text/xml') {
    0          
    0          
263 0           my $xml = XML::LibXML->load_xml (string => $response->content);
264 0           my @ret = WWW::Splunk::XMLParser::parse ($xml);
265 0 0         return $#ret ? @ret : $ret[0];
266             } elsif ($response->code eq 204) {
267             # "No content"
268             # Happens when events are requested immediately
269             # after the job is enqueued. With a text/plain content type
270             # Empty array is the least disturbing thing to return here
271 0           return ();
272             } elsif ($content_type eq 'text/plain') {
273             # Sometimes an empty text/plain body is sent
274             # even without 204 return code.
275 0           return ();
276             } else {
277             # TODO: We probably can't do much about RAW
278             # format, yet we could parse at least JSON
279 0           croak "Unknown content type: $content_type";
280             }
281             }
282              
283             =head1 SEE ALSO
284              
285             L, L
286              
287             =head1 AUTHORS
288              
289             Lubomir Rintel, L<< >>,
290             Michal Josef Špaček L<< >>
291              
292             The code is hosted on GitHub L.
293             Bug fixes and feature enhancements are always welcome.
294              
295             =head1 LICENSE
296              
297             This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
298              
299             =cut