beberlei/metrics
A lightweight PHP metrics library for instrumenting applications with counters, gauges, and timings. Pluggable collectors send measurements to backends like StatsD and Graphite, keeping monitoring code decoupled from your application logic.
Bottom line (revised for v3.0.0): the release materially retires the two strategic objections from the prior assessment — the OpenTelemetry drift and the missing tag/histogram vocabulary — and converts the fail-safe requirement from "we must build a decorator" into a stated contract. What it adds in exchange is a hard PHP 8.4 floor, a hard-break 2.x→3.x upgrade, and final classes that eliminate subclassing as an extension path. Net: the case for adopting it behind an app-owned interface is stronger than before, but the go/no-go now pivots on a single cheap question — is our target runtime on PHP 8.4? If yes, this is a credible primary metrics facade. If we are on 8.2/8.3 with no near-term 8.4 plan, the package is simply unavailable to us and the prior assessment's caution stands by default rather than by judgment.
Architecture fit
#[Target], kernel.reset tagging, lazy collector services) rather than a framework-neutral PSR container binding — a signal that Symfony is the first-class integration target and Laravel will remain DIY.CollectorInterface is now strictly typed, void-returning, and tag-native per call (array $tags = []). This resolves the earlier "weak/ambiguous tag support" finding: per-tenant and per-route dimensions are expressible without backend-specific hacks, and timing() preserves fractional milliseconds.final on all collectors and on Factory changes our extension strategy. Decoration-by-inheritance is off the table; composition is the only path. That promotes the app-owned Metrics interface from "highest-leverage nicety" to structural necessity, and it means our wrapper — not the upstream class — is the place we add cardinality guards, sampling, and Laravel-specific context.measure/increment/decrement → UpDownCounter, timing → Histogram, gauge → Gauge on a MeterProviderInterface. That is instrument-level mapping only: aggregation, views, resource attributes, exporters, exemplars and span correlation remain the OTel SDK's job and are not supplied here. It removes the "parallel track that must be replaced" objection for the API surface, but does not give us a finished OTel pipeline.Chain collector is quietly valuable for us: it enables dual-write during a backend migration (e.g. StatsD + OTel) without touching call sites. Note the documented asymmetry — gauge() is only forwarded to collectors that support it.promphp client and InfluxDB v1 on influxdb/influxdb-php, replacing two abandoned SDKs. This is a genuine hygiene improvement that reduces our transitive-dependency risk.Integration feasibility
psr/log ^3.0. The psr/log floor is compatible with Laravel 10/11/12 but rules out Laravel 9 — check before assuming a clean install across the fleet.Factory::create() reads configuration. Factory is now final, so we cannot subclass around a getenv()-only implementation; if it ignores config(), config-cached production containers silently resolve to a no-op. Mitigable by constructing collectors explicitly in our provider rather than via the factory, but confirm in the spike.aws/aws-sdk-php is a hard require, we inherit a large vendor tree into every service for a collector we will not use. This is a one-line composer.json check and a real objection if it fails.kernel.reset. The Symfony bundle resets collector state at request/message boundaries; Laravel has no such hook. Under Octane or long-lived queue workers, any buffering collector (Telegraf explicitly buffers, DoctrineDBAL flushes) must be reset or flushed by a listener we write. This is now a named, concrete piece of glue, not a generic caution.Technical risk
final everything) reads as one maintainer executing a considered roadmap rather than a drive-by. It still ships with no published support or EOL policy, and a release this large is often followed by a quiet period. Treat the next two quarterly reviews as the tell.final across the surface make a fork a heavier lift. The app-owned interface is what keeps the exit cost at ~1–2 engineer-days, and it is now doing more work than before.user.{id} dimensions, and the OTel and Prometheus paths are both unforgiving. This remains a review-convention and per-service-budget problem that we own entirely.PutMetricData over the AWS SDK includes retry/backoff, and Chain multiplies the fan-out. A collector that never throws but occasionally blocks for 800ms is still an availability problem. Async delivery or a hard latency budget is required for any collector on the request path.Collector → CollectorInterface, Null class removed, TaggableCollector removed, several bundle service aliases and the XML services file gone, Zabbix/Librato deleted, InfluxDB renamed to InfluxDbV1). The removal of Zabbix and Librato has no replacement — if any team depends on either, that team is stranded on 2.x indefinitely.Key questions
aws/aws-sdk-php and influxdata/influxdb-client-php optional in composer.json, or do we drag them into every service?Factory::create() respect Laravel config()/config:cache, or does it read getenv() directly?Stack fit
config/metrics.php, one facade, one no-op binding for tests and local dev. The difference from the prior plan is that the no-op must now be our own class implementing CollectorInterface — the deprecated Null class is gone, and the "empty config registers a null collector" behaviour is a Symfony-bundle convenience, not a plain-PHP one.Metrics interface and keep beberlei/metrics strictly behind it. This is now load-bearing rather than merely prudent: final classes mean our wrapper is the only extension point, per-call tags mean we want to normalize tag keys in one place, and the OTel bridge means we want the freedom to swap the instrument mapping without touching call sites.Chain deliberately for the migration window only — dual-write to the incumbent and the new backend, then collapse to one. Remember the gauge() forwarding asymmetry when the two paths are compared.Migration path
UPGRADE.md (which is unusually good — before/after examples per break), and inventory Zabbix/Librato usage first, because those users have nowhere to go.created column must store a full datetime (Y-m-d H:i:s) and measurement must accept floats. This is the first time this package has implied schema work — flag it explicitly if the DBAL collector is in use anywhere.Log::info('counter...') or direct backend SDK calls into the facade opportunistically.Chain window, not a code sweep.Compatibility
psr/log ^3.0 against the real fleet matrix, in a container matching production. Test config:cache behaviour specifically, not php artisan serve.kernel.reset tag gives for free. Verify no stale or unbuffered collector survives between requests.Sequencing
config() vs getenv() question; confirm CloudWatch/Influx deps are optional; prove a metric lands end-to-end; prove a dead backend neither throws nor stalls a request; prove Octane request boundaries reset collector state. Gate on the last three.Maintenance
final, upstream hotfixes cannot be applied by subclassing. Our wrapper is the patch surface; keep it thin enough to reason about during an incident.Support
examples/ application — every collector wired behind Docker, provisioning a Grafana dashboard per backend, running in CI — is a genuine ramp-up and debugging asset. It is Symfony-shaped, so expect a translation step when reading it against a Laravel provider.Scaling
PutMetricData is rate-limited and per-request unsuitable at our volume; reserve it for aggregated or batch signals. InfluxDB v2 writes per point — confirm batching before putting it on a hot path.Failure modes
config:cache → silent no-op, dashboards empty while the app looks healthy. Unchanged from prior review and now harder to patch around because Factory is final. Mitigate with a boot-time smoke assertion and an alert on ingest silence.Ramp-up
How can I help you explore Laravel packages today?