Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Metrics Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

beberlei/metrics v3 is still a framework-agnostic PHP library, not a Laravel package: no service provider, no config file, no auto-discovery, no facade. The v3 Symfony bundle does not help you here — you wire it into Laravel yourself, but v3 makes that easier than v2 because Factory is now the supported construction path.

composer require beberlei/metrics:^3.0

Check your runtime first: v3 requires PHP >= 8.4 and psr/log ^3.0. If you are on 8.2/8.3, you stay on 2.x and none of the release below applies.

Coming from 2.x? Read UPGRADE.md before touching code — Collector was renamed to CollectorInterface, every method is now strictly typed, returns void, and takes array $tags = []. Any custom collector you wrote will fatal until you update its signatures.

Then read the source in this order:

  1. src/.../Collector/CollectorInterface.php — the whole API surface. increment, decrement, timing(int|float $milliseconds), measure, gauge (only on GaugeableCollectorInterface), flush. All accept $tags.
  2. src/.../Factory.php — now the intended entry point. It builds every bundled collector (statsd, telegraf, null, prometheus, doctrine_dbal, influxdb_v1, influxdb_v2, cloudwatch, opentelemetry, chain) from a config array.
  3. src/.../Collector/ — the individual collectors, especially Chain, OpenTelemetry, and NullCollector.

Bind the interface once, driving it from config through the factory:

// app/Providers/MetricsServiceProvider.php
public function register(): void
{
    $this->app->scoped(CollectorInterface::class, function ($app) {
        return Factory::create($app['config']->get('metrics.collector'));
    });
}

Confirm the exact array shape against Factory::create() and the bundle's collectors documentation — it is the same configuration vocabulary. Register the provider manually (bootstrap/providers.php on Laravel 11+, config/app.php on older versions).

First use case: instrument one high-value path. Add a counter to a domain event and a timer around an HTTP request, verify locally with the null collector, then point the factory at statsd (or chain with [statsd, cloudwatch]) in staging. If you already run OpenTelemetry, skip straight to the opentelemetry collector and get histograms and dashboards for free.

Implementation Patterns

Config first, factory second. Keep every driver decision in config/metrics.php and let Factory::create() consume it; never read env() from inside a service class or construct collectors by hand.

// config/metrics.php
return [
    'collector' => [
        'type'   => env('METRICS_COLLECTOR', 'null'),
        'prefix' => env('METRICS_PREFIX', 'myapp'),
        'host'   => env('STATSD_HOST', '127.0.0.1'),
        'port'   => (int) env('STATSD_PORT', 8125),
        // ...collector-specific keys; see Factory and the demo app config
    ],
];

Inject CollectorInterface, never a concrete class. All collectors and Factory are final in v3, so this is no longer just good taste — extension is only possible by implementing the interface. It also lets you swap StatsD for CloudWatch, InfluxDB, or OTel without touching call sites.

Use Chain instead of duplicating calls. Fan-out to several backends is now a first-class collector: send the same metric to StatsD and OpenTelemetry, or to a local Graphite and CloudWatch. Note that gauge() is only forwarded to collectors implementing GaugeableCollectorInterface — the rest silently drop it.

Scoped bindings replace Symfony's kernel.reset. The bundle tags collectors kernel.reset; the Laravel equivalent is a scoped() binding (or forgetScopedInstances()), which resets the instance per request under Octane. Do not use singleton() for collectors that hold buffers or per-request state.

Request middleware with terminable flush. The library still has no request-lifecycle awareness, so you own flush().

final class RecordRequestMetrics
{
    public function __construct(private CollectorInterface $metrics) {}

    public function handle($request, Closure $next)
    {
        $start = microtime(true);
        $response = $next($request);

        $this->metrics->timing(
            'http.request.duration',
            (microtime(true) - $start) * 1000,     // milliseconds, float is fine in v3
            ['method' => $request->method(), 'status' => (string) $response->getStatusCode()]
        );

        return $response;
    }

    public function terminate($request, $response): void
    {
        $this->metrics->flush();
    }
}

Domain events over controller sprinkles. One listener per meaningful event (OrderPlaced, PaymentFailed, UserRegistered) keeps metric names and tag sets stable and testable, and keeps metrics out of HTTP code.

Queue and scheduler instrumentation. Time the work and flush() in a finally so long-running Horizon/queue workers don't accumulate state. increment('scheduler.ran', 1, ['command' => $event->command]) is usually enough to catch a silently dead cron.

