File Coverage

blib/lib/Etsy/StatsD.pm
Criterion Covered Total %
statement 66 68 97.0
branch 23 28 82.1
condition 5 6 83.3
subroutine 10 11 90.9
pod 5 5 100.0
total 109 118 92.3


line stmt bran cond sub pod time code
1             package Etsy::StatsD;
2 2     2   38645 use strict;
  2         4  
  2         54  
3 2     2   9 use warnings;
  2         2  
  2         53  
4 2     2   1053 use IO::Socket;
  2         38326  
  2         7  
5 2     2   647 use Carp;
  2         3  
  2         1232  
6              
7             our $VERSION = 1.002001;
8              
9             # The CPAN verion at https://github.com/sanbeg/Etsy-Statsd should be kept in
10             # sync with the version distributed with StatsD, at
11             # https://github.com/etsy/statsd (in the exmaples directory), so you can get
12             # it from either location.
13              
14             =head1 NAME
15              
16             Etsy::StatsD - Object-Oriented Client for Etsy's StatsD Server
17              
18             =head1 SYNOPSIS
19              
20             use Etsy::StatsD;
21              
22             # Increment a counter
23             my $statsd = Etsy::StatsD->new();
24             $statsd->increment( 'app.method.success' );
25              
26              
27             # Time something
28             use Time::HiRes;
29              
30             my $start_time = time;
31             $app->do_stuff;
32             my $done_time = time;
33              
34             # Timers are expected in milliseconds
35             $statsd->timing( 'app.method', ($done_time - $start_time) * 1000 );
36              
37             # Send to two StatsD Endpoints simultaneously
38             my $repl_statsd = Etsy::StatsD->new(["statsd1","statsd2"]);
39              
40             # On two different ports:
41             my $repl_statsd = Etsy::StatsD->new(["statsd1","statsd1:8126"]);
42              
43             # Use TCP to a collector (you must specify a port)
44             my $important_stats = Etsy::StatsD->new(["bizstats1:8125:tcp"]);
45              
46              
47             =head1 DESCRIPTION
48              
49             =cut
50              
51             =over
52              
53             =item new (HOST, PORT, SAMPLE_RATE)
54              
55             Create a new instance.
56              
57             =over
58              
59             =item HOST
60              
61              
62             If the argument is a string, it must be a hostname or IP only. The default is
63             'localhost'. The argument may also be an array reference of strings in the
64             form of "", ":", or "::". If the port is
65             not specified, the default port specified by the PORT argument will be used.
66             If the protocol is not specified, or is not "tcp" or "udp", "udp" will be set.
67             The only way to change the protocol, is to specify the host, port and protocol.
68              
69             =item PORT
70              
71             Default is 8125. Will be used as the default port for any HOST argument not explicitly defining port.
72              
73             =item SAMPLE_RATE
74              
75             Default is undefined, or no sampling performed. Specify a rate as a decimal between 0 and 1 to enable
76             sampling. e.g. 0.5 for 50%.
77              
78             =back
79              
80             =cut
81              
82             sub new {
83 5     5 1 1334 my ( $class, $host, $port, $sample_rate ) = @_;
84 5 100       13 $host = 'localhost' unless defined $host;
85 5 100       10 $port = 8125 unless defined $port;
86              
87             # Handle multiple connections and
88             # allow different ports to be specified
89             # in the form of "::"
90 5         8 my %protos = map { $_ => 1 } qw(tcp udp);
  10         19  
91 5         10 my @connections = ();
92              
93 5 100       12 if( ref $host eq 'ARRAY' ) {
94 2         2 foreach my $addr ( @{ $host } ) {
  2         4  
95 3         7 my ($addr_host,$addr_port,$addr_proto) = split /:/, $addr;
96 3   66     10 $addr_port ||= $port;
97             # Validate the protocol
98 3 100       4 if( defined $addr_proto ) {
99 2         3 $addr_proto = lc $addr_proto; # Normalize to lowercase
100             # Check validity
101 2 50       6 if( !exists $protos{$addr_proto} ) {
102 2         261 croak sprintf("Invalid protocol '%s', valid: %s", $addr_proto, join(', ', sort keys %protos));
103             }
104             }
105             else {
106 1         2 $addr_proto = 'udp';
107             }
108 1         3 push @connections, [ $addr_host, $addr_port, $addr_proto ];
109             }
110             }
111             else {
112 3         6 push @connections, [ $host, $port, 'udp' ];
113             }
114              
115 3         5 my @sockets = ();
116 3         5 foreach my $conn ( @connections ) {
117 3 50       25 my $sock = new IO::Socket::INET(
118             PeerAddr => $conn->[0],
119             PeerPort => $conn->[1],
120             Proto => $conn->[2],
121             ) or carp "Failed to initialize socket: $!";
122              
123 3 50       1560 push @sockets, $sock if defined $sock;
124             }
125             # Check that we have at least 1 socket to send to
126 3 50       9 croak "Failed to initialize any sockets." unless @sockets;
127              
128 3         37 bless { sockets => \@sockets, sample_rate => $sample_rate }, $class;
129             }
130              
131             =item timing(STAT, TIME, SAMPLE_RATE)
132              
133             Log timing information
134              
135             =cut
136              
137             sub timing {
138 4     4 1 318 my ( $self, $stat, $time, $sample_rate ) = @_;
139 4         18 $self->send( { $stat => "$time|ms" }, $sample_rate );
140             }
141              
142             =item increment(STATS, SAMPLE_RATE)
143              
144             Increment one of more stats counters.
145              
146             =cut
147              
148             sub increment {
149 1     1 1 470 my ( $self, $stats, $sample_rate ) = @_;
150 1         5 $self->update( $stats, 1, $sample_rate );
151             }
152              
153             =item decrement(STATS, SAMPLE_RATE)
154              
155             Decrement one of more stats counters.
156              
157             =cut
158              
159             sub decrement {
160 1     1 1 419 my ( $self, $stats, $sample_rate ) = @_;
161 1         4 $self->update( $stats, -1, $sample_rate );
162             }
163              
164             =item update(STATS, DELTA, SAMPLE_RATE)
165              
166             Update one of more stats counters by arbitrary amounts.
167              
168             =cut
169              
170             sub update {
171 5     5 1 1399 my ( $self, $stats, $delta, $sample_rate ) = @_;
172 5 100       12 $delta = 1 unless defined $delta;
173 5         3 my %data;
174 5 100       12 if ( ref($stats) eq 'ARRAY' ) {
175 1         3 %data = map { $_ => "$delta|c" } @$stats;
  2         6  
176             }
177             else {
178 4         11 %data = ( $stats => "$delta|c" );
179             }
180 5         10 $self->send( \%data, $sample_rate );
181             }
182              
183             =item send(DATA, SAMPLE_RATE)
184              
185             Sending logging data; implicitly called by most of the other methods.
186              
187             =back
188              
189             =cut
190              
191             sub send {
192 3     3   2 my ( $self, $data, $sample_rate ) = @_;
193 3 100       8 $sample_rate = $self->{sample_rate} unless defined $sample_rate;
194              
195 3         2 my $sampled_data;
196 3 100 100     12 if ( defined($sample_rate) and $sample_rate < 1 ) {
197 1         5 while ( my ( $stat, $value ) = each %$data ) {
198 1 50       36 $sampled_data->{$stat} = "$value|\@$sample_rate" if rand() <= $sample_rate;
199             }
200             }
201             else {
202 2         6 $sampled_data = $data;
203             }
204              
205 3 100       15 return '0 but true' unless keys %$sampled_data;
206              
207             #failures in any of this can be silently ignored
208 2         2 my $count = 0;
209 2         1 foreach my $socket ( @{ $self->{sockets} } ) {
  2         5  
210             # calling keys() resets the each() iterator
211 2         3 keys %$sampled_data;
212 2         6 while ( my ( $stat,$value ) = each %$sampled_data ) {
213 2         5 _send_to_sock($socket, "$stat:$value\n", 0);
214 2         7 ++$count;
215             }
216             }
217 2         8 return $count;
218             }
219              
220             sub _send_to_sock( $$ ) {
221 0     0     my ($sock,$msg) = @_;
222 0           CORE::send( $sock, $msg, 0 );
223             }
224              
225             =head1 SEE ALSO
226              
227             L
228              
229             =head1 AUTHOR
230              
231             Steve Sanbeg L
232              
233             =head1 LICENSE
234              
235             Same as perl.
236              
237             =cut
238              
239             1;