Search by

kibatic / cronitoring-bundle

jcrombez

Ping a healthchecks.io-compatible instance and sync check schedules for Symfony Scheduler cron tasks.

Package info

github.com/kibatic/cronitoring-bundle

Type:symfony-bundle

pkg:composer/kibatic/cronitoring-bundle

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-09-15 08:43 UTC

This package is auto-updated.

Last update: 2026-09-15 08:43:42 UTC


README

Lire en français : README.fr.md.

Integrates a healthchecks.io-compatible cron monitoring instance (whether the official service or a self-hosted instance, designated via base_url) into a Symfony application using the Scheduler component: pings the start/success/failure/log/exit-code of every scheduled run, and automatically syncs the corresponding check's schedule (schedule, timezone, grace, tags).

Requirements

  • PHP >= 8.5
  • Symfony 8.x
  • symfony/http-client, with an HTTP client enabled (framework.http_client)

Installation

composer require kibatic/cronitoring-bundle

Configuration

# config/packages/cronitoring.yaml
cronitoring:
    base_url: 'https://cron.example.com'          # required — healthchecks.io-compatible instance
    #                                              # (self-hosted or not) to use
    ping_key: '%env(CRONITORING_PING_KEY)%'       # ping-key of the project on the target instance
    api_key: '%env(CRONITORING_API_KEY)%'         # (read-write) API key for the same project
    slug_prefix: '%env(CRONITORING_SLUG_PREFIX)%' # slug prefix, to distinguish installations

base_url is required: this bundle points to no instance by default, each consuming project must explicitly declare which one it wants to use. ping_key, api_key and slug_prefix remain optional. As long as ping_key and api_key are not set, the bundle makes no network call at all; slug_prefix defaults to an empty string, which is a perfectly valid configuration (it never gates any network call).

Usage

use Kibatic\CronitoringBundle\Attribute\AsMonitoredCronTask;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;

#[AsCommand(name: 'app:my-task')]
#[AsMonitoredCronTask('*/5 * * * *', slug: 'my-task')]
class MyTaskCommand extends Command
{
    // ...
}

The slug is optional: when omitted, it is derived from the command's name and its arguments.

#[AsMonitoredCronTask] is only honored on a Console command class (#[AsCommand]): placed on an invokable service scheduled via #[AsPeriodicTask] or a service method call, it silently has no effect.

For any of this to actually run, a messenger:consume <schedule-transport-name> worker (e.g. scheduler_default) must be running: this bundle only wires the listener, but it's Symfony's Scheduler itself that requires an active consumer.

Making the cron expression configurable via an environment variable

#[AsMonitoredCronTask] extends the Scheduler component's #[AsCronTask]: its arguments are evaluated by PHP itself, at compile time, as a constant expression (literals, new, class constants...). The PHP engine has no notion of Symfony container parameters, so #[AsMonitoredCronTask('%env(MY_CRON)%')] does not work: %env(...)% is only interpreted in config files (YAML/XML/PHP) when the container is built, never in an attribute. Written as-is, it would just be a literal string sent to Cron\CronExpression, which would fail.

To make a schedule configurable, it therefore has to be built by hand in the application's ScheduleProviderInterface (#[AsSchedule]), injecting the env var through regular autowiring — a service constructor, unlike an attribute, does resolve %env(...)%:

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Messenger\Message\RunCommandMessage;
use Symfony\Component\Scheduler\Attribute\AsSchedule;
use Symfony\Component\Scheduler\RecurringMessage;
use Symfony\Component\Scheduler\Schedule as SymfonySchedule;
use Symfony\Component\Scheduler\ScheduleProviderInterface;

#[AsSchedule]
class Schedule implements ScheduleProviderInterface
{
    public function __construct(
        #[Autowire('%env(MY_TASK_CRON)%')] private string $myTaskCron,
    ) {
    }

    public function getSchedule(): SymfonySchedule
    {
        return (new SymfonySchedule())
            ->add(RecurringMessage::cron(
                $this->myTaskCron,
                new RunCommandMessage('app:my-task'),
            ));
    }
}

In that case, remove #[AsMonitoredCronTask(...)] from the command class: otherwise it would be scheduled twice (once via the attribute, once via the manual addition). Cronitoring monitoring (ping + sync) stays active: this bundle's listener reacts to any RunCommandMessage processed by the Scheduler, whether it was declared via attribute or added programmatically — only slug resolution differs (derived from the command name in both cases, since there is no attribute to read).

Tasks declared via #[AsMonitoredCronTask] elsewhere in the code keep showing up automatically in the final schedule, whatever getSchedule() does otherwise: it's AddScheduleMessengerPass, the Scheduler component's compiler pass, that adds them there.