Module — Monitoring & observabilité
Trois canaux : métriques Prometheus, logs applicatifs structurés, sanity check HTTP.
Métriques Prometheus
Endpoint : GET /metrics. Exposé via prometheus-fastapi-instrumentator instancié dans src/server.py :
Instrumentator().instrument(app).expose(app)Métriques applicatives
| Nom | Type | Émise par | Labels | Buckets / Notes |
|---|---|---|---|---|
n_results_in_search | Histogram | server.run_search | — | Buckets [0, 10, 25, 50, 75, 100, 150, 200] |
reindex_time_s | Histogram | reindex._do_reindex | — | Buckets [15, 30, 45, 60, 75, 90, 120, 240] (secondes) |
last_reindex_time | Gauge | reindex._do_reindex | — | Timestamp Unix de fin du dernier reindex |
db_size | Gauge | reindex._do_reindex | — | SELECT COUNT(*) FROM travels à la fin du reindex |
Métriques HTTP standard
Ajoutées automatiquement par prometheus-fastapi-instrumentator :
| Nom | Type | Labels |
|---|---|---|
http_requests_total | Counter | method, handler, status |
http_request_duration_seconds | Histogram | method, handler |
http_request_size_bytes | Summary | method, handler |
http_response_size_bytes | Summary | method, handler |
http_requests_in_progress | Gauge | method, handler |
Standard. Voir la doc de prometheus-fastapi-instrumentator pour la liste exacte.
Alertes recommandées
| Symptôme | Métrique / requête | Seuil |
|---|---|---|
| Reindex ne tourne plus | time() - last_reindex_time | > 1800s |
| Catalogue vidé | db_size | < 10 |
| Recherches qui retournent 0 résultat trop souvent | histogram_quantile(0.5, n_results_in_search) == 0 | sur 1h |
| 5xx fréquents | http_requests_total{status=~"5.."} | > 5/min |
| Latence en hausse | http_request_duration_seconds{handler="/travels"} p95 | > 2s |
Logs applicatifs
Configuration : logging.basicConfig(level=logging.INFO) dans app.py. Pas de handler structuré JSON par défaut — les logs sortent en format Python par défaut sur stdout/stderr.
Logger "server" (src/server.py)
Loggue chaque recherche avec ?search= non vide :
logger.info(dumps({
"search": search,
"destination": destination,
"category": category,
"dates": dates,
"discountclub": discountclub,
"seaside": seaside,
"n_results": total_count,
}))Le contenu est un JSON (sérialisé via json.dumps) embarqué dans la ligne de log. Utile :
- pour faire ressortir les requêtes utilisateur les plus fréquentes (Loki / Grafana avec
jsonparser). - pour spotter les filtres qui retournent systématiquement
n_results = 0(mauvais signal UX).
Logger "reindex" (src/reindex.py)
Verbose à chaque étape :
INFO reindex Starting reindex
INFO reindex Loaded 10 records on page 1 of https://horizon.buchard.ch/api/travels
INFO reindex Removing stale entries...
INFO reindex Reindex finishedUne exception dans le pipeline :
ERROR reindex Reindex failed
Traceback (most recent call last):
...Logger "db" (src/db.py)
INFO db Last reindex was 142 seconds ago. Skipping reindexÉmis au boot par _maybe_reindex() quand on skip.
Logger "schedule" (src/schedule_runner.py)
INFO schedule Starting reindexing, waiting 900 seconds.Logger "app" (app.py)
INFO app variables are loaded...
INFO app Server started
INFO app loaded the database url from fileSanity check
GET / retourne 200 toujours (tant que FastAPI tourne). Ne touche pas la DB.
Pour un health check qui valide la DB :
GET /metricsrépond200même si la DB est down (les métriques sont en mémoire process).- Aucun endpoint applicatif ne vérifie la connectivité DB. Si besoin, ajouter
/healthzqui faitSELECT 1côté psycopg.
Côté infra :
- Postgres : healthcheck Docker
pg_isready -U better_search(toutes les 5 s). better-search: pas de healthcheck Docker dans les Compose.depends_on: postgres: service_healthygarantit juste que Postgres est up au démarrage.
Debug en prod
Voir la dernière date de reindex
curl -s http://better-search/metrics | grep '^last_reindex_time 'Voir le nombre de voyages indexés
curl -s http://better-search/metrics | grep '^db_size 'Forcer un reindex
curl http://better-search/reindexRéponse immédiate (200 + JSON). Le reindex tourne en arrière-plan. Suivre les logs pour la suite.
Inspecter une recherche spécifique
Toujours utile :
curl 'http://better-search/travels?search=ski&size=3&infoDensity=card'L'infoDensity=card ramène semantic_score et tsvector_score — diagnostic immédiat des deux composantes du RRF.
Inspecter le contenu indexé
Direct côté DB :
-- combien de voyages
SELECT COUNT(*) FROM travels;
SELECT COUNT(*) FROM travels_view; -- exclut ceux sans départ futur
-- répartition par destination
SELECT destination, COUNT(*) FROM travels_view GROUP BY destination ORDER BY 2 DESC;
-- voyages balnéaires
SELECT id, name FROM travels_view WHERE is_seaside;
-- recherche brute (sans embedding)
SELECT id, name, ts_rank_cd(search_vector, websearch_to_tsquery('french', 'ski alpes')) AS score
FROM travels_view
WHERE search_vector @@ websearch_to_tsquery('french', 'ski alpes')
ORDER BY score DESC
LIMIT 10;Limites connues
- Pas de tracing distribué. Pas de Sentry, pas de OpenTelemetry. Une exception côté Infomaniak remonte dans les logs Python sans contexte de requête.
- Pas de log structuré JSON par défaut. Les lignes loggées par
server.run_searchsont du JSON dans un texte ; il faut un parser Loki/Promtail pour les exploiter. - Pas de métrique d'erreur reindex. Un reindex qui plante n'incrémente aucun compteur. Surveiller via
last_reindex_timequi n'avance pas. - Pas de métrique côté embeddings Infomaniak. Latence et taux d'erreur invisibles. À ajouter si problème observé.

