liip/monitor
liip/monitor provides health-check endpoints for Laravel applications, running configurable checks for database, cache, queue, disk space, and custom services. Use its JSON status output to power uptime monitoring and alerting.
Read this first: liip/monitor was archived in 2014 and is explicitly marked Deprecated. It still installs from Packagist (composer require liip/monitor), and its only runtime requirement is a conservative php >= 5.3 constraint, so it will resolve on PHP 8 — but nothing in it has been touched in a decade. Treat it as a reference implementation of a health-check DSL, not as infrastructure for a new service. If you are evaluating it today, you are really evaluating whether the CheckInterface + CheckResult contract is worth copying.
The whole library is essentially three classes. Read them in order, top to bottom, in about ten minutes:
Liip\Monitor\Check\CheckInterface — three methods: getName(), getGroup(), check(): CheckResult.Liip\Monitor\Result\CheckResult — value object holding a name, a status constant (OK, WARNING, CRITICAL — verify exact spelling in your installed version), a human message, and a details array.Liip\Monitor\Exception\CheckFailedException — the "this check blew up" escape hatch.There is no runner, no HTTP layer, and no Symfony dependency in the core package (the routing/controller/console plumbing lives in the separate, also-abandoned liip/monitor-bundle). In Laravel you wire it yourself. The first use case is a single /health endpoint that returns 200 or 503:
// app/Health/RedisCheck.php
use Liip\Monitor\Check\CheckInterface;
use Liip\Monitor\Result\CheckResult;
class RedisCheck implements CheckInterface
{
public function getName(): string { return 'redis'; }
public function getGroup(): string { return 'cache'; }
public function check(): CheckResult
{
try {
Redis::connection()->ping();
return new CheckResult($this->getName(), CheckResult::OK, 'Redis reachable');
} catch (\Throwable $e) {
return new CheckResult($this->getName(), CheckResult::CRITICAL, 'Redis unreachable', [
'exception' => get_class($e),
]);
}
}
}
Route::get('/health', function (array $checks) {
$results = array_map(fn ($c) => $c->check(), $checks);
$worst = in_array(CheckResult::CRITICAL, array_column($results, 'status'), true) ? 503 : 200;
return response()->json(['status' => $worst === 200 ? 'ok' : 'fail', 'checks' => $results], $worst);
});
Aggregate with worst-status-wins. The library gives you per-check results and nothing else, so the aggregation policy is yours. A single CRITICAL should poison the whole response; WARNING should stay 200 but be visible in the payload (and in your metrics), otherwise you train people to ignore the endpoint.
Split liveness from readiness. This is the pattern that actually pays for itself day-to-day. Register checks in two collections:
| Probe | Checks | Failure means |
|---|---|---|
/health/live |
php_version, disk_space, opcache |
restart the container |
/health/ready |
db, redis, queue_backlog, migrations |
stop routing traffic |
Never let liveness depend on MySQL. A DB blip should drain traffic, not restart every pod into a crash loop.
Register checks in a service provider and tag them. Keep getGroup() meaningful (db, cache, queue, external) and resolve by tag so you can run a subset:
$this->app->tag([DatabaseCheck::class, RedisCheck::class, QueueBacklogCheck::class], 'health.checks');
$this->app->tag([PhpVersionCheck::class, DiskSpaceCheck::class], 'health.liveness');
Artisan command + scheduler for alerting. The endpoint is for orchestrators; a command is for humans and Slack. Exit non-zero on CRITICAL, print WARNING to stderr, and only notify on state transitions — notify-on-every-run is how alert channels die.
Laravel-specific checks worth writing (the bundled check list is Symfony-shaped and stale): DB::select('select 1'), Redis::ping(), Queue::size('default') > threshold, Storage::disk('s3')->exists(...), Artisan::call('migrate:status') for pending migrations, cache()->has('schedule:last_run') as a scheduler heartbeat, and free bytes on the storage partition.
Cache the expensive results. Probes get hit every 5–10 seconds per replica. Wrap the whole run in Cache::remember('health', 15, ...) so an S3 or third-party HTTP check doesn't turn your health endpoint into a load source for someone else's API.
app/Health/ and delete the dependency.CheckResult's argument order and the status constants (OK/WARNING/CRITICAL vs. prefixed variants) changed across a handful of 2013-era tags. grep -n "const " vendor/liip/monitor/src/Liip/Monitor/Result/CheckResult.php before you write twenty checks against the wrong spelling.check() throws and your controller doesn't catch, you return a 500 with a stack trace instead of a 503 with a message — and some orchestrators treat 500 differently from 503. Catch \Throwable inside every check, including Error.PDO::ATTR_TIMEOUT / connect_timeout, Guzzle timeout + connect_timeout, Redis read_write_timeout. A health endpoint that hangs for 30s is functionally identical to a dead app, and it will burn a PHP-FPM worker per probe.getDetails() leaks. DSNs, absolute paths, PHP extensions, framework versions — all useful to an attacker. Gate details behind app()->isLocal() or an IP allowlist, and return only name/status/message in production.Content-Type: application/json. Load balancers, Kubernetes, and most uptime monitors key off the status code; a JSON body served as text/html breaks half the dashboards.getName() is your identity key. It becomes the array key and the metric label. Keep it stable and machine-friendly (db_primary, not Database (primary)), or your Grafana series will churn every refactor.spatie/laravel-health (check registry, Health::check(), result stores) or beyondcode/laravel-self-diagnosis for environment sanity. On the Symfony side the successor bundle is likewise abandoned — write a controller and use symfony/... probes directly. Both give you scheduled checks, history, and notifications that LiipMonitor never had.CheckInterface is the entire API surface, which is precisely why porting away from this package is cheap: your check classes survive a rewrite almost verbatim if you keep them free of Liip imports except the two interfaces. Do that deliberately.How can I help you explore Laravel packages today?