File Coverage

blib/lib/Monitor/MetricsAPI/Collector.pm
Criterion Covered Total %
statement 88 115 76.5
branch 29 54 53.7
condition 9 27 33.3
subroutine 15 15 100.0
pod 5 5 100.0
total 146 216 67.5


line stmt bran cond sub pod time code
1 13     13   80 use strict;
  13         29  
  13         540  
2 13     13   67 use warnings;
  13         24  
  13         956  
3              
4             package Monitor::MetricsAPI::Collector;
5             $Monitor::MetricsAPI::Collector::VERSION = '0.900';
6 13     13   65 use namespace::autoclean;
  13         19  
  13         143  
7 13     13   1194 use Moose;
  13         28  
  13         240  
8 13     13   92944 use Socket qw(:addrinfo SOCK_RAW);
  13         53429  
  13         5767  
9              
10 13     13   6627 use Monitor::MetricsAPI::MetricFactory;
  13         43  
  13         782  
11 13     13   7772 use Monitor::MetricsAPI::Server;
  13         81  
  13         22452  
12              
13             =head1 NAME
14              
15             Monitor::MetricsAPI::Collector - Metrics collection object
16              
17             =head1 SYNOPSIS
18              
19             You should not create your own objects from this module directly. All Collector
20             objects should be instantiated through the L<Monitor::MetricsAPI> module's
21             create() method. Please refer to that module's documentation for information on
22             setting up your application's usage of this library.
23              
24             =cut
25              
26             =head1 DESCRIPTION
27              
28             =cut
29              
30             has 'servers' => (
31             is => 'ro',
32             isa => 'HashRef',
33             default => sub { {} },
34             );
35              
36             has 'metrics' => (
37             is => 'ro',
38             isa => 'HashRef',
39             default => sub { {} },
40             );
41              
42             sub BUILDARGS {
43 12     12 1 45 my $class = shift;
44 12         44 my %args;
45              
46 12 50 33     153 if (@_ == 1 && ref($_[0]) eq 'HASH') {
    50          
47 0         0 %args = %{$_[0]};
  0         0  
48             } elsif (@_ % 2 == 0) {
49 12         68 %args = @_;
50             }
51              
52 12 100       60 if (exists $args{'metrics'}) {
53 9 50       50 if (ref($args{'metrics'}) eq 'HASH') {
54 9         17 my %m;
55 9         61 foreach my $metric (_parse_metrics_hash($args{'metrics'})) {
56 14 100       67 if (ref($metric->[1]) eq 'CODE') {
57 2         26 $m{$metric->[0]} = Monitor::MetricsAPI::MetricFactory->create(
58             name => $metric->[0],
59             type => 'callback',
60             cb => $metric->[1],
61             );
62             } else {
63 12         127 $m{$metric->[0]} = Monitor::MetricsAPI::MetricFactory->create(
64             name => $metric->[0],
65             type => $metric->[1],
66             );
67             }
68             }
69 9         43 $args{'metrics'} = \%m;
70             } else {
71 0         0 warn "metrics option must be provided as a hashref";
72             }
73             }
74              
75 12 100       68 if (exists $args{'listen'}) {
76 2         11 my ($hosts, $port) = _split_host_and_port($args{'listen'});
77              
78 2         14 $args{'servers'} = {};
79              
80 2         4 foreach my $host_ip (@{$hosts}) {
  2         6  
81 2         21 my $listen = "$host_ip:$port";
82 2 50       10 next if exists $args{'servers'}{$listen};
83              
84 2         26 $args{'servers'}{$listen} = Monitor::MetricsAPI::Server->new(
85             $host_ip, $port
86             );
87             }
88             }
89              
90 12         510 return \%args;
91             }
92              
93             sub _parse_metrics_hash {
94 23     23   51 my ($metrics, @groups) = @_;
95              
96 23         38 my @m;
97              
98 23         31 foreach my $k (keys %{$metrics}) {
  23         82  
99 29 100       81 if (ref($metrics->{$k}) eq 'HASH') {
100 13         81 push(@m, _parse_metrics_hash($metrics->{$k}, @groups, $k));
101             } else {
102 16         63 push(@m, [_make_metric_name(@groups, $k), $metrics->{$k}]);
103             }
104             }
105              
106 23         78 return @m;
107             }
108              
109             sub _make_metric_name {
110 16     16   40 my (@groups, $metric) = @_;
111              
112 16 100       41 return join('/', grep { defined $_ && $_ =~ m{\w+} } (@groups, $metric));
  49         338  
113             }
114              
115             sub _split_host_and_port {
116 3     3   8 my ($listen) = @_;
117              
118 3         14 my ($addr, $port) = split(':', $listen);
119              
120 3 50 33     47 die "address may not be omitted and must be a hostname, an IP, or an asterisk"
121             unless defined $addr && $addr =~ m{\S+};
122 3 50 33     51 die "port must be a number (or omitted entirely to use default)"
      33        
123             if defined $port && $port ne '' && $port !~ m{^\d+$};
124              
125 3 50 33     18 $port = 8200 unless defined $port && $port ne '';
126              
127 3         6 my $hosts = [];
128              
129 3 50       12 if ($addr eq '*') {
130 3         6 $hosts = ['0.0.0.0'];
131             } else {
132 0         0 my ($err, @res) = getaddrinfo($addr, "", {socktype => SOCK_RAW});
133 0 0       0 die "could not resolve $addr: $err" if $err;
134 0         0 foreach (@res) {
135 0         0 my $ipaddr;
136 0         0 ($err, $ipaddr) = getnameinfo($_->{'addr'}, NI_NUMERICHOST, NIx_NOSERV);
137 0 0       0 die "could not lookup $_->{'addr'}: $err" if $err;
138 0         0 push(@{$hosts}, $ipaddr);
  0         0  
139             }
140             }
141              
142 3         10 return ($hosts, $port);
143             }
144              
145             =head1 METHODS
146              
147             =head2 metric ($name)
148              
149             Returns the L<Monitor::MetricsAPI::Metric> object for the given name. Metric
150             names are collapsed to a slash-delimited string, which mirrors the path used
151             by the reporting HTTP server to display individual metrics. Thus, this:
152              
153             Monitor::MetricsAPI->new(
154             metrics => {
155             server => {
156             version => {
157             major => 'string',
158             minor => 'string',
159             }
160             }
161             }
162             );
163              
164             Creates two metrics:
165              
166             =over
167              
168             =item 1. server/version/major
169              
170             =item 2. server/version/minor
171              
172             =back
173              
174             The metric object returned by this method may then be modified, according to
175             its own methods documented in L<Monitor::MetricsAPI::Metric> and the
176             type-specific documentation, or its value may be accessed via the standard
177             value() metric method.
178              
179             Updating a metric:
180              
181             $collector->metric('users/total')->set($user_count);
182              
183             Retrieving the current value of a metric:
184              
185             $collector->metric('users/total')->value;
186              
187             =cut
188              
189             sub metric {
190 81     81 1 8225 my ($self, $name) = @_;
191              
192 81 50       261 unless (defined $name) {
193 0         0 warn "cannot retrieve metric value without a name";
194 0         0 return;
195             }
196              
197 81 50       3983 unless (exists $self->metrics->{$name}) {
198 0         0 warn "the metric $name does not exist";
199 0         0 return;
200             }
201              
202 81         3170 return $self->metrics->{$name};
203             }
204              
205             =head2 add_metrics (\%metrics)
206              
207             Accepts a hashref of hierarchical metric definitions (please see documentation
208             in L<Monitor::MetricsAPI::Tutorial> for a more complete description). Used to
209             bulk-add metrics to a collector.
210              
211             =cut
212              
213             sub add_metrics {
214 1     1 1 2 my ($self, $metrics) = @_;
215              
216 1 50 33     8 return unless defined $metrics && ref($metrics) eq 'HASH';
217              
218 1         4 foreach my $metric (_parse_metrics_hash($metrics)) {
219 2 50       6 if (ref($metric->[1]) eq 'CODE') {
220 0         0 $self->metrics->{$metric->[0]} = Monitor::MetricsAPI::MetricFactory->create(
221             name => $metric->[0],
222             type => 'callback',
223             cb => $metric->[1],
224             );
225             } else {
226 2         78 $self->metrics->{$metric->[0]} = Monitor::MetricsAPI::MetricFactory->create(
227             name => $metric->[0],
228             type => $metric->[1],
229             );
230             }
231             }
232              
233 1         4 return 1;
234             }
235              
236             =head2 add_metric ($name, $type, $callback)
237              
238             Allows for adding a new metric to the collector as your application is running,
239             instead of having to define everything at startup.
240              
241             If the metric already exists, this method will be a noop as long as all of the
242             metric options match (i.e. the existing metric is of the same type as what you
243             specified in add_metric()). If the metric already exists and you have specified
244             options which do not match the existing ones, a warning will be emitted and no
245             other actions will be taken.
246              
247             Both $name and $type are required. If $type is 'callback' then a subroutine
248             reference must be passed in for $callback. Refer to the documentation in
249             L<Monitor::MetricsAPI::Metric> for details on individual metric types.
250              
251             =cut
252              
253             sub add_metric {
254 1     1 1 4 my ($self, $name, $type, $callback) = @_;
255              
256 1 50 33     9 unless (defined $name && defined $type) {
257 0         0 warn "metric creation requires a name and type";
258 0         0 return;
259             }
260              
261 1 50 33     11 if ($type eq 'callback' && (!defined $callback || ref($callback) ne 'CODE')) {
      33        
262 0         0 warn "callback metrics must also provide a subroutine";
263 0         0 return;
264             }
265              
266 1 50       39 if (exists $self->metrics->{$name}) {
267 0 0       0 return if $self->metrics->{$name}->type eq $type;
268 0         0 warn "metric $name already exists, but is not of type $type";
269 0         0 return;
270             }
271              
272 1 50       12 my $metric = Monitor::MetricsAPI::MetricFactory->create(
273             type => $type,
274             name => $name,
275             ( $type eq 'callback' ? ( cb => $callback ) : ())
276             );
277              
278 1 50       6 unless (defined $metric) {
279 0         0 warn "could not create the metric $name";
280 0         0 return;
281             }
282              
283 1         35 $self->metrics->{$metric->name} = $metric;
284 1         2 return $metric;
285             }
286              
287             =head2 add_server ($listen)
288              
289             Adds a new HTTP server listener to the collector. The $listen argument must be
290             a string in the form of "<address>:<port>" where address may be an asterisk to
291             indicate all interfaces should be listened on, and where port (as well as the
292             leading colon) may be omitted if you wish to use the default port of 8200.
293              
294             Examples:
295              
296             $collector->add_server('*:8201');
297             $collector->add_server('127.0.0.1:8202');
298             $collector->add_server('192.168.1.1');
299              
300             You may add as many servers as you like. If you attempt to bind to the same
301             address and port combination more than once, a warning will be emitted and no
302             action will be taken.
303              
304             =cut
305              
306             sub add_server {
307 1     1 1 3 my ($self, $listen) = @_;
308              
309 1 50       5 return unless defined $listen;
310              
311 1         5 my ($hosts, $port) = _split_host_and_port($listen);
312              
313 1         2 foreach my $host_ip (@{$hosts}) {
  1         5  
314 1         3 my $listen = "$host_ip:$port";
315 1 50       39 if (exists $self->servers->{$listen}) {
316 0         0 warn "already listening on $listen";
317 0         0 next;
318             }
319              
320 1         10 $self->servers->{$listen} = Monitor::MetricsAPI::Server->new(
321             $host_ip, $port
322             );
323             }
324             }
325              
326             =head1 AUTHORS
327              
328             Jon Sime <jonsime@gmail.com>
329              
330             =head1 LICENSE AND COPYRIGHT
331              
332             This software is copyright (c) 2015 by OmniTI Computer Consulting, Inc.
333              
334             This module is free software; you can redistribute it and/or
335             modify it under the same terms as Perl itself. See L<perlartistic>.
336              
337             This program is distributed in the hope that it will be useful,
338             but WITHOUT ANY WARRANTY; without even the implied warranty of
339             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
340              
341             =cut
342              
343             __PACKAGE__->meta->make_immutable;
344             1;