File Coverage

blib/lib/InfluxDB/Client.pm
Criterion Covered Total %
statement 26 121 21.4
branch 0 74 0.0
condition 0 39 0.0
subroutine 9 15 60.0
pod 5 5 100.0
total 40 254 15.7


line stmt bran cond sub pod time code
1             package InfluxDB::Client;
2             # ABSTRACT: query and manage InfluxDB
3              
4              
5 3     3   97393 use strict;
  3         5  
  3         94  
6 3     3   13 use warnings;
  3         5  
  3         55  
7 3     3   27 use v5.10;
  3         8  
8              
9             our $VERSION = '0.1';
10              
11 3     3   18 use Carp;
  3         7  
  3         138  
12 3     3   1157 use LWP::UserAgent;
  3         100897  
  3         81  
13 3     3   821 use HTTP::Request::Common;
  3         5072  
  3         185  
14 3     3   19 use URI;
  3         7  
  3         73  
15 3     3   16 use HTTP::Status ();
  3         6  
  3         57  
16              
17 3     3   989 use InfluxDB::Client::Response;
  3         6  
  3         3538  
18              
19              
20             sub new {
21 0     0 1   my ( $class, %args ) = @_;
22            
23             # validate args
24 0 0         croak "no url given" unless $args{url};
25 0   0       my $url = $args{url} || 'http://localhost:8086';
26            
27 0 0 0       croak "url must be a string or an ARRAY ref"
28             if ( ref $url && ref $url ne 'ARRAY' );
29            
30 0 0         $url = ref $url ? $url : [ $url ];
31            
32 0           foreach ( @$url ) {
33             # we check each given url, if it is valid
34 0           my $uri = URI->new($_);
35 0 0 0       croak "invalid url given: $_"
      0        
36             unless ( $uri && $uri->has_recognized_scheme && $uri->scheme =~ m/^https*$/ );
37             }
38            
39 0   0       my $opts = $args{ua_opts} // {};
40 0 0 0       croak "ua_opts must be a hash reference"
41             unless ( ref $opts && ref $opts eq 'HASH' );
42            
43 0   0       $opts->{agent} //= sprintf('perl-InfluxDB-Client/%f',$VERSION);
44            
45             # 10 second timeout should be more then enough
46 0   0       $opts->{timeout} //= 10;
47            
48             # check if a user or password arge given solo
49             croak "username without a password supplied"
50 0 0 0       if ( $args{username} && !$args{password} );
51             croak "password without a username supplied"
52 0 0 0       if ( $args{password} && !$args{username} );
53            
54             my $self = bless {
55             # we don't save URI objects here, es we have construct them later on the fly to implement RR
56             urls => $url,
57             username => $args{username},
58             password => $args{password},
59             # index for the round robin mechanism
60 0           _cur_uri => 0,
61             _num_uri => scalar(@$url),
62            
63             # our useragent
64             _ua => LWP::UserAgent->new( %$opts )
65             } => $class;
66            
67 0           return $self;
68             }
69              
70             sub ping {
71 0     0 1   my ( $self ) = @_;
72 0           my $response = $self->_send_request('GET','/ping');
73            
74             # our status code must be 204
75 0 0         if ($response->http_code != 204) {
76             # everything else is an error
77 0           $response->status(InfluxDB::Client::Response::Status::SRVFAIL);
78             }
79 0           return $response;
80             }
81              
82             sub query {
83             # shortcut, if we only get an query as parameter
84 0 0   0 1   if (@_ == 2) {
85 0           return $_[0]->_send_request('GET','/query', { q => $_[1] });
86             }
87            
88             # else we expect a hash
89 0           my ( $self, %args ) = @_;
90            
91             # check the query argument
92             croak 'no query given'
93 0 0         unless $args{query};
94             croak 'query must not be a reference'
95 0 0         unless (!ref $args{query});
96            
97             # validate the method argument
98 0           my $method = 'GET';
99 0 0         if ($args{method}) {
100             croak 'method must be either "GET" or "POST"'
101 0 0         unless ($args{method} =~ m/^(GET|POST)$/);
102 0           $method = $args{method};
103             }
104            
105             # validate the epoch argument
106 0           my $epoch = 'ns';
107 0 0         if ($args{epoch}) {
108             croak 'epoch must be one of "ns","u","ms","s","m","h"'
109 0 0         unless ($args{epoch} =~ m/^(s|u|ms|s|m|h)$/);
110 0           $epoch = $args{epoch};
111             }
112            
113             # build our parameter lis
114 0           my $data;
115 0           my $qs = { epoch => $epoch };
116 0 0         $qs->{db} = $args{db} if ($args{db});
117 0 0         if ( $method eq 'POST' ) {
118             # if we post our query, we have to put it in the body
119             $data = { q => $args{query} }
120 0           } else {
121 0           $qs->{q} = $args{query};
122             }
123            
124             # send our request and return the result
125 0           return $self->_send_request($method,'/query',$qs,$data);
126             }
127              
128              
129             sub write {
130 0     0 1   my ( $self, %args ) = @_;
131            
132             # check the database argument
133             croak 'no database given'
134 0 0         unless $args{database};
135             croak 'database must not be a reference'
136 0 0         if (ref $args{database});
137            
138             # check the data argument
139             croak 'no data given'
140 0 0         unless $args{data};
141             croak 'database must be an array reference'
142 0 0 0       unless (ref $args{data} && ref $args{data} eq 'ARRAY');
143            
144             # precision argument
145 0 0         unless ($args{precision}) {
146             # check the data array first
147 0 0 0       if ( ref $args{data}[0] && ref $args{data}[0] eq 'InfluxDB::Client::DataSet' ) {
148 0           $args{precision} = $args{data}[0]->precision;
149             } else {
150 0           $args{precision} = 'ns';
151             }
152             } else {
153             croak 'precision must be one of "ns","u","ms","s","m","h"'
154 0 0         unless ($args{precision} =~ m/^(s|u|ms|s|m|h)$/);
155             }
156              
157             # the consistency argument
158 0 0         if ($args{consistency}) {
159             croak 'consistency must not be a reference'
160 0 0         if (ref $args{consistency});
161              
162             croak 'consistency must be one of "any","one","quorum","all'
163 0 0         unless ($args{consistency} =~ m/^(any|one|quorum|all)$/);
164             } else {
165 0           $args{consistency} = "one";
166             }
167            
168             # all required paramters are checked, we can build our data to write
169             my $qs = {
170             db => $args{database},
171             precision => $args{precision},
172             consistency => $args{consistency},
173 0           };
174             # we only set the retention policy if we have one
175 0 0         $qs->{rp} = $args{rp} if $args{rp};
176            
177             # we need to prepare our data
178 0           my @data;
179 0           foreach my $ds ( @{$args{data}} ) {
  0            
180 0 0         if (ref $ds) {
181 0 0         croak sprintf('invalid type "%s" for a dataset object',ref $ds)
182             unless (ref $ds eq 'InfluxDB::Client::DataSet');
183 0           push @data,$ds->to_string;
184             } else {
185 0           push @data,$ds;
186             }
187             }
188            
189             # write the data and return the result
190 0           return $self->send_request('POST','/write',$qs,join("\n",@data));
191             }
192             sub show_databases {
193 0     0 1   my ( $self ) = @_;
194 0           my $response = $self->query('SHOW DATABASES');
195            
196 0 0         if ( $response->status == InfluxDB::Client::Response::Status::OK ) {
197 0           $response->content([ map { $_->[0] } @{$response->content->[0]{series}[0]{values}} ]);
  0            
  0            
198             }
199 0           return $response;
200             }
201              
202             # helper to send a request to the target database
203             sub _send_request {
204 0     0     my ( $self, $method, $path, $qs, $data ) = @_;
205            
206             # we build a new URI object to generate the string first
207 0           my $uri = URI->new($path);
208 0 0         $uri->query_form($qs) if $qs;
209            
210 0           my $resp;
211             # iterate over our hosts and try to get an response
212 0           for(my $i = 0; $i < $self->{_num_uri}; ++$i) {
213             # make sure we are within the valid range of uri's
214 0           my $idx = $self->{_cur_uri} + $i;
215 0 0         $idx -= $idx >= $self->{_num_uri} if ( $idx >= $self->{_num_uri});
216            
217 0           my $url = $uri->abs($self->{urls}[$idx])->as_string();
218 0           my $req;
219 0 0         if ( $method eq 'POST' ) {
220             # with POST, we need to add the body content
221 0           $req = POST(
222             $url,
223             Content => $data
224             );
225             } else {
226             # all other request types could you use there constructor from HTTP::Request::Common
227 0           $req = &{\&{$method}}($url);
  0            
  0            
228             }
229            
230 0 0         if ( $self->{username} ) {
231             # if authentication is enabled, we send the login data with the request
232             # LWP::UserAgent would need a REALM, but most user wouldn't know what to enter there,
233             # and it would make the code on user-side more complex, so we HTTP::Request::Common for that
234 0           $req->authorization_basic($self->{username}, $self->{password});
235             }
236            
237             # send our request
238 0           $resp = $self->{_ua}->request($req);
239            
240             # check our response code, if we got a timeout or 5xx, we should try the next server
241 0 0 0       if ( $resp->code == HTTP::Status::HTTP_REQUEST_TIMEOUT || ( $resp->code >= 500 && $resp->code < 600 ) ) {
      0        
242 0           next;
243             }
244            
245             # increment the current index, make sure we don't overflow
246 0           ++$self->{_cur_uri};
247 0 0         $self->{_cur_uri} = 0 if $self->{_cur_uri} == $self->{_num_uri};
248            
249             # simply break the loop here, we always return the last response we got
250 0           last;
251             }
252            
253             # we encapsulate the response and return it
254 0           return InfluxDB::Client::Response->new( http_response => $resp );
255             }
256              
257             1;
258              
259             __END__