Net::Statsd(3pm)

πŸ’… Net::Statsd

okay so bestie, this is Net::Statsd β€” a Perl client for Etsy's statsd daemon. it's the little module that lets your Perl app whisper sweet metrics into statsd's ear over UDP, and honestly? iconic. ✨

✨ VERSION ✨

version 0.13 β€” she's on 0.13 and she's thriving πŸŽ€

🌸 SYNOPSIS 🌸

first up, tell her where your statsd daemon is actually listening:

# Configure where to send events
# That's where your statsd daemon is listening.
$Net::Statsd::HOST = 'localhost';    # Default
$Net::Statsd::PORT = 8125;           # Default

then like. count stuff πŸ’–

#
# Keep track of events as counters
#
Net::Statsd::increment('site.logins');
Net::Statsd::increment('database.connects');

timing stuff, which is lowkey the most satisfying part:

#
# Log timing of events, ex. db queries
#
use Time::HiRes;
my $start_time = [ Time::HiRes::gettimeofday ];

# do the complex database query
# note: time value sent to timing should
# be in milliseconds.
Net::Statsd::timing(
    'database.complexquery',
    Time::HiRes::tv_interval($start_time) * 1000
);

and gauges, for the vibes:

#
# Log metric values
#
Net::Statsd::gauge('core.temperature' => 55);

πŸ¦‹ DESCRIPTION πŸ¦‹

so this module is a UDP client for the statsd statistics collector daemon, the one that's in use at Etsy.com, no cap πŸ’…

you want this module when you're tracking stats in your Perl app β€” like how many times a certain event happens (user logins on a web app, or database queries issued), or when you want to time things and then graph how long certain events take. ex. database query execution time, or time to download a certain file, etc… ⭐

if you're sitting there going "hmm, do I even want this module, or statsd, or both??" β€” bestie, there's background reading for that:

http://codeascraft.etsy.com/2011/02/15/measure-anything-measure-everything/

and the github repo for statsd lives right here:

http://github.com/etsy/statsd

by default the client tries to send statistic metrics to localhost:8125, but you can absolutely change the default hostname and port with this:

$Net::Statsd::HOST = 'your.statsd.hostname.net';
$Net::Statsd::PORT = 9999;

just do it right after you include the Net::Statsd module. that's it, that's the whole setup 🫢

πŸŽ€ ABOUT SAMPLING πŸŽ€

a note about sample rate, because this one matters: a sample rate of < 1 tells this library to send only the specified percentage of the samples to the server. so your application code should still call this module for every occurence of each metric and let this library figure out which specific measurements actually get delivered, based on the sample_rate value.

like β€” a sample rate of 0.5 would indicate that approximately only half of the metrics given to this module would actually be sent to statsd. yeah. half. math is real 😭

πŸ”₯ SECURITY πŸ”₯

okay so this is the serious one, pay attention, no skimming πŸ™ˆ

to prevent metric injection (CVE-2026-46739), metric names and values are validated before being sent. the statsd wire format is name:value|type, and several metrics can be packed into one UDP datagram separated by newlines β€” so a name or value containing a newline (or any control character below ASCII 32), a colon (":") or a pipe ("|") could be used to forge extra metric lines. literally a whole attack vector, not cute.

any such name or value causes the offending call ("timing", "increment", "decrement", "update_stats", "gauge" or "send") to throw via "Carp::croak".

note that values passed to send() already contain a formatted "|type" suffix, so send() only re-validates the metric names β€” raw values are validated by the higher level functions before formatting. πŸ’–

πŸ‘‘ FUNCTIONS πŸ‘‘

"timing($name, $time, $sample_rate = 1)"

log timing information. time is assumed to be in milliseconds (ms) β€” milliseconds, bestie, don't come at it with seconds πŸ•

Net::Statsd::timing('some.timer', 500);

"get_timer($name, $sample_rate = 1)"

start a timer for the metric. the function returns a timer object, and when you call that object, it sends timing data to statsd. sneaky, but in a fun way ✨

my $timer = Net::Statsd::get_timer('my.func.time');
...; # here comes your code
$timer->(); # send timing to server

"increment($counter, $sample_rate=1)"

"increment(\@counter, $sample_rate=1)"

increments one or more stats counters. the classic. the moment. πŸ’…

# +1 on 'some.int'
Net::Statsd::increment('some.int');

