--- name: cronradar description: Cron-only monitoring with framework auto-discovery — decorate a job and CronRadar registers it, watches its schedule, and alerts when a run fails or is missed. Use when integrating CronRadar or discussing its HTTP API. homepage: https://cronradar.com metadata: requires: bins: [curl] env: [CRONRADAR_API_KEY] --- # CronRadar Documentation Cron monitoring with framework auto-discovery. Decorate a job — CronRadar registers it, watches its schedule, and alerts when something fails or misses a run. Quickstart SDKs ## Quickstart If your scheduler is Hangfire, Quartz, Celery, or Laravel, install the matching SDK and add one line — every recurring job is monitored. Everything else hits the HTTP API directly. ```csharp services.AddHangfire(config => config.MonitorAll(apiKey: "ck_app_demo_key") ); ``` ```python from cronradar.celery import monitor @celery.task @monitor def send_emails(): pass ``` ```php Schedule::command('emails:send') ->hourly() ->monitor(); ``` ```bash curl 'https://cron.life/ping/daily-backup/ck_app_demo_key?schedule=0%202%20*%20*%20*' ``` The first ping with `?schedule=` self-registers the monitor — no dashboard setup. The [full walkthrough](/getting-started) covers integration, authentication, verification, and alerts. --- # Getting Started Wire CronRadar into your scheduler in under a minute. The fastest path is the framework SDK matching your runtime — drop in a single line and every recurring job is monitored. --- ## Prerequisites 1. A CronRadar account — sign in at [app.cronradar.com](https://app.cronradar.com) with GitHub, Google, or Microsoft. 2. An **Application** in the dashboard. Applications group related monitors (e.g. *Production*, *Staging*). Each has its own API key. 3. The API key — format `ck_app_xxxxx`. Copy it once; CronRadar stores a hash and cannot show it again. **Treat API keys like passwords.** They go in environment variables, not source code. CronRadar can revoke and reissue them, but it can never recover the original. --- ## Pick your integration | Your scheduler | Use this | |---|---| | Hangfire | [`CronRadar.Hangfire`](#framework) — `MonitorAll()` | | Quartz.NET | [`CronRadar.Quartz`](#framework) — `MonitorAll()` | | Celery | [`cronradar`](#framework) — `@monitor` decorator | | Laravel scheduler | [`cronradar/laravel`](#framework) — `->monitor()` | | Bull / BullMQ | base [`cronradar`](#base) (Node) — extension in flight | | APScheduler | base [`cronradar`](#base) (Python) | | Plain crontab / shell | [`curl`](#base) directly | | Anything else | base SDK in your language → HTTP API | If your scheduler is in the top half, **use the framework SDK** — auto-discovery does the work. If it's in the bottom half, use a base SDK or curl. --- ## Framework SDK quickstart Install the matching package and add one line. Every recurring job is now monitored. Opt out per-job with the language-idiomatic skip annotation. ```csharp // dotnet add package CronRadar.Hangfire services.AddHangfire(config => config.MonitorAll(apiKey: "ck_app_demo_key") ); [SkipMonitor] public void InternalMaintenance() { // not monitored } ``` ```csharp // dotnet add package CronRadar.Quartz await scheduler.MonitorAll("ck_app_demo_key"); // to skip a specific job: [DisallowConcurrentExecution] [SkipMonitor] public class InternalJob : IJob { ... } ``` ```python # pip install cronradar from cronradar.celery import monitor, skip_monitor @celery.task @monitor def send_emails(): pass @celery.task @skip_monitor def internal_maintenance(): pass ``` ```php // composer require cronradar/laravel // app/Console/Kernel.php Schedule::command('emails:send') ->hourly() ->monitor(); Schedule::command('internal:cleanup') ->hourly() ->skipMonitor(); ``` The SDK pulls `CRONRADAR_API_KEY` from the environment. Pass it explicitly to `MonitorAll(apiKey: ...)` or set it as an env var — your call. --- ## Base SDK / curl quickstart For schedulers we don't have a framework extension for. The first ping with `?schedule=` self-registers the monitor. ```bash 0 2 * * * /path/to/backup.sh && \ curl 'https://cron.life/ping/daily-backup/ck_app_demo_key?schedule=0%202%20*%20*%20*' ``` ```javascript // npm install cronradar const cronradar = require('cronradar'); await cronradar.monitor('daily-backup', { schedule: '0 2 * * *' }); ``` ```python # pip install cronradar cronradar.monitor('daily-backup', schedule='0 2 * * *') ``` ```csharp // dotnet add package CronRadar CronRadar.Monitor("daily-backup", schedule: "0 2 * * *"); ``` ```php // composer require cronradar/php use CronRadar\CronRadar; CronRadar::monitor('daily-backup', '0 2 * * *'); ``` For lifecycle tracking (start / complete / fail), see the [Lifecycle reference](/lifecycle). --- ## Authentication Every request authenticates with your application's API key, format `ck_app_{appId}_{secret}`. There are two methods. **URL-based (recommended)** — key in the path, no headers. Best for cron one-liners: ```bash curl https://cron.life/ping/daily-backup/ck_app_demo_key curl https://cron.life/ping/daily-backup/ck_app_demo_key/start curl https://cron.life/ping/daily-backup/ck_app_demo_key/complete ``` **Basic Auth (alternate)** — key as the username, empty password. Required for the [bulk sync](/sync) endpoint, which sends a POST body: ```bash curl -u ck_app_demo_key: https://cron.life/api/sync ``` API keys are hashed with SHA256 and shown once — CronRadar can revoke and reissue, but never recover them. Keep them in environment variables, never in source control, and use a separate key per environment. --- ## Verify it works After the first run: - The monitor appears in your dashboard with status `pending` until the first ping arrives, then transitions to `healthy`. - Recent pings are listed with timestamps and durations. - If you provided `?schedule=`, the **next expected ping** is calculated and shown. - You can trigger a test ping manually: ```bash curl 'https://cron.life/ping/your-job/ck_app_demo_key' ``` If the dashboard shows the ping within a second or two, you're good. --- ## Alerts Email is on by default for everyone on the team. Add channels in **Application Settings → Notifications**: - **Slack** — webhook URL or OAuth integration - **Discord** — webhook URL - **PagerDuty** — Events API v2 integration key - **Webhook** — generic JSON POST to your endpoint - **Email** — already on; add per-member preferences if needed CronRadar sends alerts when: - A monitor misses its expected ping plus grace period (`InitialFailure`) - A failure persists past escalation thresholds (`ContinuedFailure`) - A monitor was registered but never received its first ping (`NeverExecuted`) - A previously failing monitor recovers (`Recovered`) --- ## Troubleshooting ### `401 Unauthorized` API key is wrong or missing. The URL-path format is `/ping/{monitor_key}/{api_key}` — confirm you have both segments. ```bash curl https://cron.life/ping/your-job/ck_app_demo_key ``` ### `404 Monitor Not Found` The monitor doesn't exist yet and you didn't include `?schedule=`. Add it once to auto-create: ```bash curl 'https://cron.life/ping/your-job/ck_app_demo_key?schedule=0%202%20*%20*%20*' ``` After the first ping, you can drop the `schedule` parameter on subsequent calls. ### How do I URL-encode a cron schedule? Spaces become `%20`. The SDKs handle this automatically; it only matters for hand-written `curl`. Full encoding table in the [Ping reference](/ping#self-healing-ping). ### Can I test without waiting for the schedule? Yes — fire a ping manually with `curl`. The dashboard updates within seconds. The next expected ping recalculates from your provided schedule. --- # Ping API Record cron job executions with simple HTTP pings. Monitors auto-register when you include schedule parameters, enabling self-healing monitoring without manual setup. --- ## Basic Ping The simplest way to monitor a job: send an HTTP ping when it executes successfully. ### Endpoint ``` GET /ping/{monitor_key}/{api_key} POST /ping/{monitor_key}/{api_key} ``` Both GET and POST are supported. Use whichever is more convenient for your setup. ```bash # GET request (simplest) curl https://cron.life/ping/daily-backup/ck_app_demo_key # POST request curl -X POST https://cron.life/ping/daily-backup/ck_app_demo_key # In crontab: run script, then ping on success 0 2 * * * /path/to/backup.sh && curl https://cron.life/ping/backup/ck_app_demo_key ``` ```json { "status": "ok", "monitor": "daily-backup", "pingedAt": "2025-01-15T02:00:00Z" } ``` --- ## Self-Healing Ping (Auto-Register) Include schedule and grace period parameters to auto-create monitors on first ping. If the monitor doesn't exist, it's created automatically. If it was deleted, it's recreated. ### Endpoint ``` GET /ping/{monitor_key}/{api_key}?schedule={cron_expression}&gracePeriod={seconds} ``` ### Parameters | Parameter | Type | Description | | --- | --- | --- | | `schedule` | string | Cron expression (URL-encoded). Example: `0%202%20*%20*%20*` | | `gracePeriod` | integer | Grace period in seconds. Default: 60. Prevents false alerts from minor delays. | | `name` | string | Optional human-readable name. Default: generated from monitor_key | **How Self-Healing Works:** 1. First ping with `schedule` parameter → Monitor created automatically 2. Subsequent pings → Schedule parameter no longer required 3. If monitor deleted → Next ping with schedule recreates it ```bash # Daily at 2 AM with 5-minute grace period curl 'https://cron.life/ping/daily-backup/ck_app_demo_key?schedule=0%202%20*%20*%20*&gracePeriod=300' # Every 15 minutes with default 60s grace period curl 'https://cron.life/ping/sync-data/ck_app_demo_key?schedule=*/15%20*%20*%20*%20*' # Hourly with custom name curl 'https://cron.life/ping/report-gen/ck_app_demo_key?schedule=0%20*%20*%20*%20*&name=Generate%20Reports' ``` #### URL Encoding Reference | Cron Expression | URL-Encoded | | --- | --- | | `0 2 * * *` | `0%202%20*%20*%20*` | | `*/15 * * * *` | `*/15%20*%20*%20*%20*` | | `0 */6 * * *` | `0%20*/6%20*%20*%20*` | --- ## Monitor Keys Monitor keys identify your jobs and appear in URLs. Choose clear, descriptive keys. ### Key Guidelines - **Use lowercase:** `daily-backup` not `Daily-Backup` - **Use hyphens or underscores:** `send-emails` or `send_emails` - **Be descriptive:** `backup-prod-db` not `job1` - **Alphanumeric only:** Letters, numbers, hyphens, underscores ### Good Examples ``` backup-production-database process-payment-queue send-weekly-reports sync-user-data cleanup-temp-files ``` ### Avoid ``` job1 cronjob test my job (spaces) Backup-Job (uppercase) ``` --- ## Supported Cron Formats CronRadar supports multiple cron expression formats: ### Standard Cron (5 fields) ```bash # ┌───────────── minute (0 - 59) # │ ┌───────────── hour (0 - 23) # │ │ ┌───────────── day of month (1 - 31) # │ │ │ ┌───────────── month (1 - 12) # │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday) # │ │ │ │ │ # * * * * * 0 2 * * * # Daily at 2:00 AM 0 */6 * * * # Every 6 hours */15 * * * * # Every 15 minutes 0 0 1 * * # First day of every month 0 0 * * 0 # Every Sunday at midnight ``` ### Extended Cron (6 fields, with seconds) ```bash # ┌────── second (0 - 59) # │ ┌────── minute (0 - 59) # │ │ ┌───── hour (0 - 23) # │ │ │ ┌──── day of month (1 - 31) # │ │ │ │ ┌─── month (1 - 12) # │ │ │ │ │ ┌── day of week (0 - 6) # │ │ │ │ │ │ # * * * * * * 0 0 2 * * * # Daily at 2:00:00 AM */30 * * * * * # Every 30 seconds ``` ### Special Strings ``` @hourly # 0 * * * * @daily # 0 0 * * * @weekly # 0 0 * * 0 @monthly # 0 0 1 * * @yearly # 0 0 1 1 * ``` --- ## Error Responses ### 404 Monitor Not Found Returned when monitor doesn't exist and no schedule parameter provided. ```json { "error": "MONITOR_NOT_FOUND", "message": "Monitor 'backup-job' not found. Provide schedule parameter for auto-registration." } ``` **Fix:** Add `?schedule=...` to your ping URL. ### 400 Invalid Schedule Returned when the cron expression is malformed or invalid. ```json { "error": "INVALID_SCHEDULE", "message": "Invalid cron expression: '0 25 * * *' (hour must be 0-23)" } ``` ### 401 Unauthorized Returned when API key authentication fails. ```json { "error": "AUTHENTICATION_REQUIRED", "message": "API key authentication is required" } ``` **Fix:** Ensure your API key is correct in the URL. See [Authentication](/getting-started#authentication). --- # Lifecycle Tracking API Track the complete lifecycle of your cron jobs with start, complete, and fail endpoints. Measure execution duration, detect hung jobs, and receive immediate alerts on failures. --- ## Why Lifecycle Tracking? Instead of just pinging when a job completes, lifecycle tracking gives you visibility into: - **Execution Duration:** How long does each job take? - **Hung Jobs:** Detect jobs that start but never complete - **Immediate Failure Alerts:** No grace period on explicit failures - **Error Messages:** Capture and display failure reasons --- ## Start Job Record when a job begins execution. Useful for detecting jobs that start but never complete. ### Endpoint ``` POST /ping/{monitor_key}/{api_key}/start ``` ```bash #!/bin/bash # Signal job start curl https://cron.life/ping/backup-job/ck_app_demo_key/start # Run your job /path/to/backup.sh # Signal completion (see below) curl https://cron.life/ping/backup-job/ck_app_demo_key/complete ``` ```json { "status": "started", "monitor": "backup-job", "startedAt": "2025-01-15T02:00:00Z" } ``` --- ## Complete Job Record successful job completion. Calculates execution duration if preceded by a start ping. ### Endpoint ``` POST /ping/{monitor_key}/{api_key}/complete ``` Duration is returned in seconds. In the example response, the job took 15 minutes and 30 seconds (930 seconds). ```bash curl https://cron.life/ping/backup-job/ck_app_demo_key/complete ``` ```json { "status": "completed", "monitor": "backup-job", "completedAt": "2025-01-15T02:15:30Z", "duration": 930 } ``` --- ## Fail Job Record job failure with optional error message. Triggers immediate alert (no grace period). ### Endpoint ``` POST /ping/{monitor_key}/{api_key}/fail?message={error_message} ``` ### Parameters | Parameter | Type | Description | | --- | --- | --- | | `message` | string | Optional error message shown in alerts and dashboard | **Immediate Alerts:** Failure pings trigger alerts immediately without waiting for the grace period. You're notified the moment a job fails, not when it misses its next scheduled run. ```bash # Without error message curl https://cron.life/ping/backup-job/ck_app_demo_key/fail # With error message curl 'https://cron.life/ping/backup-job/ck_app_demo_key/fail?message=Database%20connection%20timeout' # With detailed error curl 'https://cron.life/ping/backup-job/ck_app_demo_key/fail?message=Failed%20at%20step%203:%20disk%20full' ``` ```json { "status": "failed", "monitor": "backup-job", "failedAt": "2025-01-15T02:10:00Z", "message": "Database connection timeout" } ``` --- ## Benefits Over Simple Ping | Feature | Simple Ping | Lifecycle | | --- | --- | --- | | Execution Duration | Not tracked | Calculated automatically | | Hung Job Detection | No visibility | Detect started but not completed | | Failure Alerts | Wait for grace period | Immediate on /fail | | Error Messages | Not captured | Shown in dashboard & alerts | --- ## When to Use Lifecycle Tracking ### Use Lifecycle When: - Jobs have complex error handling - Execution duration matters (performance monitoring) - Jobs can hang or run indefinitely - You need immediate failure notifications - Debugging requires error context ### Use Simple Ping When: - Jobs are simple and rarely fail - You only care about "did it run?" - Minimal integration effort is priority - One-line monitoring is sufficient --- ## Error Responses Lifecycle endpoints fail the same way as ping. A failed lifecycle call never throws inside your job — the SDKs swallow it and log to stderr — but when calling the HTTP API directly, handle these: ### 404 Monitor Not Found Returned when you call `/start`, `/complete`, or `/fail` on a monitor that was never registered. ```json { "error": "MONITOR_NOT_FOUND", "message": "Monitor 'backup-job' not found. Provide schedule parameter for auto-registration." } ``` **Fix:** Send one ping with `?schedule=` first (see [self-healing ping](/ping#self-healing-ping)) or register via [bulk sync](/sync), then resume lifecycle calls. ### 401 Unauthorized Returned when API key authentication fails. ```json { "error": "AUTHENTICATION_REQUIRED", "message": "API key authentication is required" } ``` **Fix:** Confirm the `/ping/{monitor_key}/{api_key}/...` path includes a valid key. See [Authentication](/getting-started#authentication). ### 429 Rate Limit Exceeded Returned when the API rate limit (1000 requests per minute) is exceeded. ```json { "error": "RATE_LIMIT_EXCEEDED", "message": "API rate limit exceeded", "retry_after": 45 } ``` **Fix:** Back off for `retry_after` seconds. Lifecycle calls are once per run, so this only happens with very high job frequency. --- # Bulk Sync API Pre-register multiple monitors at once with the sync endpoint. Essential for framework integrations that auto-discover recurring jobs (Hangfire, Laravel, Celery). --- ## What is Sync? The sync endpoint allows you to register multiple monitors in a single API call. This is used by: - **Framework integrations:** Hangfire, Quartz.NET, Laravel, Celery auto-discover jobs and sync them on app startup - **CI/CD pipelines:** Register all scheduled jobs during deployment - **Dynamic job systems:** Programmatically manage monitors based on configuration **Why Use Sync?** Instead of waiting for jobs to execute and ping individually, sync makes all monitors visible in your dashboard immediately. Great UX for users expecting to see their jobs listed. --- ## Endpoint ``` POST /api/sync ``` Content-Type: `application/json` ### Authentication Use any of the supported authentication methods (see [Authentication](/getting-started#authentication)). Basic Auth is recommended for sync because the request carries a JSON body. ```bash curl -X POST -u ck_app_demo_key: \ -H "Content-Type: application/json" \ https://cron.life/api/sync \ -d @monitors.json ``` --- ## Request Format ### Request Fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `source` | string | Yes | Source identifier (e.g., "hangfire", "laravel", "celery", "manual") | | `monitors` | array | Yes | Array of monitor objects (max 100 per request) | ### Monitor Object Fields | Field | Type | Required | Description | | --- | --- | --- | --- | | `key` | string | Yes | Monitor key (lowercase, alphanumeric, hyphens, underscores) | | `name` | string | No | Human-readable name (auto-generated from key if not provided) | | `schedule` | string | Yes | Cron expression (5 or 6 fields, or special strings like @hourly) | | `gracePeriod` | integer | No | Grace period in seconds (default: 60) | ```json { "source": "hangfire", "monitors": [ { "key": "daily-backup", "name": "Daily Database Backup", "schedule": "0 2 * * *", "gracePeriod": 300 }, { "key": "hourly-sync", "name": "Hourly Data Sync", "schedule": "0 * * * *", "gracePeriod": 60 }, { "key": "process-payments", "name": "Process Payment Queue", "schedule": "*/15 * * * *", "gracePeriod": 120 } ] } ``` --- ## Response ### Response Fields | Field | Description | | --- | --- | | `synced` | Total monitors successfully synced | | `created` | Number of new monitors created | | `updated` | Number of existing monitors updated | | `errors` | Array of error objects for failed monitors | If some monitors fail validation, sync still processes the valid ones and returns `status: "partial"` with the failures listed in `errors`. ```json { "status": "success", "synced": 3, "created": 2, "updated": 1, "errors": [] } ``` ```json { "status": "partial", "synced": 2, "created": 2, "updated": 0, "errors": [ { "key": "invalid-job", "error": "Invalid cron expression: '0 25 * * *' (hour must be 0-23)" } ] } ``` --- ## Code Examples ```bash curl -X POST -u ck_app_demo_key: \ -H "Content-Type: application/json" \ https://cron.life/api/sync \ -d '{ "source": "manual", "monitors": [ { "key": "backup-prod", "name": "Production Backup", "schedule": "0 2 * * *", "gracePeriod": 300 } ] }' ``` ```csharp using System.Net.Http; using System.Text; using System.Text.Json; var apiKey = Environment.GetEnvironmentVariable("CRONRADAR_API_KEY"); var auth = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{apiKey}:")); var payload = new { source = "hangfire", monitors = new[] { new { key = "daily-backup", name = "Daily Backup", schedule = "0 2 * * *", gracePeriod = 300 }, new { key = "hourly-sync", name = "Hourly Sync", schedule = "0 * * * *", gracePeriod = 60 } } }; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth); var response = await client.PostAsync( "https://cron.life/api/sync", new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") ); var result = await response.Content.ReadAsStringAsync(); Console.WriteLine(result); ``` ```python api_key = os.getenv('CRONRADAR_API_KEY') payload = { "source": "celery", "monitors": [ { "key": "process-payments", "name": "Process Payments", "schedule": "*/15 * * * *", "gracePeriod": 120 }, { "key": "send-emails", "name": "Send Email Queue", "schedule": "*/5 * * * *", "gracePeriod": 60 } ] } response = requests.post( 'https://cron.life/api/sync', auth=(api_key, ''), json=payload ) print(response.json()) ``` ```javascript const fetch = require('node-fetch'); const apiKey = process.env.CRONRADAR_API_KEY; const auth = Buffer.from(`${apiKey}:`).toString('base64'); const payload = { source: 'nodejs', monitors: [ { key: 'cleanup-logs', name: 'Cleanup Old Logs', schedule: '0 0 * * *', gracePeriod: 300 } ] }; const response = await fetch('https://cron.life/api/sync', { method: 'POST', headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await response.json(); console.log(result); ``` --- ## Idempotency Sync is fully idempotent - safe to call multiple times: - **First call:** Creates all monitors (e.g., 5 created, 0 updated) - **Subsequent calls:** Updates existing monitors (e.g., 0 created, 5 updated) - **Changed schedules:** Updates take effect immediately - **Removed monitors:** Not deleted (manual deletion only) **Safe for App Startup:** Framework integrations call sync on every app startup. This ensures monitors are always up-to-date with your job definitions, even after deploys or config changes. --- ## Rate Limits - **Sync endpoint:** 10 requests per minute per API key - **Max monitors per request:** 100 - **Recommended frequency:** Once per app startup (not in a loop) Need higher limits? Contact [support@cronradar.com](mailto:support@cronradar.com). --- ## Best Practices ### 1. Use Descriptive Source IDs Source IDs help you filter monitors in the dashboard: ``` "source": "hangfire-production" ✓ Good "source": "hangfire-staging" ✓ Good "source": "manual" ✓ Good "source": "app" ✗ Too vague ``` ### 2. Normalize Keys Always convert job IDs to lowercase, replace spaces with hyphens: ```javascript // Good normalization "Send Welcome Email" → "send-welcome-email" "Backup_DB" → "backup-db" "ProcessPayments" → "process-payments" ``` ### 3. Generate Readable Names If keys are machine-readable, provide human-friendly names: ```json { "key": "send-welcome-email", "name": "Send Welcome Email" } ``` ### 4. Call on App Startup Framework integrations should call sync during application initialization: ```csharp // .NET Hangfire example services.AddHangfire(config => { config.UseStorage(storage); config.MonitorAll("ck_app_demo_key"); // Calls sync internally }); ```