Tags are now native — use them, carefully. Pass low-cardinality tags per call, or fix a base set once in the collector constructor:

$this->metrics->increment('orders.placed', 1, [
    'channel' => $order->channel,        // small, bounded set
    'region'  => config('app.region'),
]);

Never tag user IDs, order IDs, or raw URLs. The StatsD/Telegraf dialect encodes tags into the metric name (metric,key=value:value|type), so each unique tag set is a distinct series — tags change the ergonomics, not the cardinality math.

Prefixing via a decorator. There is still no namespace concept beyond the collector's prefix option; if you need dynamic per-tenant prefixes, wrap the collector in a class implementing CollectorInterface that prepends before delegating. Cheap, and it survives upgrades.

Testing. Bind the null collector in the test container, or write a recording fake implementing CollectorInterface (~40 lines with the tag parameter) and assert on the recorded name, value, and tags. With final classes you cannot subclass collectors to spy — the fake is the only route, and it is the better one anyway.

Choose collectors by latency profile. StatsD/Telegraf are UDP fire-and-forget. CloudWatch (PutMetricData), InfluxDB v2, and DoctrineDBAL are synchronous network/DB calls. Put those behind Chain only if you understand the added latency, and prefer emitting them from queued jobs rather than the request path.

Gotchas and Tips

  • Still not a Laravel package. No provider, config, facade, or discovery — and the v3 Symfony bundle is not a substitute. If you want Metrics::increment() ergonomics, add your own facade over the CollectorInterface binding; constructor injection remains the better default.
  • PHP 8.4 is a hard floor. Update CI matrices, Docker base images, and any shared library you publish. beberlei/metrics:^3.0 will not install on 8.3.
  • Errors are now swallowed by design. Collectors never let an underlying client exception reach your application — great for uptime, terrible for observability about your observability. Watch logs (psr/log ^3.0 is a dependency) for collector failures, and verify each backend once in staging with the shipped examples/ Docker + Grafana stack. Silent data loss is the default failure mode of every metrics pipeline.
  • Breaking renames to grep for on upgrade: Collector → CollectorInterface, GaugeableCollector → GaugeableCollectorInterface, InfluxDB → InfluxDbV1 (factory type influxdb_v1), removal of TaggableCollector/setTags(), Null, InlineTaggableGaugeable*, Zabbix, and Librato. measure()/increment()/decrement() map to an OpenTelemetry UpDownCounter, timing() to a Histogram, and gauge() to a Gauge — so measure() is not a gauge in OTel-land.
  • Timing units are now unambiguous. v3 fixes timing() to int|float milliseconds with fractional values preserved by every collector. The old "units are backend-dependent" trap is gone; still suffix or document the unit for humans reading dashboards.
  • gauge() lives on a separate interface. Anything typed against CollectorInterface cannot call it, and Chain drops it for non-gaugeable members. If you need gauges, type-hint GaugeableCollectorInterface or keep a concrete reference.
  • Long-running processes need explicit flushing. The Symfony bundle's kernel.reset tagging has no Laravel analogue unless you use scoped() bindings. Under Octane or Horizon, flush per request/job and never hold per-request state on the collector.
  • DoctrineDBAL collector schema changed. created now stores a full datetime (Y-m-d H:i:s) and measurement must accept floating-point values — migrate the table before enabling it, or flush() will fail (and, per the notes, a rollback failure no longer masks the original exception, which is an improvement but still noisy).
  • Telegraf drops buffered metrics after a failed flush. Retries are not guaranteed; do not use it for anything you would page on.
  • Factory is final with reworded error messages. Do not match on exception text in tests or tooling; assert on your own config validation instead.
  • Metrics are not a source of truth. Never place a metric call inside a transaction or on a critical path, and never derive business logic from a counter.
  • Version pinning. Pin ^3.0 and re-read UPGRADE.md before a 4.x. The API is small and stable, but v3 proved this project is willing to make sweeping, hard-breaking changes at a cadence that is not Laravel-package-friendly.
  • Extension point: implementing CollectorInterface (strict types, void returns, $tags) is the intended escape hatch. A decorator adding tenant prefixes, a measureTiming(callable $fn) helper, or a debug-mode recording collector is the fastest way to gain ergonomics without forking.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
adachsoft/changelog-linter