cron-monitor/php-sdk
最新稳定版本:v1.0.0
Composer 安装命令:
composer require cron-monitor/php-sdk
包简介
PHP SDK for cronheart.com (cron-monitor) with first-class Symfony Scheduler and Laravel Scheduler support.
关键字:
README 文档
README
Composer SDK for cronheart.com — heartbeat monitoring for scheduled jobs. Get pinged when a cron / systemd timer / scheduler entry stops checking in on time. First-class support for Symfony Scheduler and the Laravel scheduler.
Why
Uptime monitors don't catch the silent failure mode: a backup that stopped running a month ago, an invoice job that didn't fire on the 1st, an ETL pipeline whose systemd timer was renamed. cronheart's per-job dead-man switch does. This SDK takes the boilerplate out of wiring it up.
What's in the box
- Drop-in Symfony bundle — auto-registered;
bin/console cron-monitor:syncinventories SchedulerRecurringMessageproviders and console commands. - Drop-in Laravel package — auto-discovered service provider;
Schedule::command(...)->monitor()macro andMonitorQueueJobmiddleware forShouldQueuejobs. #[Monitor(uuid: ...)]attribute — the UUID lives on the command class instead of being duplicated in YAML / config. Works on both Symfony Console and Laravel Artisan commands.- Zero extra dependencies — bundled cURL PSR-18 transport and
nyholm/psr7factories. Bring Guzzle orsymfony/http-clientif you want connection pooling; otherwisecomposer requireis enough. - Never breaks the host job — every network / HTTP error becomes a
PingResult::failed(...)return value. A broken cron-monitor backend cannot punish your scheduler.
Install
composer require cron-monitor/php-sdk
PHP ≥ 8.2 with ext-curl (the bundled cURL transport uses it; almost every
PHP install has it on).
composer require is the only step. The SDK ships with its own minimal cURL
PSR-18 transport plus the PSR-17 factories from the bundled nyholm/psr7 —
no Guzzle / symfony/http-client required.
- Symfony bundle path — drop-in. The bundle prefers
symfony/http-client'sPsr18Clientif present and falls back to the bundled cURL transport otherwise. Your own PSR-17 / PSR-18 bindings always win. - Laravel path — auto-discovered. The service provider uses bindings
from the container when present and falls back to the bundled cURL
transport +
nyholm/psr7factories. - Framework-agnostic path (plain PHP / Slim / etc.) — use
CronMonitorClient::create()(see Quick start). Want to plug in Guzzle or symfony/http-client? ConstructCronMonitorClientdirectly and pass your PSR-18 client + PSR-17 factory.
Quick start (framework-agnostic)
use CronMonitor\Client\CronMonitorClient; CronMonitorClient::create()->success('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx');
Wrap a long-running job:
use CronMonitor\Client\CronMonitorClient; $client = CronMonitorClient::create(); $uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; $client->start($uuid); try { runMyImportantJob(); $client->success($uuid); } catch (\Throwable $e) { $client->fail($uuid, $e->getMessage()); throw $e; }
The client never throws on network or HTTP errors — a broken cron-monitor backend will not break your job.
For a custom endpoint (staging / preview), tighter timeout, or API key:
use CronMonitor\Client\Configuration; use CronMonitor\Client\CronMonitorClient; $client = CronMonitorClient::create( new Configuration( endpoint: 'https://staging.cronheart.com', timeoutSeconds: 3.0, retries: 2, apiKey: getenv('CRON_MONITOR_API_KEY') ?: null, ), );
Symfony Scheduler integration
Register the bundle (Flex usually does this for you):
// config/bundles.php return [ // ... CronMonitor\Bridge\Symfony\CronMonitorBundle::class => ['all' => true], ];
Configure:
# config/packages/cron_monitor.yaml cron_monitor: endpoint: '%env(CRON_MONITOR_ENDPOINT)%' # optional, defaults to SaaS timeout_seconds: 5.0 retries: 1 api_key: '%env(CRON_MONITOR_API_KEY)%' # optional messages: # FQCN of any message your Scheduler dispatches via Messenger. # The middleware ships start/success/fail pings for each one. App\Scheduler\Message\NightlyReportRun: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
Discover the FQCNs the bundle can see:
php bin/console cron-monitor:sync
It prints every RecurringMessage from every tagged
scheduler.schedule_provider, plus a YAML snippet you can paste into the
config above.
Plain bin/console commands
If your cron entry is a console command rather than a Scheduler run — e.g.
* * * * * php bin/console app:reports:nightly straight out of crontab —
either map the command name in YAML or declare the monitor UUID right
on the command class, and the bundle wraps every invocation in
start/success/fail pings via a kernel event subscriber.
YAML map (good for "configuration as deploy artefact" workflows):
cron_monitor: commands: 'app:reports:nightly': 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
Class attribute (good for "the UUID lives next to the code"):
use CronMonitor\Attribute\Monitor; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; #[AsCommand(name: 'app:reports:nightly')] #[Monitor(env: 'CRON_MONITOR_REPORTS_NIGHTLY_UUID')] final class GenerateNightlyReportCommand extends Command { // ... }
Two attribute forms are supported:
#[Monitor(env: 'VAR_NAME')]— recommended for production. The attribute carries the env-var name; the SDK resolves the value at runtime via$_ENV→$_SERVER→getenv(). The UUID — a write capability secret — stays out of git history and rotates without a redeploy.#[Monitor(uuid: '...literal...')]— convenient for local dev or one-off scripts where the UUID is not sensitive.
(#[Monitor(uuid: getenv('VAR'))] is a PHP parse error — attribute
arguments must be compile-time constants — and '%env(VAR)%' is not
expanded inside attribute payloads. The two-parameter shape is the
intended way to thread env-sourced UUIDs into the attribute path.)
Both attribute sources are honoured, and YAML wins on conflict —
so you can override either form per environment in YAML (e.g.
'app:reports:nightly': '%env(MY_UUID)%' with MY_UUID blank in dev
to suppress monitoring without touching the attribute). A missing or
empty env var is itself treated as deliberate suppression — same
policy as an empty YAML map entry.
No code changes inside the command body. A non-zero exit fires fail; an
uncaught throwable fires fail with the exception class, message, and
file:line in the body so the cron-monitor dashboard shows the immediate
cause without you tailing logs.
Laravel scheduler integration
The service provider is auto-discovered. Publish the config:
php artisan vendor:publish --tag=cron-monitor-config
Then in routes/console.php, either pass the UUID at the call site
or declare it on the command class and call ->monitor() without
arguments:
use Illuminate\Support\Facades\Schedule; // Explicit UUID Schedule::command('reports:nightly') ->dailyAt('02:00') ->monitor('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'); // UUID lives on the command class use App\Console\Commands\GenerateNightlyReportCommand; Schedule::command(GenerateNightlyReportCommand::class) ->dailyAt('02:00') ->monitor();
For the no-arg form, the macro reads the #[Monitor] attribute on the
Artisan command class behind the scheduled event. The attribute has
two construction forms — env-sourced (recommended for production) and
literal:
use CronMonitor\Attribute\Monitor; use Illuminate\Console\Command; // Recommended for production — UUID stays out of git history. #[Monitor(env: 'CRON_MONITOR_REPORTS_NIGHTLY_UUID')] final class GenerateNightlyReportCommand extends Command { protected $signature = 'reports:nightly'; // ... } // Or with a literal UUID — fine for local dev / one-off scripts. #[Monitor(uuid: 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')]
The env-sourced form resolves through $_ENV → $_SERVER →
getenv() so it works across CLI, FPM, and container-injected env
setups. A missing or empty env var is treated as deliberate
suppression for the current environment.
Precedence rule: an explicit string argument to ->monitor(...) always
wins over the attribute, and an empty string (->monitor(env('MY_UUID', '')))
is treated as explicit suppression for the current environment. The
->monitor(...) macro hooks before / onSuccess / onFailure so you
get start/success/fail pings on the same boundary as the job execution.
php artisan cron-monitor:sync lists every scheduled command and emits a
config/cron-monitor.php snippet.
Queued jobs
For ShouldQueue jobs dispatched outside the scheduler, attach the bundled
job middleware:
use CronMonitor\Bridge\Laravel\Queue\MonitorQueueJob; class GenerateNightlyReport implements ShouldQueue { public function middleware(): array { return [MonitorQueueJob::withUuid('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')]; } }
Each invocation pings start, then success on completion or fail on a
thrown exception (with the class, message, and file:line in the body). The
underlying SDK swallows its own failures, so a flaky cron-monitor backend
never breaks the queued job.
Standalone CLI
For plain cron / systemd timer users, the vendor/bin/cron-monitor
binary doesn't require any framework:
# Heartbeat from a one-liner cron entry * * * * * /opt/scripts/run.sh && cron-monitor heartbeat $UUID # Wrap a job with start/success/fail cron-monitor start $UUID if /opt/scripts/run.sh 2> /tmp/err; then cron-monitor success $UUID else cron-monitor fail $UUID --body="$(cat /tmp/err)" fi
Set CRON_MONITOR_ENDPOINT and CRON_MONITOR_API_KEY in the environment
to avoid repeating the flags.
Managing monitors via the API
Everything above is the ping path — anonymous, never throws, never
breaks your job. Since 1.0.0 the SDK also ships an authenticated
management client for listing and creating monitors from your own
account, CronMonitor\Api\MonitorApiClient.
This client is the deliberate opposite of the ping client: it throws typed exceptions, because you call it from admin screens or CLI tooling where you want to know — and react — when something fails.
Authenticate with a Personal Access Token (cmk_…) created in the
cronheart.com dashboard (Settings → API Tokens). API access requires a
Starter plan or higher. The token rides on Configuration::apiKey:
use CronMonitor\Api\Dto\CreateMonitorRequest; use CronMonitor\Api\Dto\ScheduleKind; use CronMonitor\Api\Exception\ApiException; use CronMonitor\Api\Exception\RateLimitException; use CronMonitor\Api\Exception\ValidationException; use CronMonitor\Api\MonitorApiClient; use CronMonitor\Client\Configuration; $api = MonitorApiClient::create( Configuration::withDefaultEndpoint(apiKey: getenv('CRON_MONITOR_API_KEY') ?: null), ); // List your monitors (paginated). $page = $api->listMonitors(offset: 0, limit: 50); foreach ($page->data as $monitor) { printf("%s %s %s\n", $monitor->uuid, $monitor->status->value, $monitor->name); } // Or walk every page lazily. foreach ($api->allMonitors() as $monitor) { // ... } // Create one. try { $monitor = $api->createMonitor(new CreateMonitorRequest( name: 'Nightly report', scheduleKind: ScheduleKind::Cron, scheduleExpr: '0 2 * * *', tz: 'UTC', graceSeconds: 120, )); echo $monitor->pingUrl; } catch (ValidationException $e) { foreach ($e->errors as $field => $message) { echo "$field: $message\n"; } } catch (RateLimitException $e) { echo "Slow down; retry after {$e->retryAfter}s\n"; } catch (ApiException $e) { // Catch the base type for any other API failure (auth, plan, network…). error_log($e->getMessage()); }
In Symfony the client is autowired (MonitorApiClient); in Laravel it
is a container singleton (app(MonitorApiClient::class)). Both read the
token from the same api_key config you already set.
Configuration knobs
| Setting | Default | Notes |
|---|---|---|
endpoint |
https://cronheart.com |
Self-hosted: point at your install. |
timeout_seconds |
5.0 |
Per-request, low by design. |
retries |
1 |
Pings are idempotent server-side. |
api_key |
null |
Personal Access Token (cmk_…) for the management API; not needed for pings. |
allow_insecure_endpoint |
false |
Required for http:// endpoints. |
Security
- HTTPS is required by default. The SDK refuses to send pings to plain
HTTP unless
allow_insecure_endpoint: trueis explicitly set. - The per-monitor UUID is treated as a write credential and is validated against the canonical UUID v4 shape before being concatenated into a URL — no path traversal via the action segment.
- The
Authorization: Bearer <api_key>header is attached only when an API key is configured; nothing is sent for anonymous installs. - The API token (
api_key) is a full account credential, far more powerful than a per-monitor UUID — it can list and create every monitor on the account. Keep it in an environment variable / secrets store, never commit it, and never log it. Ping-only installs do not need it at all; leave it unset. failpings include exception text and a host file path. When a monitored handler throws, the SDK sends the exception class name,getMessage(), andfile:lineof the throw site to the cron-monitor endpoint (capped at 10 KB). Exception messages routinely embed attacker-controlled input (e.g.PDOExceptionSQL fragments, validation errors echoing user data); the file path discloses the host deployment layout. If your threat model treats either as sensitive, wrap the host job in atry/catchthat throws a sanitised exception, or callCronMonitorClient::fail($uuid, $body)directly with a curated body.
Development
composer install composer test # PHPUnit composer stan # PHPStan level 8 composer cs-check # php-cs-fixer dry-run
License
MIT — see LICENSE.
统计信息
- 总下载量: 474
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 22
- 依赖项目数: 1
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-05-14