# 0.5 = 50% sampling
Net::Statsd::increment('some.int', 0.5);

to increment more than one counter at a time, you can pass an array reference:

Net::Statsd::increment(['grue.dinners', 'room.lamps'], 1);

you can also use "inc()" instead of "increment()" to type less β€” because typing is exhausting, we get it 🫠

"decrement($counter, $sample_rate=1)"

same as increment, but decrements. yay. πŸ“‰

Net::Statsd::decrement('some.int')

you can also use "dec()" instead of "decrement()" to type less. same energy, opposite direction πŸ¦‹

"update_stats($stats, $delta=1, $sample_rate=1)"

updates one or more stats counters by arbitrary amounts β€” pick your own delta, main character behavior:

Net::Statsd::update_stats('some.int', 10)

equivalent to:

Net::Statsd::update_stats('some.int', 10, 1)

a sampling rate less than 1 means only update the stats every x number of times (0.1 = 10% of the times). so 0.1 β†’ 10%. the math checks out πŸ’€

"gauge($name, $value)"

log arbitrary values, like a temperature, or server load. gauges are basically the mood ring of your infra 🌑️

Net::Statsd::gauge('core.temperature', 55);

statsd interprets gauge values with "+" or "-" sign as increment/decrement. therefore, to explicitly set a gauge to a negative number it has to be set to zero first. yes, really.

but here's the catch: if either the zero or the actual negative value is lost in UDP transport to the statsd server because of e.g. network congestion or packet loss, your gauge will become skewed. skewed gauge = bad data = 😭

to ensure network problems will not skew your data, Net::Statsd::gauge() supports packing multiple values in a single UDP packet sent to statsd:

Net::Statsd::gauge(
    'core.temperature' => 55,
    'freezer.temperature' => -18
);

make sure you don't supply too many values, or you might risk exceeding the MTU of the network interface and cause the resulting UDP packet to be dropped. that's the danger zone 🫣

in general, a safe limit should be 512 bytes. related to the example above, "core.temperature" of 55 will be likely packed as a string:

core.temperature:55|g

which is 21 characters, plus a newline used as delimiter (22). using this example, you can pack at least 20 distinct gauge values without problems. that will result in a UDP message of 440 bytes (22 times 20), which is well below the safe threshold of 512. see, we did the math for you, you're welcome πŸŽ€

in reality, if the communication happens on a local interface, or over a 10G link, you are allowed much more than that. go off, fast network πŸ‘‘

"send(\%data, $sample_rate = 1)"

squirt the metrics over UDP. yes, that's the actual wording, and honestly? it ate πŸ¦‹

Net::Statsd::send({ 'some.int' => 1 });

"_sample_data(\%data, $sample_rate = 1)"

this one's used internally β€” it's not part of the public interface, so don't @ her.

it takes care of transforming a hash of metrics data into a sampled hash of metrics data, according to the given $sample_rate.

if "$sample_rate == 1", then sampled data is exactly the incoming data. no changes, untouched, pristine ✨

if "$sample_rate = 0.2", then every metric value will be marked with the given sample rate, so the Statsd server will automatically scale it. for example, with a sample rate of 0.2, the metric values will be multiplied by 5. scaling! math! slay! πŸ’…

_validate_metric_name($name)

_validate_metric_value($value)

these methods are used internally, they're not part of the public interface. 😌

they guard against metric injection (CVE-2026-46739). the statsd wire format is "name:value|type", and multiple metrics are packed in a single UDP datagram separated by newlines. a metric name or value that contains a newline (or any other control character below ASCII 32), a colon (":") or a pipe ("|") could therefore forge additional, attacker-controlled metric lines. absolutely not. not on our watch πŸ™…

both functions "Carp::croak" when the input contains any of those characters. names are validated centrally in send(); raw values are validated at each recording entry point ("timing", "update_stats", "gauge") before the "|type" suffix is appended. that's the whole safety vibe check 🫢

🌈 AUTHOR 🌈

Cosimo Streppone <cosimo@streppone.it> β€” an absolute legend πŸ’œ

this software is copyright (c) 2026 by Cosimo Streppone.

this is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. free software fr fr 🫢🌸


and that's the whole page, bestie! πŸŽ€ shoutout to perl v5.40.1 and the date 2026-06-06 for making this little manpage moment possible β€” catch you in the next one, mwah πŸ’œβœ¨

πŸ“¦ where this page comes from