l3tum/prometheus-metrics-bundle
A Symfony bundle that exposes Prometheus metrics from your PHP application. Collect counters, gauges and histograms, serve them on a scrape endpoint, and let Prometheus monitor your app's health and performance with minimal setup.
Read this first: this is a Symfony bundle, not a Laravel package. You cannot composer require l3tum/prometheus-metrics-bundle into a Laravel app and expect it to boot. It ships a Symfony Bundle class, a DependencyInjection\Configuration tree, service YAML, and a controller/route for the scrape endpoint — none of which Laravel's container, router, or package discovery understands. There is no service provider, no facade, no publishable config. 1.9.0 does not change this — the release note mentions only request metric labels, not framework support.
What 1.9.0 does change is that the bundle's automatic request collection now emits status and uri labels. That is the first behavioural change in years and it is the one thing you must read before trusting a single dashboard.
Verify release provenance before acting on 1.9.0. The previous version of this assessment claimed no release since 2021-02-18 and zero dependents; that claim was made with no supporting data and is now contradicted. Check Packagist for the
symfony/*version constraints on 1.9.0 — if they still cap at Symfony 4/5, everything below about not installing stands. If they were widened, the package is being revived and the "dead dependency" calculus changes, though Laravel support almost certainly still does not exist.
For a Laravel developer the day-one decision is unchanged: do not install this. The bundle remains a thin Symfony-flavored wrapper around promphp/prometheus_client_php. You get the value by pulling that library directly and writing ~40 lines of glue:
composer require promphp/prometheus_client_phpCollectorRegistry singleton in a service provider, backed by the Redis or APCu adapter.GET /metrics route returning text/plain; version=0.0.4 with the registry's output.curl -s localhost:8000/metrics | head.If you are in a mixed stack with a legacy Symfony frontend (Laravel workers + Symfony web tier), open in this order: src/DependencyInjection/ (the config tree — every knob), src/Resources/config/ (which services are wired, which storage adapter is the default), the routing definition (endpoint path), and — new in 1.9.0 — the collector/subscriber that populates status and uri. Find out exactly what string it puts in the uri label before you enable it: raw path, query string included, or matched route. That single decision determines whether your Prometheus instance survives the week.
First real use case, unchanged in shape but now label-sensitive: an http_requests_total counter with method/route/status plus an http_request_duration_seconds histogram, then a Grafana rate panel. On the Symfony side 1.9.0 may hand you most of that for free — accept it only after you have normalized or dropped the uri label.
Instrument at the boundary, one place. A kernel.request / kernel.response listener pair (start timer, observe on response), or the bundle's built-in collector if 1.9.0 wires one up. For the Laravel equivalent, a middleware plus a terminate() hook so timing excludes response streaming. Never sprinkle counter increments through controllers — you will miss paths and double-count.
Registry access is getOrRegister*:
$counter = $registry->getOrRegisterCounter('app', 'http_requests_total', 'HTTP requests', ['method', 'route', 'status']);
$counter->incBy(1, [$method, $route, (string) $status]);
$histogram = $registry->getOrRegisterHistogram('app', 'http_request_duration_seconds', 'Latency', ['route'], [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5]);
$histogram->observe($seconds, [$route]);
Label discipline is the whole game — and 1.9.0 pushes against it. Keep cardinality bounded: method, matched route name (not raw URI), status class, queue name, command name. Never user ID, order ID, exception message, full URL, or anything unbounded. If you adopt 1.9.0's auto-collected uri label, immediately normalize it: map to the route pattern (/orders/{id}, not /orders/88213/invoices), replace unmatched paths with unmatched/other, and strip the query string unconditionally.
Label normalization pattern for the new uri label:
$uri = $request->getPathInfo(); // no query string
$route = $router->match($uri)['_route'] ?? null;
$label = $route ? "route:{$route}" : 'unmatched'; // collapses scanner noise
Route name is better than route pattern when names are stable; both beat raw path. Collapsing unmatched into one bucket is what stops bots hammering /wp-login.php, /.env, /phpmyadmin from creating thousands of series.
Storage adapter choice determines correctness, not performance.
| Adapter | Use when |
|---|---|
InMemory |
Unit tests only. Resets every request. |
APCu |
Single host, PHP-FPM, shared across requests on that host. |
Redis |
Multiple pods/hosts, or long-running workers. The safe default. |
If you bridge the two ecosystems, run separate scrape targets per app and keep a shared app_ namespace prefix so dashboards and alerts survive renames and rewrites.
Testing. Swap in the in-memory adapter in the test container, hit a route, assert the rendered /metrics body contains app_http_requests_total{...} 1. For 1.9.0 specifically, add a regression test that hits a parameterized route (e.g. /orders/123) and asserts the emitted uri label is the pattern, not 123. Assert on exposition text, not internal objects — that is the only place label regressions are visible.
uri — treat this as a cardinality and privacy hazard, not a feature. Raw URIs contain path parameters, query strings, and therefore possibly emails, order IDs, and access tokens. Two concrete rules: strip the query string, and collapse to route name/pattern. If you can't guarantee normalization, disable the label entirely rather than shipping it.status label is safer but still worth shaping. Raw status codes are bounded (roughly 20 realistic values) so per-code is fine; if you also want 2xx/4xx classes for alerting, derive them in PromQL with label_replace, don't double the label set.composer require.InMemory, every request resets the registry and /metrics always reports zeros or a single request. Read the config default before trusting a number — especially now that a "working" auto-collected metric set may look complete while reporting nothing.uri labels you are now publishing a sample of your URL space, including whatever clients put in the path. Bind /metrics to an internal port, an IP allowlist, or basic auth, and exclude /metrics from your own instrumentation to avoid self-referential noise.count(count by (__name__)({__name__=~"app_.+"})), plus count(count by (uri)(app_http_requests_total)) now that uri exists. Alert on series growth rate, not on absolute cardinality after the fact.uri-labeled counter in a worker is a memory leak with extra steps.bin/console cache:clear; env-var interpolation in the config tree often resolves at compile time, so a changed env var may not take effect without a rebuild. This bites hardest when toggling the new status/uri labels on.curl the endpoint, diff against expected metric names, then verify adapter connectivity independently — a Redis adapter that can't reach its backend may throw during scrape rather than during instrumentation, which reads as "Prometheus is broken" instead of "Redis is down." For label problems specifically, grep the exposition text for uri=" and eyeball it for query strings and IDs.spatie/laravel-prometheus (same underlying client, real Laravel integration) or hand-rolled promphp glue. Both are maintained and let you swap storage adapters without a framework migration. You now also get to make a deliberate choice about uri labeling instead of inheriting one.How can I help you explore Laravel packages today?