File Coverage

blib/lib/Net/Async/Graphite/API.pm
Criterion Covered Total %
statement 50 64 78.1
branch 5 8 62.5
condition 0 6 0.0
subroutine 15 19 78.9
pod 5 5 100.0
total 75 102 73.5


line stmt bran cond sub pod time code
1             package Net::Async::Graphite::API;
2              
3             our $VERSION = '0.1_1';
4              
5             =head1 NAME
6              
7             Net::Async::Graphite::API - Interface between perl and graphite.
8              
9             =head1 SYNOPSIS
10              
11             with 'Net::Async::Graphite::API';
12              
13             =head1 DESCRIPTION
14              
15             Don't use this module directly, use L and create
16             objects using its C method in the normal way. Those objects will
17             include the functionality documented here.
18              
19             This role brings the capacity to transmit HTTP requests to graphite
20             and asynchronously wait for and return the response.
21              
22             It also defines a structure describing complex target paths in a perl
23             data structure.
24              
25             =head1 BUGS
26              
27             Assumes graphite-api and doesn't handle transport errors at all.
28              
29             Using Memoize may be pointles.
30              
31             =cut
32              
33 3     3   21459 use v5.14;
  3         12  
34 3     3   21 use strictures 2;
  3         25  
  3         145  
35 3     3   695 use Moo::Role;
  3         7  
  3         15  
36 3     3   1153 use Carp;
  3         11  
  3         184  
37              
38 3     3   1648 use Memoize qw(memoize);
  3         5566  
  3         149  
39 3     3   1017 use PerlX::Maybe qw(maybe);
  3         4050  
  3         153  
40 3     3   22 use URI;
  3         7  
  3         62  
41 3     3   21 use namespace::clean;
  3         8  
  3         25  
