liuggio/statsd-client-bundle
Symfony bundle that integrates the StatsD PHP client, letting you send counters, gauges, timings, and sets to a StatsD daemon over UDP. Configure services and start emitting application metrics with minimal setup.
This is a Symfony 2.x bundle wrapping liuggio/statsd-php-client. It gives you a configured StatsD client service plus optional auto-instrumentation listeners and a profiler panel. It is a thin integration layer — almost everything you can do here you can also do with the underlying client directly.
Minimal setup:
composer require "liuggio/statsd-client-bundle:~1.0"
Register the bundle in app/AppKernel.php (namespace is Luggio\StatsDClientBundle; note the capital D):
new Luggio\StatsDClientBundle\LuggioStatsDClientBundle(),
Configure it — the key is luggio_stats_d_client (not luggio_statsd_client):
# app/config/config.yml
luggio_stats_d_client:
host: 127.0.0.1
port: 8125
protocol: udp # udp (default) or tcp
namespace: myapp # prefixes every metric key
auto_instrument: false # request/exception/doctrine listeners
enable_collector: false # Symfony profiler panel
log_exception: false
throw_exception: false
First use case — instrument a business event:
$statsd = $this->get('luggio_stats_d_client.service');
$statsd->increment('user.registered');
Where to look first:
DependencyInjection/Configuration.php — the authoritative list of options (the README lags the code).Resources/config/services.* — exact service IDs and listener wiring; this is where you confirm what auto_instrument actually registers and which metric names it emits.liuggio/statsd-php-client API (increment, decrement, updateCount, timing, gauge, startTiming/endTiming) — the bundle is mostly just wiring around it.Inject the client, don't fetch it from the container. In Symfony 2 this means constructor arguments with the service ID, or type-hint Luggio\StatsdClient\StatsdClientInterface and add an alias:
services:
luggio_stats_d_client.client_alias:
alias: luggio_stats_d_client.service
Centralize metric names in one small wrapper service (App\Metrics\Statsd) so key spelling and prefixing are consistent and the client can be swapped for a null implementation in tests.
The four metric types you'll actually use:
// Counters — events, errors, cache misses
$statsd->increment('order.created');
$statsd->updateCount('queue.processed', 25);
// Timers — always milliseconds
$start = microtime(true);
$result = $this->api->call();
$statsd->timing('api.payment.duration', (microtime(true) - $start) * 1000);
// Or the built-in pair
$statsd->startTiming('api.payment.duration');
// ... work ...
$statsd->endTiming('api.payment.duration');
// Gauges — "current value" measures (queue depth, pool size)
$statsd->gauge('queue.pending', $count);
Batch when you emit several metrics together. The factory service (luggio_stats_d_client.factory) returns value objects, and the client's send() accepts an array — one socket write instead of N:
$factory = $this->get('luggio_stats_d_client.factory');
$statsd->send([
$factory->increment('checkout.started'),
$factory->timing('checkout.duration', $ms),
$factory->gauge('cart.items', $items),
]);
Turn on auto_instrument in production only. It registers listeners that emit baseline HTTP request timing/memory/status-code metrics, exception counts, and Doctrine query metrics. That's a good free dashboard for a legacy app, but it's noise (and overhead) in dev/test. A common split:
# config_prod.yml
luggio_stats_d_client:
host: statsd.internal
namespace: myapp.prod
auto_instrument: true
enable_collector: false
# config_dev.yml / config_test.yml
luggio_stats_d_client:
namespace: myapp.dev
auto_instrument: false
enable_collector: true # profiler panel for eyeballing metrics
Use the namespace for environment separation. One StatsD/Graphite backend, myapp.prod.* vs myapp.staging.*, and deleting an environment is a prefix drop rather than a hunt through dashboards.
Extension point — decorate the sender. The socket sender is a separate service (luggio_stats_d_client.connection). Override/decorate it to drop metrics by prefix, log what's sent in dev, fan out to two backends, or substitute an echo/null sender in tests. This is cleaner than wrapping the client.
Where metrics belong in day-to-day code: controllers stay thin; emit from domain services and event listeners. A kernel.terminate listener that flushes a small batch is a reasonable place for request-scoped totals, though the client writes on each send() anyway.
The bundle is unmaintained (last release 2016) and Symfony 2-only. composer.json pins Symfony ~2.x, and services are written for the old container. Do not plan to carry it onto Symfony 3.4+/4+/5+. For legacy 2.x apps in maintenance it's fine; for anything new, use the underlying client directly or a modern metrics path (Monolog handler, Prometheus, OpenTelemetry).
It's fire-and-forget UDP. Packets can be dropped, reordered, or duplicated. Never use StatsD counters as the source of truth for billing or anything auditable — they're for trends. Large batched payloads may exceed the MTU and get fragmented or lost; keep batches small.
Metric-name cardinality is the classic mistake. Dots are hierarchy separators in Graphite. order.created.12345 creates a series per order and will melt your metrics store. Bucket instead: order.created, order.amount.bucket.50_100. Never put user IDs, URLs, or exception messages in a key — exception class is acceptable.
Sample rates are client-side and mean different things per type. increment($key, 0.1) sends |@0.1; StatsD scales counters back up, but timers are simply sampled — you lose the long tail. Default to 1 unless you're genuinely flooding the network.
Timing units are milliseconds everywhere. startTiming/endTiming already multiply the delta by 1000. If you hand-roll a timer, convert.
Gauge semantics vary by server. Etsy's StatsD treats a value starting with +/- as a delta; Influx/Telegraf-style backends may not. The client just forwards the number, so "decrement a gauge" is backend-specific. Test it against your actual daemon.
Failures are silent by default. With throw_exception: false and log_exception: false, a wrong host/port produces zero packets and zero complaints. Set throw_exception: true in dev/staging to catch misconfiguration, and leave it quiet in prod. Also watch for DNS resolution blocking on the first send — point at an IP if your resolver is slow.
Service visibility breaks on Symfony 3+. Everything assumes public services and $container->get('luggio_stats_d_client.service'). If you're on a transitional 2.8 setup with private services, you'll need explicit public: true.
Config key spelling is easy to get wrong: luggio_stats_d_client — two ds, no statsd contraction. A typo silently means "no config", so the client defaults to 127.0.0.1:8125 and your metrics vanish. After editing config, clear the cache and verify with app/console container:debug luggio_stats_d_client (or debug:container on 2.8+).
Debugging without a StatsD server: nc -u -l 8125 prints raw packets (metric:1|c, metric:123|ms). tcpdump -n -i any udp port 8125 confirms they leave the box. Running the real statsd with its console/debug backend is the fastest way to see what the auto-instrumentation listeners actually emit — useful because the metric names aren't documented clearly in the README.
protocol: tcp requires server support and can block under backpressure; UDP is the sane default. The profiler collector (enable_collector) holds metrics in memory per request — dev only.
Version pinning: no tag since 2016, so audit what composer.lock actually resolved and pin the commit. Also check the resolved liuggio/statsd-php-client version — the bundle's behavior (prefix handling, sender API) follows it.
How can I help you explore Laravel packages today?