ahmedmerza/watchtower
Ahmed Merza's Watchtower is a Laravel/PHP package for application monitoring and observability. It gives you a lightweight way to keep an eye on key events and health signals, helping you spot issues early and stay informed with simple setup.
Laravel Watchtower gives you a master-owned IP blocklist that satellites mirror, plus an admin page and a GET /blocks JSON endpoint reading the blacklisted_ips table.
composer require ahmedmerza/laravel-watchtower
php artisan vendor:publish --tag=watchtower-migrations
php artisan migrate
Upgrading to v1.1.0 adds exactly one migration, update_blacklisted_ips_table_with_list_indexes. Publish and run it on every node that holds the table — master and satellites both — not only the master.
WATCHTOWER_MASTER_URL=https://master.example.com
WATCHTOWER_SYNC_SECRET=... # must match what the master expects
As of v1.1.0 the secret is effectively mandatory wherever WATCHTOWER_MASTER_URL is set.
php artisan route:list | grep watchtower — the management page and /blocks.watchtower:sync (full signed paged pull) and watchtower:reconcile (drift repair).blacklisted_ips migration to understand source, scope, and expires_at.watchtower:sync on a satellite and confirm it lands, then run watchtower:reconcile on the satellite and read the output — on a healthy node it should do nothing.Pull topology, not push. Satellites fetch; the master never initiates. watchtower:sync does a paged signed pull of the whole active list, so schedule it at whatever cadence matches your acceptable staleness — five to fifteen minutes is typical.
Reconcile is a repair pass, not a second sync. Since v1.1.0 it reads the master's paged list and queues a PushBlockToMaster job only for a block the master is missing or holds with an earlier expiry. A node with nothing of its own to repair doesn't contact the master at all. That makes tight scheduling safe — job volume tracks drift, not blocklist size — so pairing watchtower:reconcile (every minute) with watchtower:sync (every five) is a reasonable default.
Feeds are the bulk path. Tens of thousands of rows arrive from feed import. Set source on every imported row so the admin filter and the new (source, created_at) index stay useful, and don't route bulk data through per-row admin actions.
Reuse the page's predicates in custom tooling. The management page and /blocks sort by (created_at, id), filter by source, and expose an expired view. Any dashboard or report you add should use the same columns and ordering so it stays on the new indexes.
Schedule through the scheduler, not ad-hoc calls:
$schedule->command('watchtower:sync')->everyFiveMinutes()->withoutOverlapping();
$schedule->command('watchtower:reconcile')->everyMinute()->withoutOverlapping();
withoutOverlapping() matters on large blocklists: reconcile now reads in chunks, but a slow or unreachable master still makes a run long.
update_blacklisted_ips_table_with_list_indexes on every node. Without it things still work — just at the old 15–96 ms per page on 50k rows instead of sub-millisecond. Three indexes, no backfill, no downtime.scope. It's deliberately unindexed: nearly every row is global, so an index on it selects almost the whole table and the planner discards it. expires_at is indexed only because the expired view matches rarely; a walk of the sort index alone was slower there.WATCHTOWER_MASTER_URL but no WATCHTOWER_SYNC_SECRET fails the laravel target's reconcile with an explicit message. This replaces the old behaviour of queueing PushBlockToMaster jobs that each logged a warning and sent nothing — expect your failure notifications to be noisier right after the upgrade, and treat that as intended.watchtower:sync does. A master outage that used to leave reconcile "quietly broken" now shows up as a failing run. If that's too loud, gate the schedule behind a health check rather than disabling it.PushBlockToMaster; check failed_jobs and the reconcile command output before assuming the master is rejecting writes. The pre-v1.1.0 false negative — jobs queued, nothing sent, warnings only — no longer exists, so a warning now means an actual problem.source drives admin filtering and the source index; scope decides whether reconcile and the sync pull consider a row node-local or global. Set both on any custom importer, or your rows will be invisible to the filters and misjudged as drift.How can I help you explore Laravel packages today?