File Coverage

blib/lib/WWW/Splunk/API.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


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