ijanki/prometheus-bundle
ijanki/prometheus-bundle provides a Symfony integration for Prometheus monitoring. It exposes configurable application and infrastructure metrics via a scrape-ready endpoint, making it easy to collect, visualize, and alert on service health.
Read this first: this is a Symfony bundle, not a Laravel package. There is no service provider, no config/prometheus.php, no facade, no auto-discovery. ijanki/prometheus-bundle exists to wire tweedegolf/prometheus-client into a Symfony DI container (prometheus.registry, storage adapters, an exporter, and a /metrics controller declared in Resources/config/). A Laravel developer does not get that wiring for free, and in practice you should not try to install it — its composer constraints target Symfony 2/3/4 era and it was last released in December 2018.
What to look at instead, in order:
tweedegolf/prometheus-client — the actual library this bundle wraps. This is where the useful API lives: TweedeGolf\PrometheusClient\Registry, the Collector\Counter|Gauge|Histogram classes, and Storage\*Adapter implementations.promphp/prometheus_client_php — the maintained fork/successor that the PHP ecosystem (and every real Laravel Prometheus integration) has moved to. For a Laravel app today, this is the dependency you want.ijanki/prometheus-bundle's Resources/config/services.xml — read it as pseudo-documentation for how to wire a registry + adapter. That's the only thing you'd "leverage."Minimal Laravel bootstrap (the pattern the bundle would have given you):
// composer require promphp/prometheus_client_php
// app/Providers/PrometheusServiceProvider.php
use Prometheus\Storage\Redis;
use Prometheus\CollectorRegistry;
public function register(): void
{
$this->app->singleton(CollectorRegistry::class, function () {
Redis::setDefaultOptions([
'host' => config('prometheus.redis.host', '127.0.0.1'),
'port' => config('prometheus.redis.port', 6379),
'database' => config('prometheus.redis.db', 0),
'timeout' => 0.2,
'read_timeout' => 10,
]);
return new CollectorRegistry(new Redis());
});
}
// routes/web.php (or a dedicated routes/metrics.php)
Route::get('/metrics', function (CollectorRegistry $registry) {
return response(
(new RenderTextFormat())->render($registry->getMetricFamilySamples()),
200,
['Content-Type' => RenderTextFormat::MIME_TYPE]
);
})->withoutMiddleware(['web']);
First use case: request counter + latency histogram via middleware, exported at /metrics, scraped by Prometheus. That's 90% of what anyone does with this library family.
Middleware instrumentation (the workhorse pattern).
class PrometheusMiddleware
{
public function __construct(private CollectorRegistry $registry) {}
public function handle($request, Closure $next)
{
$start = microtime(true);
$response = $next($request);
$this->registry->getOrRegisterCounter('app', 'requests_total', 'Total requests', ['route', 'status'])
->inc([$request->route()?->getName() ?? 'unmatched', (string) $response->getStatusCode()]);
$this->registry->getOrRegisterHistogram('app', 'request_duration_seconds', 'Latency', ['route'])
->observe(microtime(true) - $start, [$request->route()?->getName() ?? 'unmatched']);
return $response;
}
}
Register it globally but exclude the metrics route itself (see Gotchas).
Domain-level counters in jobs/commands. The bundle's value in Symfony is that the registry is a shared DI service — replicate that with a singleton binding and inject CollectorRegistry into queued jobs. Count business events, not just HTTP:
$this->registry->getOrRegisterCounter('app', 'orders_placed_total', 'Orders placed', ['channel'])
->inc([$order->channel]);
Gauges for state you can recompute. Gauges are last-write-wins in shared storage; set them from a scheduled command (set($value, $labels)) rather than trying to inc/dec from concurrent workers, which is racy.
Adapter selection (mirroring the bundle's config keys).
| Adapter | Use when |
|---|---|
InMemory |
CLI single-shot only — useless across FPM workers |
APC / APCu |
Single host, multiple FPM workers; per-host, so multi-server scrapes show divergent values |
Redis / Predis |
Default choice for any real Laravel deployment; shared across workers and hosts |
PDO |
Only if you have no Redis and accept table growth |
The Symfony bundle exposes this as prometheus.storage.type: redis plus connection params in YAML. In Laravel, translate that to a config/prometheus.php with the same shape — do not try to parse the bundle's YAML.
Porting the bundle's config surface. If you have an existing Symfony config like:
prometheus:
storage:
type: redis
host: '%env(REDIS_HOST)%'
…the Laravel equivalent is a flat config file plus an adapter factory keyed on type. That's the entire "port," and it's ~30 lines.
Expose via a dedicated route file loaded without the web middleware group so you skip sessions, CSRF, and cookie encryption — scrapers send no cookies and you don't want a Set-Cookie on every scrape.
tweedegolf/prometheus-client is unmaintained, the bundle is from 2018, and its Symfony constraints will fight Laravel 9+/PHP 8.1+. Expect composer require ijanki/prometheus-bundle to fail outright on a modern app. Treat this package as a reference implementation, not a dependency.bundles.php, no kernel to register it, and no ContainerBuilder doing the wiring. Even if Composer resolves it, nothing happens at runtime. Port the service definitions into a provider.InMemoryAdapter in an FPM worker means every request starts from zero and every scrape is empty or wrong. Queue workers are separate processes too — jobs that increment counters against in-memory storage lose everything./metrics inflates requests_total and pollutes latency percentiles. Exclude by path or route name./metrics behind auth unless your Prometheus scraper sends credentials — and don't put it behind web at all. Scrape from an internal network or bind the route behind a token middleware you control._total to counters automatically in the text exposition. Naming yours requests_total yields requests_total or requests_total_total depending on the client version — check the rendered output with curl, don't guess.X-Request-Id will explode the Redis key space and Redis memory. Cap to route name, status class, method, tenant shard — never unbounded values.getOrRegisterHistogram($ns, $name, $help, $labels, $buckets) — if you redeclare with different buckets the first registration wins and you'll be confused why your le labels don't match. Pin bucket arrays in config.redis-cli --scan --pattern 'prometheus:*' to confirm writes are landing; curl -s localhost:8000/metrics | head -50 to confirm exposition; then check Prometheus targets page for scrape errors. If HELP/TYPE lines are missing, the exporter is being bypassed.%env(VAR)% and Symfony's TreeBuilder validation. Copying YAML verbatim into config/ does nothing. Re-express as PHP arrays with env() calls.Exporters/ and any CollectorRegistry decoration show how to add a custom exporter or filter metric families. In Laravel, decorate the singleton rather than subclassing — same idea, one line in the provider.Superbalist/laravel-prometheus (historic) and spatie/laravel-prometheus-style packages exist for a reason — they do the provider + middleware + route wiring this bundle does for Symfony. Start there and skip ijanki entirely.How can I help you explore Laravel packages today?