kobaltdigital / pingpong-agent
Reports a Laravel app's scheduler and health to PingPong
Requires
- php: ^8.2
- illuminate/bus: ^12.0|^13.0
- illuminate/console: ^12.0|^13.0
- illuminate/contracts: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/process: ^12.0|^13.0
- illuminate/queue: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^11.0.0|^10.0.0
- pestphp/pest: ^3.8|^4.0
- pestphp/pest-plugin-arch: ^3.1|^4.0
- pestphp/pest-plugin-laravel: ^3.2|^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- spatie/laravel-ray: ^1.35
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v1.1.0
- v1.0.0
- dev-feature/tick-tijd-aanpassen-omgeving
- dev-fix/windows-host-tests
- dev-feature/pm2-monitor-check-pingpong
- dev-fix/endpoint-redirect
- dev-feature/ping-pong-monitoring-brainstorm
- dev-feature/resend-on-request
- dev-feature/inventory
- dev-feature/scheduled-tasks
- dev-feature/queue-canary
- dev-feature/port-louis
This package is auto-updated.
Last update: 2026-10-15 13:43:35 UTC
README
The Agent reports a Laravel app to PingPong, the uptime and incident platform of Kobalt Digital. Once installed it announces itself to PingPong and ticks every minute (configurable per environment) from the app's own scheduler, so PingPong raises an Incident when the scheduler stops.
This package is built for internal use at Kobalt Digital. It is public so client apps can install it without credentials, but it comes without support. Issues and pull requests from outside Kobalt Digital may go unanswered.
Requirements
- PHP 8.2 or higher
- Laravel 12 or 13
- The app's scheduler in cron, once a minute:
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1
The tick runs from that scheduler. Without the cron line PingPong receives the handshake but no ticks, and says so on the Monitor.
Installation
Install the package via composer:
composer require kobaltdigital/pingpong-agent
Create an Agent key on the app's Monitor in PingPong and add it to the app's .env:
PINGPONG_KEY=pp_agent_...
That is all. Every environment of an app is its own Monitor with its own key, so acceptance and production each get their own PINGPONG_KEY. Without a key the Agent sends nothing and schedules nothing.
Optionally, publish the config file:
php artisan vendor:publish --tag="pingpong-agent-config"
This is the contents of the published config file:
use KobaltDigital\PingPong\Signals; return [ 'enabled' => env('PINGPONG_ENABLED', true), 'endpoint' => env('PINGPONG_ENDPOINT', 'https://pingpong.kobaltdigital.nl'), 'key' => env('PINGPONG_KEY'), 'tick_minutes' => env('PINGPONG_TICK_MINUTES', 1), 'signals' => [ Signals\Database::class, Signals\Cache::class, Signals\Disk::class, Signals\FailedJobs::class, Signals\Queue::class, Signals\Load::class, Signals\Memory::class, Signals\AppState::class, Signals\Pm2::class, ], ];
Set PINGPONG_ENDPOINT only to point the app at a local or staging PingPong. It must be the final https URL. The Agent still follows a redirect, but logs a warning naming the URL to set instead. Removing a class from signals stops the tick from sending that signal.
The tick runs every minute. Set PINGPONG_TICK_MINUTES to tick less often on one environment, for example 5 on acceptance. The value must divide 60 (1, 2, 3, 4, 5, 6, 10, 12, 15, 20 or 30); any other value logs a warning and ticks every minute. Each tick carries its interval, so PingPong can tell a slower environment from a late tick.
Switching it off
Set PINGPONG_ENABLED=false to silence the Agent without removing the key, for example on a local machine that has a copy of the production .env. The Agent then sends no handshake and adds nothing to the schedule.
The Agent is also silent while the app runs its unit tests (APP_ENV=testing), even with a key set, so a test suite never reports to the real Monitor.
What it sends
Handshake. On the first boot with a key set, the Agent posts its version, the PHP version, the Laravel version, the OS family (os) and the number of CPU cores (cores) to PingPong. It sends from app()->terminating(), after the response has gone out, so no visitor waits on it. A deploy boots the app anyway (package:discover, migrate), so the handshake usually lands during the deploy. It is sent once per Agent version; upgrading the package sends a new one. A failed handshake is tried again ten minutes later.
Tick. pingpong:ping is added to the app's schedule by the package itself. It runs every minute (or every PINGPONG_TICK_MINUTES) in the foreground with withoutOverlapping, and posts a tick. The tick keeps running while the app is down for maintenance (php artisan down), and says so in app.maintenance. Queue workers pause while the app is down, so the canary of such a tick does not come back; PingPong should read app.maintenance before judging the canary. The tick proves the scheduler is alive and carries health signals measured on the server that sent it:
- Database. Whether the default connection connects, with a 2 second connect timeout, and answers a
select 1, and how long the query took. - Cache. Whether the default cache store writes, reads and removes a probe key, and how long that took.
- Disk. Free and total bytes of the disk that holds the app.
- Failed jobs. How many jobs landed in the
failed_jobstable since the previous tick. - Queue. Unless the default queue runs jobs without a worker (
sync,deferred,background,null), every tick dispatches a small canary job to it. The worker that runs the canary reports it back to PingPong, so PingPong knows the workers are alive and how long a job waited. This is the only report that goes through the queue. On thedatabasedriver the tick also counts how many jobs wait per queue and how long the oldest due one has waited. - Load. The system load average over 1, 5 and 15 minutes.
- Memory. Total and available memory, and swap in use.
- App. Whether the app is in maintenance or debug mode.
- PM2. Every process PM2 runs for the scheduler's user, with its status, restarts, uptime and memory. Left out when PM2 is not installed. No PM2 command runs while no daemon does, so the Agent never starts one; the list is then empty. The environment PM2 keeps per process is never sent.
These are raw numbers. PingPong decides what counts as a problem. A check that fails, for example because the database is down, is reported in the tick as a fact rather than thrown into the app. A signal the server cannot provide (memory on macOS, queue backlog on Redis) is simply left out.
The signals are cheap: a few syscalls, one file read, a few small queries and, on a server that runs PM2, one pm2 jlist. The backlog count reads at most 10000 jobs, so a large backlog does not make it slower. The database signals share one connection to the default database, opened once per tick with a 2 second connect timeout that Laravel retries once. An unreachable default database costs the tick about 4 seconds, and no canary is dispatched to a database queue on it. Not bounded: a select 1 that hangs after connecting, connections other than the default, and the cache signal and overlap lock when the cache store is database. Collecting them costs less than booting the scheduler that runs them.
Inside a container, load, memory and cores describe the host machine, not the container's limits.
Every request carries the Agent key as a bearer token, the payload schema and the hostname of the sending server. Requests time out after 5 seconds. When PingPong is down, slow or answers with an error, the Agent logs a warning and carries on; it never throws into the app. A 429 means PingPong asked the Agent to slow down, so that tick is skipped rather than retried.
Check-ins. Every scheduled task the app defines checks in with PingPong when it starts and when it succeeds, fails or is skipped, straight from the scheduler process. PingPong learns the task's cron expression and timezone from the Check-in, so there is nothing to set up per task. A tick carries a hash of the schedule, and the full task list whenever that hash changes or PingPong asks for it again, so a task removed from the code can be archived in PingPong.
A task can set its own limits where it is scheduled, in minutes:
Schedule::command('backup:run --only-db') ->dailyAt('03:00') ->pingpong(maxRuntime: 240, grace: 10);
A task is known by its command and arguments, or by the name of a closure or job. Closures need a name (->name('prune-exports')); without one they are skipped with a log line. Tasks that run more than once a minute are ignored.
Inventory. The first tick, and every tick after the PHP version, the Laravel version or composer.lock changed, carries the versions and every package the lock pins. PingPong keeps that list per server. When nothing changed, a tick carries only a hash of it.
The payloads are described in docs/payloads.md.
Testing
composer test
composer analyse
composer format
Changelog
Please see CHANGELOG for more information on what has changed recently.
Credits
License
The MIT License (MIT). Please see License File for more information.