File Coverage

blib/lib/Metrics/Any/Adapter/Statsd.pm
Criterion Covered Total %
statement 66 66 100.0
branch 11 18 61.1
condition 3 6 50.0
subroutine 14 14 100.0
pod 0 9 0.0
total 94 113 83.1


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   1891 use strict;
  4         9  
  4         115  
9 4     4   21 use warnings;
  4         8  
  4         149  
10              
11             our $VERSION = '0.01';
12              
13 4     4   21 use Carp;
  4         16  
  4         244  
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   553 use IO::Socket::INET;
  4         20038  
  4         29  
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 location of the statsd server is set by two package variables, defaulting
39             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.
45              
46             =head1 METRIC HANDLING
47              
48             Unlabelled counter, gauge and timing metrics are handled natively as you would
49             expect for statsd; with multipart names being joined by periods (C<.>).
50              
51             Distribution metrics are emitted as two sub-named metrics by appending
52             C and C. The C metric in incremented by one for each
53             observation and the C by the observed amount.
54              
55             Labels are not handled by this adapter and are thrown away. This will result
56             in a single value being reported that accumulates the sum total across all of
57             the label values. In the case of labelled gauges using the C
58             method this will not be a useful value.
59              
60             For better handling of labelled metrics for certain services which have
61             extended the basic statsd format to handle them, see:
62              
63             =over 2
64              
65             =item *
66              
67             L - a metrics reporting adapter for DogStatsd
68              
69             =item *
70              
71             L - a metrics reporting adapter for SignalFx
72              
73             =back
74              
75             =cut
76              
77             sub new
78             {
79 3     3 0 36 my $class = shift;
80 3         9 my ( %args ) = @_;
81              
82 3         24 return bless {
83             metrics => {},
84             gauge_initialised => {},
85             }, $class;
86             }
87              
88             sub mangle_name
89             {
90 12     12 0 34 my $self = shift;
91 12         20 my ( $name ) = @_;
92              
93 12 50       35 return join ".", @$name if ref $name eq "ARRAY";
94              
95             # Convert _-separated components into .
96 12         30 $name =~ s/_/./g;
97 12         25 return $name;
98             }
99              
100             sub socket
101             {
102 14     14 0 25 my $self = shift;
103              
104 14   66     77 return $self->{socket} //= IO::Socket::INET->new(
105             Proto => "udp",
106             PeerHost => $Net::Statsd::HOST,
107             PeerPort => $Net::Statsd::PORT,
108             );
109             }
110              
111             sub send
112             {
113 14     14 0 35 my $self = shift;
114 14         26 my ( $stats, $labelnames, $labelvalues ) = @_;
115              
116             $self->socket->send(
117             join "\n", map {
118 14         50 my $name = $_;
  16         1037  
119 16         30 my $value = $stats->{$name};
120 16 100       44 map { sprintf "%s:%s", $name, $_ } ref $value eq "ARRAY" ? @$value : $value
  17         121  
121             } sort keys %$stats
122             );
123             }
124              
125             sub _make
126             {
127 12     12   7062 my $self = shift;
128 12         35 my ( $handle, %args ) = @_;
129              
130 12   33     53 my $name = $self->mangle_name( delete $args{name} // $handle );
131              
132             $self->{metrics}{$handle} = {
133             name => $name,
134             labels => $args{labels},
135 12         67 };
136             }
137              
138             *make_counter = \&_make;
139              
140             sub inc_counter_by
141             {
142 3     3 0 37 my $self = shift;
143 3         7 my ( $handle, $amount, @labelvalues ) = @_;
144              
145 3 50       11 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
146              
147 3         40 my $value = sprintf "%g|c", $amount;
148              
149 3         18 $self->send( { $meta->{name} => $value }, $meta->{labels}, \@labelvalues );
150             }
151              
152             *make_distribution = \&_make;
153              
154             sub inc_distribution_by
155             {
156 2     2 0 38 my $self = shift;
157 2         5 my ( $handle, $amount, @labelvalues ) = @_;
158              
159             # A distribution acts like two counters; `sum` a `count`.
160              
161 2 50       11 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
162              
163 2         17 my $value = sprintf "%g|c", $amount;
164              
165             $self->send( {
166             "$meta->{name}.sum" => $value,
167             "$meta->{name}.count" => "1|c",
168 2         14 }, $meta->{labels}, \@labelvalues );
169             }
170              
171             *make_gauge = \&_make;
172              
173             sub inc_gauge_by
174             {
175 1     1 0 601 my $self = shift;
176 1         2 my ( $handle, $amount, @labelvalues ) = @_;
177              
178 1 50       4 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
179 1         3 my $name = $meta->{name};
180              
181 1         1 my @value;
182 1 50       3 push @value, "0|g" unless $self->{gauge_initialised}{$name};
183 1         9 push @value, sprintf( "%+g|g", $amount );
184              
185 1         5 $self->send( { $name => \@value }, $meta->{labels}, \@labelvalues );
186 1         58 $self->{gauge_initialised}{$name} = 1;
187             }
188              
189             sub set_gauge_to
190             {
191 4     4 0 685 my $self = shift;
192 4         9 my ( $handle, $amount, @labelvalues ) = @_;
193              
194 4 50       15 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
195 4         8 my $name = $meta->{name};
196              
197 4         8 my @value;
198             # wire format interprets a leading - as a decrement request; so negative
199             # absolute values must first set zero
200 4 100       12 push @value, "0|g" if $amount < 0;
201 4         34 push @value, sprintf( "%g|g", $amount );
202              
203 4         20 $self->send( { $name => \@value }, $meta->{labels}, \@labelvalues );
204 4         236 $self->{gauge_initialised}{$name} = 1;
205             }
206              
207             *make_timer = \&_make;
208              
209             sub inc_timer_by
210             {
211 3     3 0 58 my $self = shift;
212 3         6 my ( $handle, $duration, @labelvalues ) = @_;
213              
214 3 50       12 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
215              
216 3         19 my $value = sprintf "%d|ms", $duration * 1000; # msec
217              
218 3         14 $self->send( { $meta->{name} => $value }, $meta->{labels}, \@labelvalues );
219             }
220              
221             =head1 TODO
222              
223             =over 4
224              
225             =item *
226              
227             Support non-one samplerates; emit only one-in-N packets with the C<@rate>
228             notation in the packet.
229              
230             =item *
231              
232             Optionally support one dimension of labelling by appending the conventional
233             C notation to it.
234              
235             =back
236              
237             =cut
238              
239             =head1 AUTHOR
240              
241             Paul Evans
242              
243             =cut
244              
245             0x55AA;