Skip to content

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 :

python
Instrumentator().instrument(app).expose(app)

Métriques applicatives

NomTypeÉmise parLabelsBuckets / Notes
n_results_in_searchHistogramserver.run_searchBuckets [0, 10, 25, 50, 75, 100, 150, 200]
reindex_time_sHistogramreindex._do_reindexBuckets [15, 30, 45, 60, 75, 90, 120, 240] (secondes)
last_reindex_timeGaugereindex._do_reindexTimestamp Unix de fin du dernier reindex
db_sizeGaugereindex._do_reindexSELECT COUNT(*) FROM travels à la fin du reindex

Métriques HTTP standard

Ajoutées automatiquement par prometheus-fastapi-instrumentator :

NomTypeLabels
http_requests_totalCountermethod, handler, status
http_request_duration_secondsHistogrammethod, handler
http_request_size_bytesSummarymethod, handler
http_response_size_bytesSummarymethod, handler
http_requests_in_progressGaugemethod, handler

Standard. Voir la doc de prometheus-fastapi-instrumentator pour la liste exacte.

Alertes recommandées

SymptômeMétrique / requêteSeuil
Reindex ne tourne plustime() - last_reindex_time> 1800s
Catalogue vidédb_size< 10
Recherches qui retournent 0 résultat trop souventhistogram_quantile(0.5, n_results_in_search) == 0sur 1h
5xx fréquentshttp_requests_total{status=~"5.."}> 5/min
Latence en haussehttp_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 :

python
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 json parser).
  • 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 finished

Une 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 file

Sanity check

GET / retourne 200 toujours (tant que FastAPI tourne). Ne touche pas la DB.

Pour un health check qui valide la DB :

  • GET /metrics répond 200 mê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 /healthz qui fait SELECT 1 cô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_healthy garantit juste que Postgres est up au démarrage.

Debug en prod

Voir la dernière date de reindex

sh
curl -s http://better-search/metrics | grep '^last_reindex_time '

Voir le nombre de voyages indexés

sh
curl -s http://better-search/metrics | grep '^db_size '

Forcer un reindex

sh
curl http://better-search/reindex

Ré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 :

sh
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 :

sql
-- 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_search sont 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_time qui n'avance pas.
  • Pas de métrique côté embeddings Infomaniak. Latence et taux d'erreur invisibles. À ajouter si problème observé.

Contributors

No contributors

Changelog

No recent changes