m6web/statsd-prometheus-bundle
Symfony bundle that bridges StatsD metrics to Prometheus: it collects counters, gauges and timings via the m6web/statsd library and exposes them in Prometheus exposition format for scraping, typically through a /metrics endpoint.
Install and register
composer require m6web/statsd-prometheus-bundle
// config/bundles.php
M6Web\Bundle\StatsdPrometheusBundle\M6WebStatsdPrometheusBundle::class => ['all' => true],
Requires at least v8.0.2 if you are on a modern Symfony: that patch widened the constraints to symfony/contract 3.* and psr/container 2.*, which is what Symfony 6.3+/7.x ships. Older tags will fight your lockfile on psr/container.
Configure the bridge — the bundle wires two things: a DogStatsD client that emits tagged metrics, and a Prometheus exporter that turns those metrics into a scrapeable exposition format. A typical config block looks like this (confirm the exact alias and keys against the bundle's DependencyInjection/Configuration.php, since the package is lightly documented):
# config/packages/m6web_statsd_prometheus.yaml
m6web_statsd_prometheus:
statsd:
host: '%env(STATSD_HOST)%'
port: '%env(int:STATSD_PORT)%'
prometheus:
namespace: 'app'
# storage adapter (in-memory / apcu / redis) is usually configurable here
Where to look first
README.md for the canonical usage example (there is usually one end-to-end snippet).DependencyInjection/Configuration.php — the real source of truth for config keys and defaults.Resources/config/services.yaml — service IDs and aliases you can autowire.bin/console debug:container --tag=statsd (and search for prometheus) to see what's actually injected in your app.First use case — inject the client into a controller/service and emit a tagged counter:
use M6Web\Bundle\StatsdPrometheusBundle\Client\Client; // confirm class/interface name
final class CheckoutController
{
public function __construct(private Client $statsd) {}
public function pay(): Response
{
$this->statsd->increment('checkout.payment', 1, [
'gateway' => 'stripe',
'status' => 'ok',
]);
// ...
}
}
Then hit the Prometheus exposition endpoint (or bin/console command) the bundle registers and verify the metric appears with the tags translated to labels.
#gateway:stripe,status:ok in DogStatsD becomes {gateway="stripe",status="ok"} in Prometheus. Design your tag set once and reuse it via constants/enums so the client call sites and the scrape output stay consistent.final class InstrumentedPaymentGateway implements PaymentGateway
{
public function __construct(
private PaymentGateway $inner,
private Client $statsd,
) {}
public function charge(int $cents): void
{
$start = microtime(true);
try {
$this->inner->charge($cents);
$this->statsd->increment('payment.charge', 1, ['result' => 'success']);
} catch (\Throwable $e) {
$this->statsd->increment('payment.charge', 1, ['result' => 'error']);
throw $e;
} finally {
$this->statsd->timing('payment.charge.duration', (microtime(true) - $start) * 1000);
}
}
}
Wire it in services.yaml with decorates: App\PaymentGateway.kernel.terminate subscriber is the natural place to emit http.request counters and duration histograms, keyed by route, method, and status class — keeping tag cardinality bounded./metrics endpoints.(name, value, tags) triples and assert on those; this is more reliable than trying to read the Prometheus registry back.kernel.terminate — otherwise the Prometheus side may not see updates until the process exits (depending on storage adapter).user_id, order_id, email, or raw URLs will explode the time series and take down your Prometheus server. Restrict tags to low-cardinality dimensions (status, route, gateway, region).payment.charge.duration); Prometheus prefers snake_case with units suffixes (payment_charge_duration_seconds). The bundle usually normalizes, but verify the output — a silent rename breaks dashboards. Reserve a naming convention up front.nc -u -l 8125 and look for packets, then check the Prometheus endpoint separately./metrics will often look empty.symfony/contract ^3.0 and psr/container ^2.0 are now allowed. That is what unblocks Symfony 6.3+/7.x installs (Symfony ships psr/container: ^2.0 since 6.3). It is a patch-level constraint widening with no API change, so it does not by itself prove runtime compatibility with Symfony 7 — run your test suite and boot the kernel before assuming it. If you are still on 5.4, stay on the older tag; if you are on 6.4/7.x, pin ^8.0.2.curl it, and grep for the name. If it's absent, check: (1) was the client actually called, (2) is the tag set valid, (3) is the storage backend the one you think, (4) is the route registered (bin/console debug:router | grep metrics).services.yaml (decorates: or alias replacement) to add sampling, redaction of sensitive tags, or to route to an alternate registry. The exporter is also the right seam to add a prometheus_client_php adapter if the bundled default doesn't fit.BedrockStreaming/StatsdTagsPrometheusBundle upstream, with low star count and no meaningful dependent base. The v8.0.2 patch is maintenance-only — dependency bumps, no features, no roadmap. Pin the exact version in composer.json ("m6web/statsd-prometheus-bundle": "8.0.2"), commit the lockfile, and re-check constraints on every Symfony upgrade; be prepared to fork. Note that a two-year-old patch release means the next Symfony minor may well break the install again with no upstream fix. If you only need one of the two halves (tagged statsd or a /metrics endpoint), a smaller hand-rolled service plus promphp/prometheus_client_php may be lower risk than adopting the whole bundle.debug:config doesn't recognize your YAML block, dump Configuration.php and copy the root key exactly.How can I help you explore Laravel packages today?