liuggio/statsd-php-client
A lightweight PHP client for StatsD and Etsy StatsD metrics. Send counters, gauges, timers, and sets with sampling and batching support, plus configurable backends, so you can instrument your application and feed data into Graphite dashboards.
liuggio/statsd-php-client is a thin, dependency-light PHP client for the StatsD line protocol over UDP. It is an old package (last tagged release July 2015) but its surface area is small enough that you can read the whole thing in an hour. Treat it as a "protocol formatter + socket wrapper," not as a full observability SDK.
Install and verify
composer require liuggio/statsd-php-client
Then run a smoke test against a local listener (nc -ul 8125 or the official statsd docker image) to confirm packets leave the process.
Where to look first (in order)
src/Liuggio/StatsdClient/StatsdClient.php – the orchestrator; send() accepts a single StatsdDataInterface or an array.src/Liuggio/StatsdClient/Factory/StatsdDataFactory.php – the metric vocabulary (increment, decrement, timing, gauge, …). This is where you'll spend most of your time.src/Liuggio/StatsdClient/Sender/SocketSender.php – the UDP transport. Read open()/write()/close() so you understand where failures are swallowed.src/Liuggio/StatsdClient/Entity/StatsdData.php – __toString() is the actual wire format (name:value|type, optionally |@sampleRate).Minimal wiring
use Liuggio\StatsdClient\Factory\StatsdDataFactory;
use Liuggio\StatsdClient\Sender\SocketSender;
use Liuggio\StatsdClient\StatsdClient;
$sender = new SocketSender('127.0.0.1', 8125);
$client = new StatsdClient($sender);
$factory = new StatsdDataFactory();
$client->send($factory->increment('app.requests'));
$client->send($factory->timing('app.render_ms', 128));
$client->send($factory->gauge('app.queue_depth', 42));
First real use case — instrument one endpoint. Count the request, time the handler, emit a gauge for a queue depth, and don't refactor the rest of the app yet. Confirm the metrics arrive in your backend before standardizing names.
Laravel wiring (one provider, one config file):
// config/statsd.php
return [
'host' => env('STATSD_HOST', '127.0.0.1'),
'port' => (int) env('STATSD_PORT', 8125),
'prefix' => env('STATSD_PREFIX', 'app'),
];
// AppServiceProvider::register()
$this->app->singleton(StatsdClient::class, fn () => new StatsdClient(
new SocketSender(config('statsd.host'), config('statsd.port'))
));
$this->app->singleton(StatsdDataFactory::class, fn () => new StatsdDataFactory());
1. Factory + client is the unit of work. The factory produces immutable StatsdData value objects; the client writes them out. Keep the factory available anywhere (container singleton) and only hand the client to code that actually transmits.
2. Batch aggressively. StatsdClient::send() accepts an array, so accumulate in a request-scoped buffer and flush once. This is the biggest throughput lever you have.
$buffer = [];
$buffer[] = $factory->increment('api.requests');
$buffer[] = $factory->timing('api.latency_ms', $ms);
$client->send($buffer); // one datagram
3. StatsdService for ergonomic sugar. The bundled service wrapper gives you increment(), decrement(), timing(), gauge() shortcuts that chain and buffer, plus a send() to flush. It's convenient in a controller; verify its exact flush semantics in your pinned version before relying on auto-send in long-running workers (Octane/Swoole), where an un-flushed buffer will leak across requests.
4. Middleware is the canonical Laravel integration point. A StatsdMiddleware that records http.requests, http.latency_ms, and http.status.<code> gives you 80 % of your operational visibility for ~20 lines. Flush in terminate() so you're off the response-critical path:
public function handle($request, Closure $next)
{
$start = hrtime(true);
$response = $next($request);
$this->buffer->push($this->factory->increment('http.requests'));
$this->buffer->push($this->factory->timing(
'http.latency_ms',
(int) ((hrtime(true) - $start) / 1e6)
));
return $response;
}
public function terminate($request, $response) { $this->buffer->flush(); }
5. Centralize metric names. Build a small constants class or enum (Metrics::HTTP_REQUESTS) and prepend the configured prefix inside a thin wrapper. This prevents the app.foo / foo / Foo drift that makes dashboards useless six months later.
6. Sampling. Every factory method takes a $sampleRate argument (default 1); pass 0.1 to send roughly 10 % of events with a |@0.1 suffix and let StatsD scale the counts. Sample counters and timings by all means, but don't sample gauges — you'll get misleading spiky values. For high-volume endpoints, sampling is preferable to batching larger payloads, because datagram size is bounded.
7. Timing helper. The package has no stopwatch abstraction, so write one yourself (hrtime(true) for monotonicity, microtime(true) if you must support PHP < 7.3). A tiny Timing::start() / Timing::stop() utility keeps every call site measuring the same way.
8. Testing. Two options, both cheap: (a) inject a mock SenderInterface into StatsdClient and assert on write() calls; (b) use the bundled EchoSender, which prints instead of sending, for a fast integration test against the real factory/entity pipeline. Never let tests touch a real UDP socket — a dropped packet is a flaky assertion.
9. Custom senders are the natural extension point. Implement SenderInterface and hand it to StatsdClient to ship metrics to a log, a queue, a Unix-domain socket, or a Telegraf/vector sidecar. This is how you get off UDP later without touching call sites.
10. Framework-agnostic use is fine. Outside Laravel, hold the client in whatever container or global you already have; there's no framework coupling in the library.
composer update blindly.Sender may return false or throw depending on your version — always wrap send() in a try/catch and log at debug level. Never let metrics failures fail a user request.SocketSender). On a busy web app this is real overhead. Batch every request into one send() call; if you need more, implement a sender that keeps a persistent socket or writes to a sidecar.\n in a metric name (also sanitize user-supplied labels).|@x is a scaling hint for counters/timers, not a filter for the client.gauge as "last value wins," others support relative +n/-n updates. Don't assume update()/gauge() do the same thing across StatsD, Telegraf, and Datadog; check the emitted type character (g vs c vs ms) with EchoSender if unsure.decrement('x') sends roughly x:-1|c. Keep that in mind when writing dashboards.terminate() are fine in classic PHP-FPM; in Octane you must make sure the buffer is reset per request, or you'll attribute metrics to the wrong request.STATSD_ENABLED=false for local dev) belong in config. The client has no NullSender, but a one-line SenderInterface that returns true is a 30-second write and makes local dev noise-free.EchoSender + var_dump((string) $data) to see exactly what would hit the wire, then nc -ul 8125 to confirm the packet arrives.How can I help you explore Laravel packages today?