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

App Metrics Laravel Laravel Package

avto-dev/app-metrics-laravel

Laravel package for collecting and exposing application metrics, with built-in integrations for Prometheus and other backends. Adds middleware and service providers to track requests, responses, and custom counters, helping you monitor performance and health with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the version line first. This package is small (4 stars) but has had a semver-in-minor event: v2.10.0 raised the floor to Laravel ^11.0 while still being a 2.x release. Before anything else:

composer require avto-dev/app-metrics-laravel:^2.10
composer show avto-dev/app-metrics-laravel   # confirm resolved version + requires

If you are on Laravel 10, do not run a bare composer update with a permissive constraint — pin to the last Laravel 10-compatible release and upgrade the framework first.

Auto-discovery means near-zero bootstrap. The service provider is discovered automatically, so the first thing to look at is not code but config:

php artisan vendor:publish --provider="AvtoDev\AppMetrics\Laravel\ServiceProvider" --tag=config

Open the published config/metrics.php and read four things, in order:

  1. The route/endpoint definition (path, middleware, domain) — this is what Prometheus scrapes.
  2. The enabled collectors / metrics list — which built-ins run by default.
  3. Whether the HTTP middleware (request timing / status-code counters) is opt-in or on.
  4. Any storage/cache settings for expensive collectors.

Verify the endpoint before writing any code. This is the fastest smoke test:

php artisan route:list | grep -i metrics
curl -s http://localhost:8000/metrics | head -50

