Skip to content

Observabilité

L'équipe self-host une stack Prometheus / Grafana / Loki utilisée pour l'alerting. Prometheus atteint la VM par le réseau privé/VPN et scrape directement les ports publiés par Docker — sans passer par le nginx de l'hôte.

Sources de métriques

Trois sources, aucune dépendance Composer ajoutée :

SourceEndpointProtection
ApplicationGET /api/metrics (port backend 8800)Bearer token statique METRICS_TOKEN
FrankenPHP/Caddy:2112/metrics (publié en 8803)Réseau (firewall vers le host Prometheus)
imgproxy:8081 (publié en 8804)Réseau (firewall vers le host Prometheus)

GET /api/metrics — gauges applicatives calculées en SQL

Contrôleur : app/Http/Controllers/MetricsController.php, protégé par le middleware metrics-token (app/Http/Middleware/EnsureMetricsToken.php, même contrat que contract-token : fail-closed sans token configuré, hash_equals, check sauté en environnement local).

Choix structurant : aucun compteur accumulé en PHP. Avec plusieurs workers FrankenPHP et sans stockage partagé (pas de Redis ni d'APCu), un client Prometheus classique n'exposerait que la vue d'un seul worker. Chaque métrique est donc une gauge recalculée en SQL à chaque scrape (règle « SQL d'abord » du projet) et le format texte Prometheus (0.0.4) est rendu à la main.

Catalogue (namespace roadtrip_, familles à label status zero-filled depuis les enums PHP pour que les séries ne disparaissent jamais) :

  • roadtrip_info{version=…} — version applicative (APP_VERSION).
  • roadtrip_queue_jobs_pending / roadtrip_queue_jobs_reserved — profondeur de la queue database (table jobs).
  • roadtrip_queue_oldest_pending_age_seconds — âge du plus ancien job en attente ; un worker mort devient visible en quelques minutes (série qui croît de façon monotone). 0 quand la queue est vide.
  • roadtrip_queue_failed_jobs — lignes de failed_jobs.
  • roadtrip_activation_codes{status=…}GROUP BY status sur la vue activation_codes_with_status (7 statuts).
  • roadtrip_users{status=…}, roadtrip_organizations{status=…}, roadtrip_tours{status=…} — comptes par statut d'enum.
  • roadtrip_providers, roadtrip_activities — tailles du catalogue.
  • roadtrip_scheduler_heartbeat_age_seconds — âge du heartbeat écrit chaque minute par la commande planifiée metrics:scheduler-heartbeat (cache database, donc visible depuis le conteneur backend) ; -1 si jamais écrit. Alerte recommandée : > 300 s = conteneur scheduler mort.

Exemple de scrape_config :

yaml
- job_name: roadtrip-app
  metrics_path: /api/metrics
  scrape_interval: 60s
  authorization:
    credentials: <METRICS_TOKEN>
  static_configs:
    - targets: ["<vm>:8800"]

Métriques FrankenPHP/Caddy

Activées via le bloc caddy.env de config/octane.php : l'option globale metrics est ajoutée à CADDY_GLOBAL_OPTIONS (en préservant la valeur d'Octane auto_https disable_redirects — l'env du process est spread après celle de la commande, vendor/laravel/octane/src/Commands/StartFrankenPhpCommand.php) et CADDY_EXTRA_CONFIG ajoute un vhost :2112 servant le handler metrics. Pas de Caddyfile custom : l'admin localhost:2019, dont dépendent octane:reload/octane:stop, reste intact.

Séries utiles :

  • RED global : caddy_http_requests_total, caddy_http_request_duration_seconds{code,method,handler} (taux de 5xx, p95). Pas de label par route — la latence par endpoint s'analyse dans Loki.
  • Saturation (l'histoire OOM de juillet 2026) : frankenphp_busy_threads, frankenphp_total_threads, frankenphp_queue_depth, frankenphp_worker_crashes, frankenphp_worker_restarts.

imgproxy

Support Prometheus natif (version OSS) : IMGPROXY_PROMETHEUS_BIND: :8081 et IMGPROXY_PROMETHEUS_NAMESPACE: imgproxy dans le compose de déploiement. Séries : imgproxy_requests_total, imgproxy_request_span_duration_seconds, imgproxy_workers_utilization, …

Logs → Loki

Par défaut Laravel écrit dans storage/logs/laravel.log à l'intérieur du conteneur — invisible pour Loki. Sur la VM, le .env partagé passe les trois conteneurs applicatifs (backend, queue, scheduler) en JSON sur stderr, sans aucun changement de code :

dotenv
LOG_CHANNEL=stderr
LOG_STDERR_FORMATTER=Monolog\Formatter\JsonFormatter

Le channel stderr stock de config/logging.php résout le formatter via le conteneur d'injection. Les logs d'accès Caddy sortent déjà en JSON sur stderr.

Côté VM : Grafana Alloy (ou promtail) avec loki.source.docker, un label par service compose ; scraper aussi les access/error logs du nginx de l'hôte — c'est le seul endroit où un « backend down ⇒ 502 » est visible. Alerte Loki type : taux de lignes level=ERROR du backend sur 5 min.

Suites possibles (non construites)

Exporters côté VM (node_exporter, cAdvisor pour la proximité des limites mémoire 384M/100M, postgres_exporter, blackbox_exporter sur https://…/api/up), règles d'alerte Prometheus versionnées, et — si le besoin de latence par route se confirme — extension APCu + histogrammes par middleware (nécessite l'approbation d'une dépendance promphp).

Contributors

No contributors

Changelog

No recent changes