vmorozov/laravel-prometheus
A Laravel package for integrating Prometheus metrics into your application. Expose metrics endpoints, track counters and gauges, and add observability to Laravel services with minimal setup. Ideal for exposing application and request metrics to Prometheus scrapers.
Install and publish config
composer require vmorozov/laravel-prometheus
php artisan vendor:publish --provider="Vm\Prometheus\PrometheusServiceProvider"
In Laravel ≥5.5 the service provider is auto-discovered; on older versions add it to config/app.php under providers.
Where to look first — after publishing you'll get a config/prometheus.php (namespace, storage adapter, route enabled flag). Read src/ in this order:
PrometheusServiceProvider.php — what's bound in the container, what tags it registers.Collectors/ — the built-in collector classes you can subclass.Storage/ — the adapter (APCu / Redis / InMemory) that persists metric samples between requests.routes/ (or the Route:: call in the provider) — the /metrics endpoint you expose to Prometheus.First use case — expose an HTTP-request counter so Prometheus has something to scrape:
// app/Http/Middleware/TrackRequests.php
public function handle($request, Closure $next)
{
$counter = Prometheus::counter('http_requests_total', 'Total HTTP requests')
->labels($request->method(), $request->path());
$counter->inc();
return $next($request);
}
Register it in Kernel.php and point your local Prometheus scrape_configs at http://app.local/metrics. Verify with curl -s localhost/metrics | head, you should see # HELP, # TYPE and sample lines in the text/plain; version=0.0.4 format.
Exposing the endpoint. Either let the package register /metrics automatically (config flag route.enabled) or mount it yourself so you can wrap it in auth/throttle middleware:
Route::get('/metrics', [\Vm\Prometheus\Http\Controllers\MetricsController::class, 'show'])
->middleware(['auth:api', 'throttle:60,1']);
Custom collectors for domain metrics. Subclass the base collector, implement describe() (returns metric families) and collect() (populates them). Register the class in config/prometheus.php under collectors so it runs on every scrape:
class OrderCollector extends Collector
{
public function describe(): array { /* HELP/TYPE definitions */ }
public function collect(): array { /* query DB, build MetricFamilySamples */ }
}
Counters / gauges / histograms via the facade. Day-to-day you'll mostly use the fluent API rather than raw collectors:
Prometheus::gauge('queue_depth', 'Jobs waiting')->set(Queue::size('default'));
Prometheus::histogram('order_value_usd', 'Order totals', [10, 50, 100, 500])->observe($order->total);
Instrumenting Laravel subsystems. The common pattern is a small set of middleware + event listeners:
http_requests_total and http_request_duration_seconds (histogram keyed by route, method, status).Illuminate\Queue\Events\JobProcessed to bump a job-success counter; one on JobFailed for failures.Multi-process storage. For classic PHP-FPM you cannot use the in-memory adapter — each worker has its own memory. Use the Redis adapter (shared across workers and pods) or APCu if you accept per-worker visibility. Document the choice in config/prometheus.php:
'storage' => [
'driver' => 'redis',
'redis' => ['connection' => 'metrics', 'prefix' => 'prom:'],
],
Grafana alignment. Keep metric names stable and snake_case; use the same label set across dashboards so PromQL joins don't break. Precompute expensive metrics in scheduled commands (or collectors with an internal Cache::remember) rather than doing heavy SQL on every scrape.
Testing. Boot the app, run the middleware, then hit /metrics in a feature test and assert the sample line is present:
$this->get('/metrics')->assertSee('http_requests_total{method="GET",path="orders"}');
Reset storage between tests (Prometheus::clear() or the adapter's flush()) so counters don't leak across cases.
/metrics public. It leaks route names, queue names, and sometimes business metrics. Put it behind network policy or a shared secret header, and remember throttle middleware can drop scrapes — Prometheus retries, but gaps appear in dashboards.$request->path(), user IDs, order IDs, or query strings as labels. Normalize to the route name ($request->route()->getName()) and cap the number of distinct values for any label with an unbounded set (e.g. status codes → bucket into 2xx, 4xx, 5xx).rate() interprets as a spike. Use a persistent backend or increase()/rate() tolerant dashboards with a unless guard.pm.max_children = 20 you'll get 20 divergent counters and the scraper only sees whichever worker answers. Redis (or a dedicated exporter sidecar) is the safe default in production.spatie/laravel-prometheus or promphp/prometheus_client_php are also installed, they may register their own /metrics. Pick one, alias the service bindings, and check php artisan route:list | grep metrics before debugging "why is my metric missing".Prometheus::counter() returns an existing metric, ->labels(...) mutates the shared instance. Always pass labels on the same call as counter(); caching the instance and re-calling ->labels() across requests is a classic source of cross-request label bleed in Octane.InMemory storage survives between requests — metrics accumulate forever (memory leak) and reset only on Octane reload. Either flush per request via a terminating middleware or (better) use Redis and treat the adapter as authoritative.+Inf and your p95 is meaningless.curl -v http://localhost/metrics — check content type and that the metric family is present at all.describe() output — a missing # TYPE line means the collector is registered but its family isn't described; Prometheus will reject the sample.config/prometheus.php (config is cached — run php artisan config:clear after edits).StorageAdapter contract); collectors are additive — register one class per domain rather than one god-collector; and the exporter route/controller is not final, so you can subclass MetricsController to add auth or content negotiation for OpenMetrics (application/openmetrics-text) instead of the legacy text format.How can I help you explore Laravel packages today?