spiral/roadrunner-metrics
PHP bridge for RoadRunner's Metrics plugin. Declare counters, gauges, and histograms from your application workers and publish them over RPC to RoadRunner's metrics endpoint for Prometheus scraping, with simple .rr.yaml configuration and worker examples.
Install the package via Composer (note the new name as of 3.4.0):
composer require roadrunner/metrics
The package was renamed from spiral/roadrunner-metrics to roadrunner/metrics. The old name is deprecated; pin spiral/roadrunner-metrics:^3.3 only if you must stay on RoadRunner v2.
Check your platform requirements. 3.4.0 requires PHP 8.2+ (was 8.1). Bump config.platform.php in composer.json, your CI matrix, and any Docker base image before upgrading, otherwise composer update will silently resolve back to 3.3.x.
Enable the metrics plugin in your RoadRunner config (.rr.yaml):
version: "3"
metrics:
address: "0.0.0.0:2112"
RoadRunner v3 is now the supported runtime; the version: "3" preamble and the reworked plugin/RPC section are the main config deltas versus the v2 examples you'll find in older blog posts.
Update namespaces. The rename moved the namespace too:
// before
use Spiral\RoadRunner\Metrics\Metrics;
// after
use RoadRunner\Metrics\Metrics;
use RoadRunner\Metrics\Collector;
Do a project-wide search for Spiral\RoadRunner\Metrics — this is the single most common upgrade failure.
First use case — declare a metric once per worker, then write to it per request:
use RoadRunner\Metrics\Metrics;
use RoadRunner\Metrics\Collector;
/** @var Metrics $metrics */
$metrics = $container->get(Metrics::class); // prefer the container/factory over `new`
$metrics->declare(
'http_request_duration_seconds',
Collector::histogram('Duration of HTTP requests', ['route', 'method'])
);
$start = microtime(true);
// ... handle request ...
$metrics->observe('http_request_duration_seconds', microtime(true) - $start, [
'route' => '/api/users',
'method' => 'GET',
]);
Scrape at http://localhost:2112/metrics (Prometheus exposition format) and point your Prometheus scrape config at it. See the refreshed README (rewritten in this release) for the current constructor/factory wiring — most stale snippets online use the pre-3.x client API.
Declare once, write many. Registration happens through declare() at worker boot (service provider boot(), middleware constructor, or a RoadRunner worker bootstrap listener). Writes go through add() / observe() / set() on the hot path:
$metrics->declare('http_requests_total', Collector::counter('Total HTTP requests', ['method', 'route']));
$metrics->declare('http_request_duration_seconds', Collector::histogram('Latency', ['route'], [0.01, 0.05, 0.1, 0.5, 1.0, 5.0]));
$metrics->declare('queue_depth', Collector::gauge('Jobs waiting'));
Middleware is the natural seam for HTTP instrumentation — start the clock in handle(), record in a finally block, and tag with the route pattern ($request->route()->getCompiled() / route name) rather than the raw URI so cardinality stays bounded:
$route = $request->route()?->getName() ?? 'unmatched';
Buckets are chosen at declaration time. Pass them as the third argument to Collector::histogram(). There is no runtime bucket mutation — pick buckets that match your SLO thresholds (e.g. [0.05, 0.1, 0.25, 0.5, 1, 2.5] for web requests) and restart workers to apply changes.
Wrap the client behind an interface. Given the 3.4.0 package/namespace rename and the v2→v3 runtime jump, binding RoadRunner\Metrics\Metrics in a small MetricsRecorder service (or a facade) means the next rename touches one file instead of every controller and job.
Queue/CLI workers need the same treatment. Long-running rr workers can drift in memory; a gauge updated from a Loop/tick middleware is the cheapest way to expose worker health, queue depth, or cache hit ratios.
Per-instance labels for multi-tenant paths. Anything user-supplied (tenant id, customer slug) should be mapped through a bounded allow-list or hashed bucket; unbounded label values will blow up the RoadRunner metric registry, not just Prometheus.
🚨 Breaking: package and namespace rename. spiral/roadrunner-metrics → roadrunner/metrics, Spiral\RoadRunner\Metrics\* → RoadRunner\Metrics\*. Composer will not auto-alias the old name, so you'll get "class not found" at boot if you only bump the constraint. Grep for both strings in config, DI bindings, and any class_exists/reflection-based feature detection.
🚨 Breaking: PHP 8.2 minimum. Also expect #[\Override] attributes on the implementation — harmless unless you extend the shipped classes and override methods that no longer exist in the parent (that now becomes a fatal error rather than a silent no-op). If you subclass Metrics or implement its interfaces, re-check signatures after the upgrade.
🚨 Breaking: RoadRunner v3 only. On the flip side, this release drops v2 support. If you're still on RR 2.x, stay on 3.3.x and the old package name — those two constraints move together.
🛑 Never call declare() inside the request handler. Re-declaring the same metric name with different metadata throws, and doing it per request leaks registry entries across the (reused) worker process. Guard with a bool $registered flag or once().
🔍 Label sets must match exactly — same names, same order, same count, on every write for a given metric. A missing or extra label doesn't warn; it creates a distinct series or fails silently at the RPC layer, and you'll discover it as a flat or missing panel in Grafana.
📏 Counter vs. gauge semantics. Use add() for counters (monotonic), set() for gauges (absolute). Incrementing a gauge with add() works but produces weird reset artifacts on worker restart, since gauges are reset to the worker's view on scrape.
🐞 Debugging: run RoadRunner with -v/-vv (v3's logging replaced some of the v2 --debug output). Metric RPC failures surface as logged errors, not exceptions in your request path — check the worker log before assuming your instrumentation code is at fault.
🔧 Config quirks: the metrics plugin must be listed under the server's plugin set for the RPC to be reachable from workers. A metrics section present in .rr.yaml but absent from the RPC plugin list yields "method not found" on the first declare().
📁 File descriptors: the exposition address binds a socket per RoadRunner instance; on high-fan-out deployments (many containers with low nofile limits) raise ulimit -n and monitor with process_open_fds.
🔄 Version alignment: pair roadrunner/metrics:^3.4 with the matching RoadRunner v3 binary. Mixing a v3 PHP client with a v2 rr binary fails at handshake time, usually with an unhelpful RPC transport error.
🔌 Extensibility: for non-Prometheus backends, don't fork the client — scrape /metrics yourself or implement a thin adapter around the recorder service. The package's job is the RPC bridge plus exposition; keeping that boundary clean is what makes the next major bump cheap.
How can I help you explore Laravel packages today?