Instrumentation (7a) + stack local & vérification bout-en-bout (7b). Dashboards (§26.6) et alertes (§26.7) = 7c. Référence : cahier (§21, §26).
Diagnostiquer : énergie incohérente, café refusé, vote non clôturé, accessoire non appliqué, fuite mémoire worker, latence, événement Mercure non publié, séquence visuelle non initialisée (§26.1).
L'app émet en OTLP (manuel : spans/métriques/logs ; auto : Doctrine/Messenger
via l'extension PECL opentelemetry) vers un OTel Collector qui ventile :
traces → Tempo, métriques → Prometheus (scrape de l'exporter Prometheus du
Collector), logs → Loki. Grafana lit les trois, corrélation trace↔logs
provisionnée (§21). Tout reste non bloquant et sans accumulation worker
(garanties 7a, préservées par l'auto-instrumentation : mêmes pipelines globaux).
# 1. Démarrer le stack d'observabilité (réseau friday-duck-observability).
docker compose -f compose.observability.yaml up -d
# 2. Démarrer l'app en pointant son OTLP sur le Collector (port publié sur l'hôte).
OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4318 make up
# UIs : Grafana http://localhost:3000 · Prometheus http://localhost:9090Config versionnée sous observability/ (collector, tempo, prometheus, loki,
datasources Grafana).
En prod, l'image embarque l'extension PECL opentelemetry (Dockerfile) et
l'auto-instrumentation est active. Sur l'hôte de dev et en CI, l'ext est ABSENTE :
les paquets open-telemetry/opentelemetry-auto-* émettent alors un
E_USER_WARNING à l'autoload Composer — donc trop tôt pour être neutralisé via
.env* (chargés par Symfony après l'autoload). On le fait taire avec une VRAIE
variable d'environnement, OTEL_PHP_DISABLED_INSTRUMENTATIONS=all, posée :
- dans le Makefile (
export, couvremake test|qa|stan|…) ; - dans le job CI (
.github/workflows/php.yml,env:au niveau du job).
Les conteneurs Docker n'héritent pas de cette variable (elle reste sur l'hôte) :
ils gardent l'instrumentation active. En invocation directe hors make, exporter
la variable dans le shell reproduit le silence.
- Trace d'un café (Tempo). Offrir un café puis, dans Grafana → Explore →
Tempo, chercher
service.name=friday-duck: une traceHTTP POSTcouvrantcoffee.contribution.validate→energy.recalculate→coffee.contribution.persist,db.client.operation.duration(auto Doctrine). Le relais produitmercure.update.publish, enfant de la trace requête via letraceparentporté par la ligne outbox (frontière async franchie, visible). - Métriques (Prometheus).
http://localhost:9090→ requêterduck_energy,duck_coffee_total,db_client_operation_duration_*,messenger_message_processed_*,worker_memory_bytes,mercure_publish_count_total. - Logs corrélés (Loki). Depuis un span Tempo, « Logs for this span » ouvre Loki
filtré sur le
trace_id; les logs portenttrace_id/span_id(bridge Monolog). - Front (santé). Les 5 métriques
duck.animation.*/duck.mercure.*/duck.svg.missing_targetarrivent viaPOST /api/telemetry→ Prometheus.
friday.current.resolve, coffee.contribution.validate,
coffee.contribution.persist, energy.recalculate, accessory.vote.close,
mercure.update.publish, …
- Techniques : durée/nombre de requêtes HTTP, erreurs, durée DB, Messenger (traités/échoués), mémoire worker, publications Mercure.
- Métier :
duck.energy,duck.coffee.total,duck.overcaffeination.total,duck.accessory.winner,duck.friday.unique_visitors, … - Front (minimal) : init Theatre.js, état connexion Mercure, cibles SVG manquantes — sans donnée personnelle inutile.
Critique : « Nous sommes vendredi, mais le canard est DORMANT. » Énergie hors
0–100. Vote ouvert après l'heure de clôture. Taux d'échec Mercure / Theatre.js.
Mémoire worker en hausse durable.
Tout provisionné sous observability/ :
- Dashboards (§26.6) : « Métier » (vendredi actif, énergie, état, cafés, vitesse, visiteurs, vote, conseil/réactions) et « Technique » (erreurs/latence HTTP, mémoire worker, Mercure, backlog/file d'échec, divergence, APP_FAKE_NOW, santé front, café tracé Tempo). Grafana http://localhost:3000.
- Alertes (§26.7) : règles Prometheus (
prometheus-rules.yml) routées par Alertmanager, chacune avec seuil +for:(anti-fatigue), severity et lien runbook. Dead-man switch (TelemetryPipelineSilent,CollectorScrapeDown) : un pipeline mort est lui-même alerté, jamais confondu avec « tout va bien ». - Diagnostic :
DiagnosticsMetricsEmitter(tick/minute) expose les jauges rendant ces alertes possibles (divergence horloge/statut, backlog, file d'échec, APP_FAKE_NOW) — la divergence est un signal, jamais une correction (inv. B). - Procédure de test des alertes + marche à suivre par alerte :
runbook.md.
La 7 a posé l'instrumentation ; la 7b/7c étaient validées STATIQUEMENT. La 8b a
monté la chaîne COMPLÈTE et prouvé, en exécution réelle, que les signaux
atterrissent et qu'une alerte se déclenche. Stack dédiée :
compose.observability-e2e.yaml (app + VRAI worker Messenger + relais + bases
isolées, image AVEC l'ext PECL, OTLP → Collector du réseau d'observabilité).
docker compose -f compose.observability.yaml up -d # backends
docker compose -f compose.observability-e2e.yaml up -d --build --wait
bash observability/verify-e2e.sh # assertions
# Alerte AppFakeNowInProduction (noyau PROD + APP_FAKE_NOW présent) :
docker compose -f compose.observability-e2e.yaml --profile fakenow up -d --waitProuvé en exécution réelle :
- Café tracé bout-en-bout (Tempo). Une seule trace
HTTP POSTcouvrevisitor.resolve→friday.current.resolve→coffee.contribution.validate→energy.recalculate→coffee.contribution.persist→INSERT outbox→Doctrine::commit(+ auto-instrumentation Doctrine). Le span du relaismercure.update.publish, émis par un PROCESS CLI séparé (app:outbox:relay), s'y rattache en ENFANT via letraceparentporté par la ligne outbox — frontière async franchie et visible (vérif jamais exécutée en 7). - Métriques (Prometheus). Métier
duck_coffee_total/duck_energy, auto-instrumentéehttp_server_request_duration_*(auto Symfony), jauges de diagnosticworker_memory_bytes,mercure_outbox_backlog,duck_clock_friday_active,mercure_publish_count. - Logs corrélés (Loki). Les logs portent
trace_id/span_id(pont Monolog), requêtables par trace. - Alertes Firing (réelles, pas juste valides).
AppFakeNowInProduction(Pending→Firing dès la jaugeduck_app_fake_now_active{environment="prod"}=1) etCollectorScrapeDown(dead-man switch, Firing aprèsfor:5mcollector arrêté).
Corrections de dette livrées (8b) :
symfony/doctrine-messengerétait MANQUANT → aucun worker Messenger réel ne pouvait tourner (la 8a relayait viaapp:outbox:relay, jamais via un worker). Installé : le transportasync/failedet le Scheduler sont enfin consommables.- Flush télémétrie sur
ConsoleEvents::TERMINATE(TelemetryWorkerSubscriber) : sans lui, une commande COURTE (app:outbox:relay) créait le span du relais puis le PERDAIT à la sortie du process. C'est ce qui masquaitmercure.update.publish. - Monolog
when@preprod: preprod n'avait AUCUN handler → aucun log OTLP. Ajout du pontotelpour que l'env pré-prod (e2e) émette les logs corrélés comme la prod.
Routage retries → failed (B4) : RÉSOLU et PROUVÉ (§25.4). Les messages de
cycle sont désormais REDISPATCHÉS sur async (Schedule enveloppe RunCycleStep
dans un RedispatchMessage('async')), au lieu d'être traités en ligne sur
scheduler_default. La marque de dédup ET l'écriture outbox de l'annonce sont
rendues ATOMIQUES (une transactional() dans OpenFriday/PublishWinner/
CloseFriday), pour que le rejeu reste exactement-une-fois. Prouvé avec un VRAI
worker (déclenchement via app:cycle:dispatch PublishFridayOpened, écriture outbox
bloquée par un trigger SQL) :
- le
RunCycleStepéchoue, est REJOUÉ (#1 1s, #2 2s, #3 4s), puis — retries épuisés — « Removing from transport after 3 retries » et « Rejected … sent to the failure transport » → ligne enqueue_name='failed', visible viamessenger:failed:show(PLUS « 0 retries ») ; - exactement-une-fois préservé : pendant l'échec
processed_messagereste VIDE (marque rollback atomique) ; aprèsmessenger:failed:retry, l'annonce sort UNE fois (1 ligne outboxFRIDAY_OPENED, 1 marque, 0 en échec).
Le transport async (DSN auto_setup=0 en prod) est désormais possédé par la
migration Version20260628130000 (messenger_messages). Le filet app:friday:repair
(sync, idempotent) couvre toujours « worker async down ».
Limites honnêtes restantes (assumé, NON prouvé vert) :
- Métriques auto-instrumentées :
db.client.operation.duration/messenger.message.processedne sont PAS émises par les versions d'auto-instr installées (elles produisent des SPANS, pas ces métriques). La métrique auto-instrumentée réellement émise esthttp_server_request_duration(auto Symfony). OutboxBacklogStuck(min_over_time[15m] > 20) non déclenché : fenêtre de 15 min incompatible avec un e2e court ; déclenchement local long-run uniquement.
CI : non branché par défaut (empreinte ~7 conteneurs + 2 bases + 6 backends) → lancement LOCAL documenté ci-dessus, conformément au périmètre 8b.
-
Router les messages de cycle par— fait (8b/B4)asyncpour exercer retries →failed - Récepteur Alertmanager réel (Slack/e-mail) — Phase 9
- Déploiement opérationnel du stack (hors local) — Phase 9