m6web/statsd-bundle
Symfony bundle for sending StatsD metrics over UDP. It provides service configuration and helpers for counters, gauges, timers, and sets, making it easy to instrument applications with lightweight performance and business metrics.
Read this first: it is a Symfony bundle, not a Laravel package. m6web/statsd-bundle (M6Web\Bundle\StatsdBundle) registers a statsd service, a config tree under m6web_statsd, and an annotation/event listener system tied to Symfony's kernel. Composer will install it into a Laravel app, but nothing will be wired up — there is no service provider, no facade, no config publish. The only thing directly reusable in Laravel is the underlying framework-agnostic client library (m6web/statsd, the M6Web\Statsd\Client class), which the bundle wraps.
So the practical first step is to decide which layer you need:
m6web/statsd (the client), not the bundle. Skip to the service-provider wiring below.Minimal Laravel wiring for the underlying client:
composer require m6web/statsd
// config/statsd.php
return [
'host' => env('STATSD_HOST', '127.0.0.1'),
'port' => (int) env('STATSD_PORT', 8125),
'namespace' => env('STATSD_NAMESPACE', 'myapp'),
'protocol' => env('STATSD_PROTOCOL', 'udp'),
];
// app/Providers/StatsdServiceProvider.php
public function register(): void
{
$this->app->singleton(\M6Web\Statsd\Client::class, function ($app) {
$config = $app['config']['statsd'];
$client = new \M6Web\Statsd\Client($config['namespace']);
// The bundle maps this to `m6web_statsd.clients.default.servers`;
// in plain Laravel you attach a server directly.
$client->addServer($config['host'], $config['port'], $config['protocol']);
return $client;
});
}
First use case: time and count HTTP requests in a global middleware, because that is the highest-value metric and exercises the whole path (client → UDP → collector) end to end. Run socat -u UDP-RECV:8125 - in a terminal, hit a route, and confirm a line arrives before you write any more instrumentation. Once that works, add a facade (Statsd::increment(...)) or inject the client directly.
Look first at the bundle's README sections on the m6web_statsd.servers / clients config and its EventListener classes — those are the parts worth copying conceptually. Ignore the Annotations\Statsd machinery unless you are staying on Symfony.
Singleton client + thin facade. Bind the client once (as above), then expose a facade so callers don't have to inject. Keep the facade thin — it should map 1:1 onto the client's increment, decrement, timing, gauge, update, and pass tags/sample rate through unchanged. This mirrors what the Symfony bundle does with the statsd service, and it makes the client swappable in tests.
HTTP middleware for request timing. This replaces the bundle's kernel.request/kernel.response event config and its annotation-based controller timing:
public function handle($request, Closure $next)
{
$start = microtime(true);
$response = $next($request);
$this->statsd->timing('http.request', (microtime(true) - $start) * 1000, 1.0, [
'route' => $request->route()?->getName() ?? 'unmatched',
'method' => $request->method(),
'status' => $response->getStatusCode(),
]);
$this->statsd->increment('http.request.count', 1.0, [
'status' => $response->getStatusCode(),
]);
return $response;
}
The bundle's annotation listener effectively does the same thing around a controller action; in Laravel, a #[StatsdMetric] PHP attribute plus a route middleware or a decorator is the equivalent. PHP attributes are the correct translation of the bundle's Doctrine annotations — do not carry over @Statsd(...) docblocks, they will be silently ignored.
Event listeners for domain metrics. The bundle's events: config is just a declarative mapping of “on event X, emit metric Y.” In Laravel you get this for free with Event::listen, which is more flexible:
Event::listen(OrderPlaced::class, function (OrderPlaced $event) {
Statsd::increment('order.placed', 1.0, ['channel' => $event->order->channel]);
Statsd::gauge('order.total', $event->order->total_cents);
});
Prefer explicit listeners over a generic config-driven dispatcher. The bundle's YAML event mapping is convenient in Symfony because listeners are already registered; in Laravel, a config-driven dispatcher adds indirection without buying much.
Queue and job instrumentation. Wrap jobs with a job middleware (or a base job class) that times execution and increments success/failure counters. This is where the bundle has no equivalent — its event config targets Symfony kernel events, not async workers — and where StatsD is most valuable in a Laravel app.
Testing. Bind a recording double in the container during tests and assert on emitted metrics:
$this->app->instance(Client::class, $fake = new FakeStatsdClient());
$this->get('/orders');
$fake->assertIncremented('http.request.count', ['status' => 200]);
Because the real client is fire-and-forget over UDP, tests will otherwise pass while silently emitting nothing. Always assert against a fake, never against a real socket.
Tags over metric names. The bundle predates widespread tag support; its config encourages metric names like request.GET.users.show. Use the |#tag:value format instead (Datadog/Telegraf/statsd_exporter all support it). It keeps cardinality manageable and dashboards composable.
m6web/statsd client is small and stable enough to vendor or copy into a Support namespace if you want zero external risk.namespace: myapp in config and also emit myapp.http.request, you get myapp.myapp.http.request. The bundle's clients.default.namespace key behaves the same way. Pick one place to prefix and never do it in call sites.socat -u UDP-RECV:8125 - or tcpdump -i lo -n udp port 8125, not with your application code. Add a dev-only fallback logger so local development surfaces intended emissions.microtime(true) - $start (seconds) with (microtime(true) - $start) * 1000 will produce dashboards that are off by a factor of 1000 and are painful to diagnose later.increment($metric, 0.1) sends 1 in 10 events but the backend scales it back up. If you pass a sample rate and your collector/aggregator does not honor it, counters will read 10× low. Leave it at 1.0 unless you have a measured volume problem.env() outside a config file, php artisan config:cache can produce a client with stale or null connection values. Always read config('statsd.*'), never env() in the service provider body.register() (per-worker) rather than holding references across requests.How can I help you explore Laravel packages today?