42              
43             =head1 ROLE
44              
45             Asynchronity is achieved by using L inside
46             L.
47              
48             JSON data is decoded using a L object handled by
49             L.
50              
51             =cut
52              
53             with 'Net::Async::Graphite::HTTPClient';
54              
55             with 'Net::Async::Graphite::JSONCodec';
56              
57             =head1 ATTRIBUTES
58              
59             =over
60              
61             =item endpoint (required, read-only)
62              
63             The base of the graphite URL to which C and C
64             requests will be directed. Everything up to and B the
65             trailing C. eg. C.
66              
67             If the trailing C is not included it's assumed you know what you're
68             doing so only a warning will be printed.
69              
70             =cut
71              
72             has endpoint => (
73             is => 'ro',
74             isa => sub {
75             carp 'endpoint should include a trailing "/", attempting to continue'
76             unless $_[0] =~ /\/$/;
77             croak 'endpoint must include a protocol' unless $_[0] =~ /^[a-z-]+:/i;
78             },
79             required => 1,
80             );
81              
82             =back
83              
84             =head1 PUBLIC METHODS
85              
86             All of these methods return a L which will complete with the
87             indicated result (or which will die).
88              
89             =over
90              
91             =item metrics ($method, $query, [%extra])
92              
93             Perform C<$method> on/with the metrics which match C<$query>, which
94             should be a simple scalar describing the query to perform, or an
95             arrayref of such scalars if the method can handle multiple query
96             parameters. ie. this will request a URI which looks like:
97             C<< /metrics/<$method>?query=<$query>[&query=...] >>. If
98             arguments are supplied in C<%extra>, they are added to the generated
99             URI as encoded query parameters.
100              
101             The three methods which graphite-api exposes at this time are supported:
102              
103             =over
104              
105             =item find
106              
107             =item expand
108              
109             =item index[.json]
110              
111             C can be requested with C<$method> of C or
112             C because requiring the C<.json> suffix is silly. Also I
113             can consider creating real methods C, C and C.
114              
115             =back
116              
117             The L will complete with the http content. If you want the
118             HTTP request object see the C<_download> private method, but note
119             I.
120              
121             =cut
122              
123             my %allowed_metrics_params = (
124             expand => [qw(groupByExpr leavesOnly)],
125             find => [qw(wildcards from until position)],
126             index => [],
127             );
128             $allowed_metrics_params{'index.json'} = $allowed_metrics_params{index}; # FFS
129              
130             sub metrics {
131 8     8 1 123 my $self = shift;
132 8         28 my ($method, $query, %extra) = @_;
133 8 100       33 $method = 'index.json' if $method eq 'index';
134             return Future->fail("Invalid metrics method: $method")
135 8 50       39 unless exists $allowed_metrics_params{$method};
136 8         89 my $uri = URI->new($self->endpoint . "metrics/$method");
137             my %query = (
138             maybe query => $query, # No query in index
139 8         12102 map { maybe $_ => $extra{$_} } @{ $allowed_metrics_params{$method} },
  12         64  
  8         45  
140             );
141             # Only if .../find. Is default_* stupid?
142 8 50 0     53 $query{from} ||= $self->default_from if $self->has_default_from;
143 8 50 0     38 $query{until} ||= $self->default_until if $self->has_default_until;
144 8         54 $uri->query_form(%query);
145 8     8   814 $self->_download($uri)->then(sub { Future->done($_[0]->content) });
  8         76737  
146             }
147              
148             =item metrics_asperl ($method, $query, [%extra])
149              
150             Calls C with the same arguments and decodes the result,
151             which is assumed to be JSON text, into a perl data structure.
152              
153             =cut
154              
155             sub metrics_asperl {
156 4     4 1 111 my $self = shift;
157 4     4   22 $self->metrics(@_)->then(sub { Future->done($self->_json->decode(shift)); });
  4         664  
158             }
159              
160             # L. ie. this
161              
162             =item render ($format, $target, [%extra])
163              
164             Fetch the metric data for the given C<$target>, which should be a
165             simple scalar specifying a path identifying metrics, or an arrayref of
166             such scalars, as described in the
167             L<< graphite C documentation | http://graphite.readthedocs.io/en/latest/render_api.html >>
168             C will then request a URI which looks like:
169             C<< /render?format=<$format>&target=<$query>[&target=...] >>.
170              
171             The name of the format may also be called as a method directly,
172             without the first (C<$format>) argument:
173              
174             =over
175              
176             =item csv ($target, [%extra])
177              
178             =item dygraph ($target, [%extra])
179              
180             =item json ($target, [%extra])
181              
182             =item pdf ($target, [%extra])
183              
184             =item png ($target, [%extra])
185              
186             =item raw ($target, [%extra])
187              
188             =item rickshaw ($target, [%extra])
189              
190             =item svg ($target, [%extra])
191              
192             =back
193              
194             =cut
195              
196             sub render {
197 8     8 1 201 my $self = shift;
198 8         29 my ($format, $target, %extra) = @_;
199 8         86 my $uri = URI->new($self->endpoint . 'render');
200             my %query = (
201             format => $format,
202             target => $target,
203 8         7258 map { maybe $_ => $extra{$_} } qw(graphType jsonp maxDataPoints noNullPoints), # Many more
  32         123  
204             );
205 8         61 $uri->query_form(%query);
206 8     8   846 $self->_download($uri)->then(sub { Future->done($_[0]->content) });
  8         63919  
207             }
208              
209             for my $format (qw(
210             csv
211             dygraph
212             json
213             pdf
214             png
215             raw
216             rickshaw
217             svg
218             )) {
219 3     3   2307 no strict 'refs';
  3         6  
  3         1446  
220 0     0     *{$format} = sub { $_[0]->render($format => @_[1..$#_]) };
221             }
222              
223             =item render_asperl ($target, [%extra])
224              
225             Calls C with the same arguments but prepended by the format
226             C, and decodes the result.
227              
228             Graphite's raw format is documented in
229             L<< graphite C documentation | http://graphite.readthedocs.io/en/latest/render_api.html >>
230             but in short it's an ASCII (or UTF-8 if you're so inclined) encoded
231             file consisting of one line per metric found. Each line consists of a
232             header and the data separated by a C<|>. Within each of these each
233             datum is separated by a C<,>.
234              
235             I don't know or care if the line endings include C<\r>.
236              
237             The data is returned as a list of hashrefs containing the data broken
238             up into B of its components. ie. Each datum of each metric will
239             be turned into its own perl scalar.
240              
241             Each hashref will contain five values:
242              
243             =over
244              
245             =item target
246              
247             The path of metric.
248              
249             =item start
250              
251             Unix timestamp pinpointing the beginning of the data.
252              
253             =item end
254              
255             Unix timestamp pinpointing the end of the data.
256              
257             =item step
258              
259             The time interval in seconds (probably) between each datum.
260              
261             =item data
262              
263             The data. An arrayref of however many scalars it takes.
264              
265             The data are B converted from their original text form, and
266             graphite include the string C in the list of returned values, so
267             not every datum will necessarily look like a number.
268              
269             =back
270              
271             =cut
272              
273             sub render_asperl {
274 0     0 1   my $self = shift;
275             $self->render(raw => @_)->then(sub {
276             my @sets = map {
277 0     0     chomp;
  0            
278 0           my ($head, $data) = split /\|/;
279 0           my %set;
280 0           (@set{qw(target start end step)}) = split /,/, $head;
281 0           $set{data} = [ split /,/, $data ];
282 0           \%set;
283             } split /\n/, $_[0];
284 0           Future->done(@sets);
285 0           });
286             }
287              
288             =item find_target_from_spec ($spec, ...)
289              
290             Construct strings for use by the C parameter of C
291             queries from the specifications in each C<$spec>, which must be a
292             plain text scalar of the metric's path or an arrayref containing 2
293             items (second optional):
294              
295             =over
296              
297             =item $function
298              
299             The name of a function as defined by the graphite API.
300              
301             =item [ @arguments ] (optional)
302              
303             An arrayref containing 0 or more arguments. Each argument may be a
304             scalar, which left alone, or an arrayref which must itself be another
305             C<$spec>-like scalar/pair.
306              
307             =back
308              
309             It sounds a lot more confusing than it is. These are valid:
310              
311             # Scalar
312              
313             'simple.metric.name'
314              
315             # Becomes "simple.metric.name"
316              
317              
318             # Function
319              
320             [ 'summarize' => [ 'simple.metric.name' ] ]
321              
322             # Becomes "summarize(simple.metric.name)"
323              
324              
325             # Function with more arguments
326              
327             [ 'summarize' => [ 'me.tr.ic', '"arg1"', 42 ] ]
328              
329             # Becomes "summarize(me.tr.ic,%52arg1%52,42)"
330              
331              
332             # Recursive function
333              
334             [
335             'summarize' => [
336             'randomize' => [
337             'pointless.metric', 'argue', 'bicker'
338             ],
339             'and-fight',
340             ]
341             ]
342              
343             # Becomes "summarize(randomize(pointless.metric,argue,bicker),and-fight)"
344              
345              
346             Any other form is not.
347              
348             Simply put, where it's not just a plain string the specification is a
349             recursive nest of function/argument-list pairs, where each argument
350             can itself be another function pair.
351              
352             The implementation of the target string construction uses L
353             to avoid repeatedly calling functions which return the same data. It
354             probably destroys any advantage gained by doing this by normalising
355             the function arguments with JSON first. I'll measure it some day.
356              
357             =cut
358              
359             sub find_target_from_spec {
360 0     0 1   my $self = shift;
361 0           return map { __construct_target_argument($_) } @_;
  0            
362             }
363              
364             =back
365              
366             =head1 PRIVATE METHODS
367              
368             =over
369              
370             =item __construct_target_argument ($argument)
371              
372             This is not an object or class method.
373              
374             Compile C<$argument> into its part of the string which will build up
375             the (or a) target component of a Graphite request URI. C<$argument>
376             must be a scalar or an arrayref with exactly one or two items in
377             it. If included, the second must be an arrayref of the function's
378             arguments.
379              
380             Returns the scalar as-is or calls C<__construct_target_function> to
381             decode the arrayref.
382              
383             This function uses L for which it normalises its arguments
384             using L.
385              
386             =cut
387              
388             memoize('__construct_target_argument',
389             NORMALIZER => sub { ref($_[0]) ? encode_json($_[0]) : $_[0] });
390              
391             sub __construct_target_argument {
392             my ($argument) = @_;
393             my $ref = ref $argument;
394             if (not $ref) {
395             $argument;
396             } elsif ($ref eq 'ARRAY' and scalar @$argument <= 2) {
397             croak 'Function arguments must be an arrayref'
398             unless not defined $argument->[1] or ref $argument->[1] eq 'ARRAY';
399             __construct_target_function($argument->[0], @{ $argument->[1] || [] });
400             } else {
401             croak "Unknown argument reference type: $ref";
402             }
403             }
404              
405             =item __construct_target_function ($name, [@arguments])
406              
407             This is not an object or class method.
408              
409             Compile C<$name> and C<@arguments> into their part of the string which
410             will build up the target component of a Graphite request URI. Recurses
411             back into C<__construct_target_argument> to build each argument
412             component.
413              
414             This function uses L for which it normalises its arguments
415             using L.
416              
417             =cut
418              
419             memoize('__construct_target_function',
420             NORMALIZER => sub { encode_json({@_}) });
421              
422             sub __construct_target_function {
423             my $name = shift;
424             croak 'Function name must be a scalar' if ref $name;
425             my @arguments = map { __construct_target_argument($_) } @_;
426             $name . '(' . join(',', @arguments) . ')';
427             }
428              
429             1;
430              
431             =back
432              
433             =head1 SEE ALSO
434              
435             Graphite's C documentation L
436              
437             L
438              
439             L
440              
441             L
442              
443             L
444              
445             L
446              
447             L
448              
449             =head1 AUTHOR
450              
451             Matthew King
452              
453             =cut