You should see Prometheus exposition text (# HELP, # TYPE, name{label="value"} 1). If you see HTML, a login redirect, or a 419, jump to Gotchas — the endpoint is being swallowed by the web middleware group.

First real use case. Do not start with custom metrics. Start by enabling the request middleware so you get status-code counters and request-duration histograms for free, confirm they appear in the scrape output after a few requests, then add one business metric (e.g. orders_created_total) in the place that already owns that domain event. That sequence gives you a proven scraper → storage → dashboard path before you invest in instrumentation.


Implementation Patterns

Pattern 1: Class-based custom metrics (the default shape)

The underlying app-metrics library is built around metric objects that expose a name, labels, and a value computed at scrape time — not around an in-process counter you increment. Follow that model:

namespace App\Metrics;

use AvtoDev\AppMetrics\Metrics\MetricInterface; // confirm exact contract in vendor/

final class OrdersCreated implements MetricInterface
{
    public function name(): string
    {
        return 'orders_created_total';
    }

    /** @return array<string, string> */
    public function labels(): array
    {
        return ['channel' => 'web'];
    }

    public function value(): int|float
    {
        return Order::query()->where('created_at', '>=', now()->subDay())->count();
    }
}

Register it either in the config's metric list (preferred — it survives config caching) or at runtime via the manager binding. Check src/ for the registration method name; both a metrics array in config and a Manager::addMetric()-style call typically exist.

Why value-at-scrape matters: in PHP-FPM each request is a fresh process, so an in-memory counter incremented in a controller resets to zero before the next scrape. Metrics whose value() reads from the source of truth (DB, Redis, cache) are correct across processes and across pod replicas. Reserve in-memory increments for long-lived processes (Octane, queue workers) and only when you understand the reset semantics.

Pattern 2: Instrument at the domain boundary, not the controller

Put custom instrumentation in events/listeners, jobs, or service-layer methods rather than sprinkling it across controllers. A single OrderCreated listener that records to Redis is testable and survives refactors of the HTTP layer. Controllers are the wrong seam for business metrics; they are the right seam for HTTP metrics, which the package's middleware already covers.

Pattern 3: Gauges for queue and cache state, refreshed on a schedule

For values that are expensive to compute (queue depth, stale-cache rows), don't recompute on every scrape. Refresh into the cache on a schedule and have the metric read the cached value:

// app/Console/Kernel.php (or routes/console.php in Laravel 11+)
$schedule->call(fn () => Cache::put('metrics.queue_depth', Queue::size('default'), 120))->everyMinute();

Then value() is a Cache::get() — a scrape costs one Redis round-trip, not a SELECT COUNT(*) over a large table. With a 15s Prometheus scrape interval and 20 replicas, that difference is the difference between a metrics endpoint and a self-inflicted outage.

Pattern 4: Keep the endpoint off the public internet

The /metrics route should be reachable by the scraper only. Practical options, in order of preference:

  • Bind it to an internal domain or a separate port via the config's route settings.
  • Restrict with allowed_ips-style middleware or a network policy at the ingress.
  • If you must authenticate, use a static bearer-token middleware — not session auth, which will trip CSRF and redirects on a GET from Prometheus.

Pattern 5: Testing instrumentation

Feature-test the value, not the exposition format:

it('reports order count', function () {
    Order::factory()->count(3)->create();

    $this->get('/metrics')
        ->assertOk()
        ->assertSee('orders_created_total', false);
});

Because value() reads from the DB, tests with RefreshDatabase are deterministic. Assert on the metric name and label set rather than parsing the whole payload — the exposition format is the library's concern, not yours.

Pattern 6: Multi-service consistency

If you run several Laravel services, standardise three things in a shared internal package or a documented convention: metric naming (snake_case, base units — seconds not milliseconds, bytes not MB), a common service/env label injected via config, and a shared Grafana dashboard skeleton. The package gives you the plumbing; naming discipline is what makes the data joinable across services.


Gotchas and Tips

The endpoint is behind web middleware by default in many setups. Symptoms: a 419, a 302 to /login, or an HTML body in the scrape. Fix by moving the route out of the web group in config/metrics.php (or overriding the route definition). This is the single most common first-day failure with metrics packages in Laravel.

Cardinality is the real risk, not performance. A label with a user ID, an order ID, or a raw URI (/orders/12345) will create unbounded series and eventually take down Prometheus. Rules of thumb: label on route name not path; label on enum-like values (status, channel, queue); never label on anything a user can generate. If you need per-tenant breakdowns, cap the tenant count and bucket the rest into other.

Prometheus naming conventions are enforced by convention only. orders_created and orders_created_total are different metrics to Prometheus. Counters must end in _total, and once a name is in use in a backend, renaming it silently breaks dashboards and alerts. Decide names before the first deploy — migrating a metric name in Prometheus is a manual, historical-data-losing operation.

Config caching interacts with runtime registration. If you register metrics by calling the manager during a service provider's boot(), it still works under config:cache, but metrics registered by reading a dynamically-built config array will freeze at cache time. Prefer a static metrics array in config and let each metric class do its own work in value().

Long-running processes change the rules. Under Octane, Swoole, or a queue worker that stays alive, an in-memory counter accumulates across requests/jobs — and accumulates per worker process, so N workers each report a fraction of the truth. Either compute from a shared store, or make peace with per-process semantics and don't aggregate across replicas.

Scrape cost is a production concern. Every scrape executes every registered metric's value(). Ten metrics each doing a COUNT(*) on a million-row table, scraped every 15s by a HA Prometheus pair, is 8 queries/second of pure overhead per replica. Cache aggressively; keep the number of DB-backed metrics in the single digits.

Semver cannot be trusted here — read the changelog on every bump. Dropping Laravel 10 shipped as 2.10.0, a minor. Treat any 2.x update as potentially containing a breaking platform requirement: pin to ~2.10 (or exact), read the release notes, and validate in a branch. Set a Renovate/Dependabot rule that never auto-merges this package.

Laravel 13 support may be declarative. The release notes show a constraint bump plus CI image changes; there is no evidence of Laravel-13-specific code changes. Before adopting on 13, confirm the package's CI matrix actually runs against 13, and run your own integration test against the exact 13 minor you deploy.

PHP version drift between the package's Docker image (8.5) and your runtime. The composer.json runtime constraint is the authoritative one, not the CI container. Check composer show avto-dev/app-metrics-laravel for php requirements and confirm it matches or is below your production PHP.

Debugging a missing metric. Work outward: (1) curl the endpoint directly on the pod; (2) check the metric appears in the raw exposition text; (3) if it's absent, confirm the class is registered — a class that throws in value() is typically swallowed rather than fatal, so a broken metric looks identical to an unregistered one; (4) temporarily wrap value() in a try/catch with Log::warning to surface the exception; (5) confirm the scrape target's instance/job labels in Prometheus, since a successful scrape of the wrong pod looks like missing data downstream.

Extension points worth knowing: the config array (metrics, route, middleware), the manager binding in the container (swap it to add cross-cutting labels like service/env to every metric), custom metric classes implementing the library contract, and the middleware (subclass or replace it if you need route-name labels or want to exclude health-check and metrics paths from your own instrumentation to avoid feedback loops).

One self-referential trap to close early: exclude the /metrics and health-check routes from the request middleware's own counters, otherwise scraper traffic dominates your http_requests_total and every latency dashboard is polluted by 15-second-interval scrapes.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin