pulse-si / symfony
Surveillance Pulse pour Symfony : signal de vie, Doctrine, Redis, Messenger et Scheduler détectés tout seuls.
Package info
github.com/Charles-Bally/pulse-symfony
Type:symfony-bundle
pkg:composer/pulse-si/symfony
Requires
- php: >=8.1
- pulse-si/php: ^0.0.2
- symfony/config: ^6.4|^7.0|^8.0
- symfony/console: ^6.4|^7.0|^8.0
- symfony/dependency-injection: ^6.4|^7.0|^8.0
- symfony/event-dispatcher: ^6.4|^7.0|^8.0
- symfony/http-kernel: ^6.4|^7.0|^8.0
Requires (Dev)
- doctrine/dbal: ^3.8|^4.0
- doctrine/doctrine-bundle: ^2.11|^3.0
- dragonmantank/cron-expression: ^3.4
- monolog/monolog: ^3.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5|^11.0|^12.0
- symfony/doctrine-messenger: ^6.4|^7.0|^8.0
- symfony/framework-bundle: ^6.4|^7.0|^8.0
- symfony/messenger: ^6.4|^7.0|^8.0
- symfony/monolog-bundle: ^3.10|^4.0
- symfony/scheduler: ^6.4|^7.0|^8.0
- symfony/yaml: ^6.4|^7.0|^8.0
Suggests
- symfony/messenger: surveillance des files
- symfony/monolog-bundle: recopie des journaux vers Pulse
- symfony/scheduler: planning et passages des tâches planifiées
Provides
None
Conflicts
None
Replaces
None
README
Ton app Symfony surveillée par Pulse, sur ton iPhone et ton Mac. Tu ajoutes le bundle, Pulse trouve tout seul tes connexions Doctrine, ton Redis, tes transports Messenger et ton Scheduler, et te prévient dès que quelque chose cloche.
ton app Symfony ton iPhone, ton Mac
┌──────────────────────────┐ HTTPS ┌──────────┐ ┌──────────────┐
│ bin/console pulse:work │ ──────────────────▶ │ Pulse │ ────▶ │ app Pulse │
│ ├ Doctrine SELECT 1 │ état, tâches, │ (API) │ push │ alertes, │
│ ├ Redis PING │ journaux │ │ ◀──── │ journaux, │
│ ├ Messenger en attente│ (chiffrés si tu │ │ SSE │ en direct │
│ └ Scheduler passages │ le veux) └──────────┘ └──────────────┘
└──────────────────────────┘
▲ var/pulse/ : journaux et passages des tâches, écrits par tes autres processus
Une requête PHP est courte : le rapport ne part donc jamais d'une requête web. Un petit worker, bin/console pulse:work, tourne à côté de ton app (comme messenger:consume) et parle à Pulse à sa place.
En deux minutes
composer require pulse-si/symfony
Ajoute le bundle s'il n'y est pas déjà :
// config/bundles.php return [ // … Pulse\Symfony\PulseBundle::class => ['all' => true], ];
Puis la clé que l'app Pulse t'affiche quand tu ajoutes une app, et lance le worker :
# .env.local
PULSE_KEY=pk_live_…
bin/console pulse:work
Le premier signal arrive dans Pulse deux secondes plus tard. Pour un essai ponctuel, bin/console pulse:report envoie un seul rapport et affiche ce qui a été mesuré.
Aucune configuration n'est obligatoire : le bundle lit les variables PULSE_*. Les mêmes réglages existent en YAML et passent avant les variables :
# config/packages/pulse.yaml pulse: send: checks,crons,logs exclude: 'File failed' log_level: WARN http_probes: 'Route /health': 'https://nimbus.fr/health'
Lancer le worker
Le worker suit le rythme dicté par l'API, s'arrête proprement sur SIGTERM et SIGINT (avec l'extension pcntl) et ne fait jamais tomber ton app.
Supervisor
[program:pulse] command=php /var/www/nimbus/bin/console pulse:work autostart=true autorestart=true stopwaitsecs=15 user=www-data
systemd
[Unit] Description=Pulse pour Nimbus After=network-online.target [Service] WorkingDirectory=/var/www/nimbus ExecStart=/usr/bin/php bin/console pulse:work Restart=always User=www-data [Install] WantedBy=multi-user.target
Docker Compose
services: pulse: image: nimbus-app command: php bin/console pulse:work env_file: .env.local restart: unless-stopped
Le worker, messenger:consume et tes requêtes partagent le dossier var/pulse/ : sur plusieurs conteneurs, monte var/ en volume commun, ou au moins ce dossier.
Ce que Pulse surveille
| Dépendance trouvée | Ce que Pulse fait | Nom affiché |
|---|---|---|
| connexions Doctrine DBAL | SELECT 1 (requête neutre de la plateforme) |
PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, Oracle |
services \Redis, Predis\ClientInterface, Relay\Relay, pools de cache sur Redis |
PING |
Redis |
| transports Messenger qui savent compter (Doctrine, Redis, SQS…) | lit le nombre de messages en attente | File async, File failed… |
Scheduler (#[AsSchedule], #[AsCronTask], #[AsPeriodicTask]) |
expression, dernier et prochain passage, réussite | le message de la tâche |
Chaque contrôle a 5 secondes pour répondre et ne fait que lire. Une dépendance en échec, une tâche qui a planté ou qui a sauté son passage de plus de 5 minutes, et l'app passe en « dégradée » dans Pulse avec une notification qui dit quoi.
Le nom d'une tâche est celui de son message (RelanceAbonnements), de la commande pour un RunCommandMessage (app:relance) ou du service pour #[AsCronTask]. Les passages sont relevés par messenger:consume scheduler_default lui-même : un handler qui lève compte comme un échec, avec son message.
Pour suivre un travail qui ne passe pas par le Scheduler (un cron système, une commande), enveloppe-le avec le service Pulse\Pulse :
use Pulse\Pulse; public function __construct(private Pulse $pulse) {} public function __invoke(): void { $this->pulse->track('relance-abonnements', fn () => $this->abonnements->relancer()); }
En cas d'erreur, l'échec est noté puis l'erreur te revient telle quelle.
Choisir ce qui part
Tu décides de ce que ton app raconte à Pulse. Le signal de vie part toujours, le reste se règle :
# Seulement l'état des dépendances et les journaux, pas les tâches ni les versions PULSE_SEND=checks,logs # Ignorer certaines dépendances ou certaines tâches (* sert de joker) PULSE_EXCLUDE=File failed,Nettoyage* # Ou au contraire, ne suivre que celles-ci PULSE_INCLUDE=PostgreSQL,Redis,RelanceAbonnements # Journaux : seulement les avertissements et les erreurs PULSE_LOG_LEVEL=WARN
Journaux et mode direct (Pro et Team)
Avec MonologBundle, le bundle déclare un handler pulse qui reçoit tous les canaux sauf event, sans rien changer à tes handlers : il passe après eux (priorité basse) et ne garde jamais un message pour lui. Le niveau et le canal sont repris, les secrets masqués avant de partir (token=***, Bearer ***, identifiants dans les URL de connexion).
app.ERROR: Paiement refusé token=abc123
│
▼ dans l'app Pulse
ERROR app Paiement refusé token=***
Le handler écrit chaque ligne nettoyée dans var/pulse/logs.ndjson (1000 lignes au plus) et le worker vide ce fichier toutes les 10 secondes. Tant que Pulse n'a pas confirmé que ton offre comprend les journaux, rien n'est écrit : sans journaux dans l'offre, ton app ne paie aucun surcoût. Si le worker s'arrête, la recopie se coupe d'elle-même quelques minutes plus tard.
Quand tu ouvres l'app dans Pulse, le serveur prévient le worker qu'on le regarde : l'état part toutes les 5 secondes et les journaux chaque seconde, puis tout revient au calme quand tu quittes l'écran. Les journaux sont gardés 3 jours en Pro et 7 jours en Team.
Mode sécurisé : Pulse ne peut rien lire
C'est l'option pour qui ne veut confier ses données à personne, nous compris. Ton iPhone ou ton Mac fabrique une paire de clés. La clé publique va dans ton .env.local, la clé privée ne quitte jamais tes appareils.
PULSE_PUBLIC_KEY=pulse_pub_… # copiée depuis l'app, Réglages de l'app > Mode sécurisé
ton app ──── rapport chiffré (HPKE, X25519 + AES-256-GCM) ────▶ Pulse ──▶ ton iPhone
│ │
garde un paquet ouvre avec
qu'il ne sait pas la clé privée
ouvrir du trousseau
Un serveur qui ne lit rien ne peut pas alerter. C'est pour ça qu'un petit résumé reste en clair par défaut : « tout va bien » ou « 2 soucis », sans nom ni message. Tu peux en laisser plus, ou rien du tout :
PULSE_CLEAR=status # par défaut : un booléen et un nombre PULSE_CLEAR=status,checks # Pulse peut dire « Redis ne répond plus » PULSE_CLEAR=none # silence complet : Pulse ne sait plus que si l'app est en vie
Le mode sécurisé demande les extensions sodium et openssl de PHP (présentes dans la plupart des installations). Sans elles, le worker refuse de démarrer plutôt que d'envoyer en clair. Une fois le mode actif, Pulse refuse tout rapport en clair : un .env oublié ne fuit pas.
Sécurité, en clair
- Ton app d'abord. Le bundle ne lève jamais d'erreur vers ton app (sauf
track, qui te rend l'erreur de ta tâche). Pulse injoignable : un seul avertissement, le worker continue. Clé refusée : les envois s'arrêtent. - Rien n'est écrit chez toi. Les contrôles ne font que lire (
SELECT 1,PING, nombre de messages), avec 5 secondes au plus. Le bundle n'écrit que dansvar/pulse/. Les transports AMQP ne sont pas comptés : les interroger déclare la file. - Rien de superflu ne part. Ni requête, ni donnée métier, ni variable d'environnement. Les messages d'erreur sont nettoyés et coupés à 300 caractères.
- La clé ne voyage qu'en HTTPS, dans un en-tête, jamais dans une URL. Elle ne sert qu'à écrire l'état de cette app : elle n'ouvre pas ton compte.
- Une seule dépendance en dehors de Symfony :
pulse-si/php, qui n'utilise que des extensions courantes de PHP.
Variables
| Variable | Défaut | Rôle |
|---|---|---|
PULSE_KEY |
(obligatoire) | clé de l'app : pk_live_… en production, pk_test_… en staging |
PULSE_SEND |
checks,crons,logs,versions |
ce qui part en plus du signal de vie |
PULSE_INCLUDE |
(tout) | seuls ces contrôles et tâches, noms séparés par des virgules, * permis |
PULSE_EXCLUDE |
(rien) | contrôles et tâches ignorés |
PULSE_LOG_LEVEL |
DEBUG |
niveau minimal des journaux envoyés |
PULSE_LOGS |
true |
false coupe les journaux |
PULSE_PUBLIC_KEY |
(aucune) | active le mode sécurisé |
PULSE_CLEAR |
status |
en mode sécurisé, ce qui reste lisible par Pulse |
PULSE_ENDPOINT |
https://api.pulse-app.si |
ton Pulse Relay en mode VPS perso (Team), par exemple https://relais.nimbus.fr |
PULSE_RELAY_FINGERPRINT |
(aucune) | empreinte du certificat d'un relais sans domaine : rien ne part si elle ne correspond pas |
PULSE_APP_VERSION |
(aucune) | version affichée dans Pulse |
PULSE_DISABLED |
false |
true coupe tout (tests, CI) |
Si rien n'arrive
| Message | Ce qu'il faut faire |
|---|---|
PULSE_KEY absente |
la variable n'est pas chargée : vérifie .env.local, ou les variables du service |
Clé Pulse refusée |
la clé a été remplacée dans l'app : recopie la nouvelle |
Mode sécurisé actif pour cette app |
ajoute PULSE_PUBLIC_KEY |
PULSE_PUBLIC_KEY ne correspond plus |
la clé a changé sur ton téléphone : recopie la clé publique |
Pulse injoignable |
réseau ou pare-feu : le worker continue, les envois reprennent seuls |
| aucune tâche planifiée | lance messenger:consume scheduler_default avec la même PULSE_KEY que le worker |
| aucun journal | MonologBundle doit être installé ; web et consommateurs doivent voir la même PULSE_KEY et le même var/pulse/ |
Le rythme des rapports suit celui de l'app dans Pulse (30 secondes en Pro, 5 minutes en Gratuit).
Licence
MIT