File Coverage

blib/lib/Metrics/Any/Adapter/Statsd.pm
Criterion Covered Total %
statement 66 66 100.0
branch 13 20 65.0
condition 5 12 41.6
subroutine 14 14 100.0
pod 0 9 0.0
total 98 121 80.9


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-2026 -- leonerd@leonerd.org.uk
5              
6             package Metrics::Any::Adapter::Statsd 0.04;
7              
8 4     4   604643 use v5.14;
  4         14  
9 4     4   21 use warnings;
  4         8  
  4         164  
10              
11 4     4   26 use Carp;
  4         7  
  4         276  
12              
13             # We don't use Net::Statsd because it
14             # a) is hard to override sending for custom formats e.g. SignalFx or DogStatsd
15             # b) sends differently-named stats in different packets, losing atomicity of
16             # distribution updates
17 4     4   464 use IO::Socket::INET;
  4         17677  
  4         26  
18              
19             # TODO: Keep the same config for now
20             $Net::Statsd::HOST //= "127.0.0.1";
21             $Net::Statsd::PORT //= 8125;
22              
23             =head1 NAME
24              
25             C - a metrics reporting adapter for statsd
26              
27             =head1 SYNOPSIS
28              
29             =for highlighter language=perl
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 37 my $class = shift;
93 3         5 my ( %args ) = @_;
94              
95             return bless {
96             host => $args{host},
97             port => $args{port},
98 3         22 metrics => {},
99             gauge_initialised => {},
100             }, $class;
101             }
102              
103             sub mangle_name
104             {
105 17     17 0 19 my $self = shift;
106 17         26 my ( $name ) = @_;
107              
108 17 50       54 return join ".", @$name if ref $name eq "ARRAY";
109              
110             # Convert _-separated components into .
111 17         34 $name =~ s/_/./g;
112 17         28 return $name;
113             }
114              
115             sub socket
116             {
117 16     16 0 27 my $self = shift;
118              
119             return $self->{socket} //= IO::Socket::INET->new(
120             Proto => "udp",
121             PeerHost => $self->{host} // $Net::Statsd::HOST,
122 16   33     78 PeerPort => $self->{port} // $Net::Statsd::PORT,
      33        
      66        
123             );
124             }
125              
126             sub send
127             {
128 16     16 0 17 my $self = shift;
129 16         25 my ( $stats, $labelnames, $labelvalues ) = @_;
130              
131             $self->socket->send(
132             join "\n", map {
133 16         32 my $name = $_;
  18         796  
134 18         24 my $value = $stats->{$name};
135 18 100       42 map { sprintf "%s:%s", $name, $_ } ref $value eq "ARRAY" ? @$value : $value
  19         98  
136             } sort keys %$stats
137             );
138             }
139              
140             sub _make
141             {
142 17     17   20893 my $self = shift;
143 17         47 my ( $handle, %args ) = @_;
144              
145 17   33     55 my $name = $self->mangle_name( delete $args{name} // $handle );
146 17 100       463 $name =~ m/[\x00-\x1f:\|]/ and
147             croak "Metric name '$name' is not allowed by statsd";
148              
149             $self->{metrics}{$handle} = {
150             name => $name,
151             labels => $args{labels},
152 14         89 };
153             }
154              
155             *make_counter = \&_make;
156              
157             sub inc_counter_by
158             {
159 5     5 0 69 my $self = shift;
160 5         8 my ( $handle, $amount, @labelvalues ) = @_;
161              
162 5 50       13 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
163              
164 5         36 my $value = sprintf "%g|c", $amount;
165              
166 5         18 $self->send( { $meta->{name} => $value }, $meta->{labels}, \@labelvalues );
167             }
168              
169             *make_distribution = \&_make;
170              
171             sub report_distribution
172             {
173 2     2 0 58 my $self = shift;
174 2         17 my ( $handle, $amount, @labelvalues ) = @_;
175              
176             # A distribution acts like two counters; `sum` a `count`.
177              
178 2 50       6 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
179              
180 2         15 my $value = sprintf "%g|c", $amount;
181              
182             $self->send( {
183             "$meta->{name}.sum" => $value,
184             "$meta->{name}.count" => "1|c",
185 2         20 }, $meta->{labels}, \@labelvalues );
186             }
187              
188             *inc_distribution_by = \&report_distribution;
189              
190             *make_gauge = \&_make;
191              
192             sub inc_gauge_by
193             {
194 1     1 0 324 my $self = shift;
195 1         2 my ( $handle, $amount, @labelvalues ) = @_;
196              
197 1 50       3 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
198 1         2 my $name = $meta->{name};
199              
200 1         1 my @value;
201 1 50       3 push @value, "0|g" unless $self->{gauge_initialised}{$name};
202 1         5 push @value, sprintf( "%+g|g", $amount );
203              
204 1         5 $self->send( { $name => \@value }, $meta->{labels}, \@labelvalues );
205 1         30 $self->{gauge_initialised}{$name} = 1;
206             }
207              
208             sub set_gauge_to
209             {
210 4     4 0 1402 my $self = shift;
211 4         10 my ( $handle, $amount, @labelvalues ) = @_;
212              
213 4 50       17 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
214 4         9 my $name = $meta->{name};
215              
216 4         55 my @value;
217             # wire format interprets a leading - as a decrement request; so negative
218             # absolute values must first set zero
219 4 100       9 push @value, "0|g" if $amount < 0;
220 4         26 push @value, sprintf( "%g|g", $amount );
221              
222 4         19 $self->send( { $name => \@value }, $meta->{labels}, \@labelvalues );
223 4         157 $self->{gauge_initialised}{$name} = 1;
224             }
225              
226             *make_timer = \&_make;
227              
228             sub report_timer
229             {
230 3     3 0 113 my $self = shift;
231 3         6 my ( $handle, $duration, @labelvalues ) = @_;
232              
233 3 50       10 my $meta = $self->{metrics}{$handle} or croak "No metric '$handle'";
234              
235 3         14 my $value = sprintf "%d|ms", $duration * 1000; # msec
236              
237 3         12 $self->send( { $meta->{name} => $value }, $meta->{labels}, \@labelvalues );
238             }
239              
240             *inc_timer_by = \&report_timer;
241              
242             =head1 TODO
243              
244             =over 4
245              
246             =item *
247              
248             Support non-one samplerates; emit only one-in-N packets with the C<@rate>
249             notation in the packet.
250              
251             =item *
252              
253             Optionally support one dimension of labelling by appending the conventional
254             C notation to it.
255              
256             =back
257              
258             =cut
259              
260             =head1 AUTHOR
261              
262             Paul Evans
263              
264             =cut
265              
266             0x55AA;