flagbit/metrics-bundle
Flagbit MetricsBundle is a Symfony bundle for gathering and exposing application metrics. It offers a simple way to instrument services and publish measurements to your monitoring stack, helping teams track performance and business KPIs. Designed for easy setup in existing Symfony projects.
flagbit/metrics-bundle is a Symfony bundle, not a Laravel package. It is archived (last meaningful activity years ago), has a single star and zero dependents, and its service wiring is Symfony DI (YAML/XML + compiler passes). You should not composer require it into a Laravel app and expect anything to boot — there is no config/bundles.php, no auto-discovery, and no Illuminate\Contracts bridge.
Treat it as a reference implementation for a pattern: tag collector services, resolve them from the container, and expose an aggregated metrics output. Port the idea, not the package.
Minimal steps for a Laravel developer:
collect()/getValue() method),name{labels} value) because it scrapes cleanly; StatsD or JSON are alternatives.MetricCollector),MetricsServiceProvider that binds and tags collectors,/metrics route (or php artisan metrics:collect command) that resolves tagged collectors and renders.First use case: expose queue depth, failed-job count, signup count for the last 24h, and cache hit ratio as a single protected /metrics endpoint scraped by Prometheus.
Define a narrow interface. Keep it value-only; labels belong on the registration, not inside the collector.
interface MetricCollector
{
public function name(): string;
/** @return array<string,string|int|float> */
public function labels(): array;
public function value(): float|int;
}
Register and tag in a service provider. This is the Laravel analogue of the bundle's compiler pass.
final class MetricsServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(MetricRegistry::class, fn ($app) =>
new MetricRegistry($app->tagged('metrics.collectors'))
);
}
public function boot(): void
{
$collectors = [
QueueDepthCollector::class,
FailedJobsCollector::class,
SignupCountCollector::class,
CacheHitRatioCollector::class,
];
foreach ($collectors as $class) {
$this->app->bind($class);
}
$this->app->tag($collectors, 'metrics.collectors');
}
}
Aggregate lazily, render once. The registry should resolve tagged() on demand, not cache values across requests (important under Octane).
final class MetricRegistry
{
public function __construct(private iterable $collectors) {}
public function all(): iterable
{
foreach ($this->collectors as $collector) {
try {
yield $collector;
} catch (Throwable $e) {
report($e); // one broken collector must not blank the endpoint
}
}
}
}
Expose a protected endpoint. Never leave /metrics public; use an internal host, a token, or throttle.
Route::get('/metrics', MetricsController::class)
->middleware(['throttle:60,1', 'metrics.token']);
final class MetricsController
{
public function __invoke(MetricRegistry $registry): Response
{
$lines = [];
foreach ($registry->all() as $c) {
$labels = $c->labels();
$suffix = $labels ? '{' . collect($labels)
->map(fn ($v, $k) => sprintf('%s="%s"', $k, addslashes((string) $v)))
->implode(',') . '}' : '';
$lines[] = sprintf('%s%s %s', $c->name(), $suffix, $c->value());
}
return response(implode("\n", $lines)."\n", 200)
->header('Content-Type', 'text/plain; version=0.0.4');
}
}
Push instead of pull. If you scrape nothing, schedule a command that iterates the same registry and ships to StatsD/Pushgateway. Reusing the registry keeps pull and push in sync.
Update counters from domain events. Listeners (UserRegistered, JobFailed) writing into a cache/Redis key are cheaper and more accurate than recomputing from the DB on scrape.
Test with fakes. Bind a FakeCollector into the container and assert exact exposition output; snapshot the /metrics response with assertSee on each metric name.
composer.json; port the interfaces and wiring instead.tagged() resolves. $app->tag() only records tags for abstracts the container can make; bind concrete classes first or use $this->app->singleton() to be explicit.status, queue, driver).*_collector_errors_total gauge so you notice silently broken metrics.COUNT(*) over a big table on every scrape will stall your metrics endpoint and, if it shares a pool, your app. Cache values for 15–60s in Redis or the cache store._total suffix for counters, do not emit +Inf/NaN unquoted incorrectly, and always terminate lines with \n.php artisan tinker → app()->tagged('metrics.collectors') to confirm registration; app(MetricRegistry::class) to inspect the resolved iterable; php artisan route:list --path=metrics to verify middleware. There is no bundle debug command to lean on.metrics.collectors) as the public contract other packages can honor.How can I help you explore Laravel packages today?