File Coverage

blib/lib/Metrics/Any/Adapter/Statsd.pm
Criterion Covered Total %
statement 66 66 100.0
branch 11 18 61.1
condition 5 12 41.6
subroutine 14 14 100.0
pod 0 9 0.0
total 96 119 80.6


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2020 -- leonerd@leonerd.org.uk
5              
6             package Metrics::Any::Adapter::Statsd;
7              
8 4     4   1634 use strict;
  4         7  
  4         98  
9 4     4   17 use warnings;
  4         6  
  4         136  
10              
11             our $VERSION = '0.02';
12              
13 4     4   17 use Carp;
  4         7  
  4         240  
14              
15             # We don't use Net::Statsd because it
16             # a) is hard to override sending for custom formats e.g. SignalFx or DogStatsd
17             # b) sends differently-named stats in different packets, losing atomicity of
18             # distribution updates
19 4     4   484 use IO::Socket::INET;
  4         17145  
  4         41  
20              
21             # TODO: Keep the same config for now
22             $Net::Statsd::HOST //= "127.0.0.1";
23             $Net::Statsd::PORT //= 8125;
24              
25             =head1 NAME
26              
27             C - a metrics reporting adapter for statsd
28              
29             =head1 SYNOPSIS
30              
31             use Metrics::Any::Adapter 'Statsd';
32              
33             =head1 DESCRIPTION
34              
35             This L adapter type reports metrics to statsd via the local UDP
36             socket. Each metric value reported will result in a new UDP packet being sent.
37              
38             The default location of the statsd server is set by two package variables,
39             defaulting to
40              
41             $Net::Statsd::HOST = "127.0.0.1";
42             $Net::Statsd::PORT = 8125
43              
44             The configuration can be changed by setting new values or by passing arguments
45             to the import line:
46              
47             use Metrics::Any::Adapter 'Statsd', port => 8200;
48              
49             =head1 METRIC HANDLING
50              
51             Unlabelled counter, gauge and timing metrics are handled natively as you would
52             expect for statsd; with multipart names being joined by periods (C<.>).
53              
54             Distribution metrics are emitted as two sub-named metrics by appending
55             C and C. The C metric in incremented by one for each
56             observation and the C by the observed amount.
57              
58             Labels are not handled by this adapter and are thrown away. This will result
59             in a single value being reported that accumulates the sum total across all of
60             the label values. In the case of labelled gauges using the C
61             method this will not be a useful value.
62              
63             For better handling of labelled metrics for certain services which have
64             extended the basic statsd format to handle them, see:
65              
66             =over 2
67              
68             =item *
69              
70             L - a metrics reporting adapter for DogStatsd
71              
72             =item *
73              
74             L - a metrics reporting adapter for SignalFx
75              
76             =back
77              
78             =head1 ARGUMENTS
79              
80             The following additional arguments are recognised
81              
82             =head2 host
83              
84             =head2 port
85              
86             Provides specific values for the statsd server location.
87              
88             =cut
89              
90             sub new
91             {
92 3     3 0 43 my $class = shift;
93 3         9 my ( %args ) = @_;
94              
95             return bless {
96             host => $args{host},
97             port => $args{port},
98 3         25 metrics => {},
99             gauge_initialised => {},
100             }, $class;
101             }
102              
103             sub mangle_name
104             {
105 12     12 0 20 my $self = shift;
106 12         17 my ( $name ) = @_;
107              
108 12 50       31 return join ".", @$name if ref $name eq "ARRAY";
109              
110             # Convert _-separated components into .
111 12         26 $name =~ s/_/./g;
112 12         16 return $name;
113             }
114              
115             sub socket
116             {
117 14     14 0 29 my $self = shift;
118              
119             return $self->{socket} //= IO::Socket::INET->new(
120             Proto => "udp",
121             PeerHost => $self->{host} // $Net::Statsd::HOST,
122 14   33     98 PeerPort => $self->{port} // $Net::Statsd::PORT,
      33        
      66        
123             );
124             }
125              
126             sub send
127             {
128 14     14 0 22 my $self = shift;
129 14         22 my ( $stats, $labelnames, $labelvalues ) = @_;
130              
131             $self->socket->send(
132             join "\n", map {
133 14         33 my $name = $_;
  16         948  
134 16         26 my $value = $stats->{$name};
135 16 100       38 map { sprintf "%s:%s", $name, $_ } ref $value eq "ARRAY" ? @$value : $value
  17         109  
136             } sort keys %$stats
137             );
138             }
139              
140             sub _make
141             {
142 12     12   6217 my $self = shift;
143 12         52 my ( $handle, %args ) = @_;
144              
145 12   33     45 my $name = $self->mangle_name( delete $args{name} // $handle );
146              
147             $self->{metrics}{$handle} = {
148             name => $name,
149             labels => $args{labels},
150 12         74 };
151             }
152              
153             *make_counter = \&_make;
154              
155             sub inc_counter_by
156             {
157 3     3 0 34 my $self = shift;
158 3         8 my ( $handle, $amount, @labelvalues ) = @_;
159              
160 3 50       10 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
161              
162 3         44 my $value = sprintf "%g|c", $amount;
163              
164 3         13 $self->send( { $meta->{name} => $value }, $meta->{labels}, \@labelvalues );
165             }
166              
167             *make_distribution = \&_make;
168              
169             sub report_distribution
170             {
171 2     2 0 76 my $self = shift;
172 2         5 my ( $handle, $amount, @labelvalues ) = @_;
173              
174             # A distribution acts like two counters; `sum` a `count`.
175              
176 2 50       9 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
177              
178 2         17 my $value = sprintf "%g|c", $amount;
179              
180             $self->send( {
181             "$meta->{name}.sum" => $value,
182             "$meta->{name}.count" => "1|c",
183 2         12 }, $meta->{labels}, \@labelvalues );
184             }
185              
186             *inc_distribution_by = \&report_distribution;
187              
188             *make_gauge = \&_make;
189              
190             sub inc_gauge_by
191             {
192 1     1 0 490 my $self = shift;
193 1         2 my ( $handle, $amount, @labelvalues ) = @_;
194              
195 1 50       5 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
196 1         1 my $name = $meta->{name};
197              
198 1         1 my @value;
199 1 50       3 push @value, "0|g" unless $self->{gauge_initialised}{$name};
200 1         7 push @value, sprintf( "%+g|g", $amount );
201              
202 1         4 $self->send( { $name => \@value }, $meta->{labels}, \@labelvalues );
203 1         47 $self->{gauge_initialised}{$name} = 1;
204             }
205              
206             sub set_gauge_to
207             {
208 4     4 0 634 my $self = shift;
209 4         9 my ( $handle, $amount, @labelvalues ) = @_;
210              
211 4 50       13 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
212 4         8 my $name = $meta->{name};
213              
214 4         6 my @value;
215             # wire format interprets a leading - as a decrement request; so negative
216             # absolute values must first set zero
217 4 100       11 push @value, "0|g" if $amount < 0;
218 4         29 push @value, sprintf( "%g|g", $amount );
219              
220 4         18 $self->send( { $name => \@value }, $meta->{labels}, \@labelvalues );
221 4         248 $self->{gauge_initialised}{$name} = 1;
222             }
223              
224             *make_timer = \&_make;
225              
226             sub report_timer
227             {
228 3     3 0 97 my $self = shift;
229 3         8 my ( $handle, $duration, @labelvalues ) = @_;
230              
231 3 50       13 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
232              
233 3         17 my $value = sprintf "%d|ms", $duration * 1000; # msec
234              
235 3         12 $self->send( { $meta->{name} => $value }, $meta->{labels}, \@labelvalues );
236             }
237              
238             *inc_timer_by = \&report_timer;
239              
240             =head1 TODO
241              
242             =over 4
243              
244             =item *
245              
246             Support non-one samplerates; emit only one-in-N packets with the C<@rate>
247             notation in the packet.
248              
249             =item *
250              
251             Optionally support one dimension of labelling by appending the conventional
252             C notation to it.
253              
254             =back
255              
256             =cut
257              
258             =head1 AUTHOR
259              
260             Paul Evans
261              
262             =cut
263              
264             0x55AA;