whatwedo/monitor-bundle
whatwedo/monitor-bundle is a Symfony bundle for application health monitoring. Define checks, run them via console or HTTP endpoints, and review status in a dashboard to catch service failures and regressions early.
First, the framework mismatch. This is a Symfony bundle, not a Laravel package. There is no service provider, no config/monitor.php, and it will not install into a Laravel app. How you use it depends on which side you're on:
spatie/laravel-health (the mature Laravel equivalent) or ~80 lines of your own route + check classes. Don't try to bridge the container.Symfony install (3 steps):
composer require whatwedo/monitor-bundle
Register the bundle in config/bundles.php (Flex usually does this for you), then hit the monitoring route — confirm the exact path in the README, it's typically /monitor:
curl -i http://localhost:8000/monitor
You should get JSON describing overall status plus per-check results, with HTTP 200 for healthy and a non-2xx for failures.
Where to look first, in order:
README.md — route path, config tree, and the check interface name.src/Controller/ — what the endpoint actually returns; this is your integration contract.src/Check/ (or equivalent) — the built-in checks (database, cache, etc.) and the interface you'll implement for your own.DependencyInjection/Configuration.php or config/*.yaml — the real config keys, not the ones in blog posts.Your first use case: don't build a dashboard. Point an existing prober at it — Uptime Kuma, Healthchecks.io, Pingdom, or a Docker HEALTHCHECK. The bundle's job is to answer "is this app able to serve traffic?" in one HTTP call; the prober's job is alerting and history.
Prober-first integration. The endpoint exists to be polled every 15–60s by something outside the app. Wire it into:
# docker-compose.yml
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/monitor"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
Split liveness from readiness. A single endpoint that includes a database check is a liveness endpoint that will restart your pods during a DB failover. If the bundle only exposes one route, keep the built-in route as readiness (dependencies included) and add a trivial static controller for liveness:
#[Route('/healthz', methods: ['GET'])]
public function live(): Response
{
return new Response('ok', 200);
}
Custom check for your domain. The pattern is always the same: implement the bundle's check interface, return a status plus a human message, and let autoconfiguration pick it up (verify the DI tag in the bundle's service config — if autoconfigure isn't on, you'll need to tag it explicitly in services.yaml).
final class QueueWorkerCheck implements CheckInterface
{
public function check(): CheckResult
{
$last = Cache::get('queue:heartbeat');
$stale = $last === null || $last < time() - 300;
return new CheckResult($stale ? 'error' : 'ok', 'queue worker heartbeat');
}
}
Pair it with a scheduled job or a worker loop that writes queue:heartbeat every minute. This catches the failure mode that plain DB checks miss: the app is fine, the workers are dead, and jobs are silently piling up.
Make each check cheap and bounded. Health checks run forever, on a timer, on every instance. A check that does SELECT COUNT(*) on a 50M-row table, or calls a third-party API with a 30s timeout, will eventually be the reason your monitoring is unreliable. Cache expensive check results for ~30s and set explicit connect timeouts on DB/Redis clients.
Use it as a build/deploy gate. Run the same endpoint in CI after deploy and fail the pipeline on non-200 — cheaper than a smoke-test suite and it exercises the real container wiring (env vars, DB credentials, cache connectivity).
In Laravel, the equivalent shape:
Route::get('/monitor', function () {
$checks = [
'database' => rescue(fn () => DB::select('select 1'), fn () => false, false) !== false,
'cache' => rescue(fn () => Cache::put('hc', 1, 10), fn () => false, false),
'queue' => Cache::get('queue:heartbeat', 0) > now()->subMinutes(5)->timestamp,
];
return response()->json(['checks' => $checks], in_array(false, $checks, true) ? 503 : 200);
})->middleware('throttle:60,1');
The HTTP status is the API. Most probers and all container orchestrators ignore the JSON body and key off the status code. If a check fails but the endpoint still returns 200, your monitoring is decorative. Verify with curl -i after deliberately breaking a dependency (stop the DB, hit the endpoint, confirm 5xx).
It is a Symfony bundle — there is no Laravel path. Bundle registration, the DI container, and the config tree are all Symfony concepts. Forcing a bridge (custom service provider that boots a Symfony kernel) is far more work than reimplementing three checks. spatie/laravel-health exists specifically for this.
Don't let /monitor be public. The JSON response typically names your services and versions — a free recon endpoint. Restrict by network (internal: true in Docker, no ingress rule), by middleware IP allowlist, or by bearer token in the prober config.
A hanging check is worse than a failing one. No timeout on the DB connection means your 5s probe timeout fires and you get a false alarm that resolves on retry — alert noise that trains people to ignore alerts. Set connect_timeout on PDO/Redis to something under your probe's timeout.
Cache checks need a round-trip, not a ping. Redis::ping() succeeding tells you the socket is open. Writing a key with a short TTL and reading it back tells you the cache is actually usable (correct DB index, not read-only replica, not out of memory).
Config only loads in the right environment. If the route 404s, check in order: is the bundle in config/bundles.php? Is the route file imported for that env? bin/console debug:router | grep -i monitor and bin/console debug:container --tag=whatwedo_monitor.check (confirm the tag name in the bundle source) will tell you in seconds what guessing won't.
Alpine containers may not have curl. curl -f is the right call, but a slim/alpine image often lacks it. Either install it in the Dockerfile or use wget --spider -q, which behaves differently on non-2xx — test the healthcheck in isolation before trusting it.
Adoption risk. Three stars, zero dependents, one release in the recent past. That's not a reason to avoid it — it's MIT and small enough to read end to end — but it is a reason to (a) pin the exact version in composer.json, (b) keep your custom checks in your own namespace so they survive a swap to a different bundle, and (c) be prepared to fork. The maintenance cost of a health endpoint is low; the lock-in cost should be too.
Extension points to prefer over forking: custom CheckInterface implementations for domain logic, your own controller/route when you need a different response shape, and an event listener if you need to attach context (build SHA, region) to responses. Only touch the bundle's own classes if you're fixing a bug — and then open the PR, because you're now the maintainer.
Debugging loop that actually works: curl -i locally → if status is wrong, check the controller; if status is right but a specific check is wrong, bin/console debug:container to confirm your check is registered; if it isn't registered, it's autoconfiguration or a missing tag, not your logic.
How can I help you explore Laravel packages today?