Skip to content

Commit e030993

Browse files
committed
Tests du logging ELK
1 parent dfc2c12 commit e030993

File tree

2 files changed

+210
-0
lines changed

2 files changed

+210
-0
lines changed

api_integration/Makefile

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Makefile — tâches utilitaires pour dev / CI
2+
3+
.PHONY: help install start dev docker-build docker-up test test-quick lint fmt newman newman-report clean
4+
5+
SHELL := /bin/bash
6+
PY := python3
7+
PIP := pip
8+
UVICORN_MODULE := app.main:app
9+
HOST := 0.0.0.0
10+
PORT := 8000
11+
12+
help:
13+
@echo "Usage:"
14+
@echo " make install # Installer dépendances (virtualenv recommandé)"
15+
@echo " make start # Lancer API (uvicorn) en production"
16+
@echo " make dev # Lancer API en mode dev (reload)"
17+
@echo " make docker-build # Build docker image"
18+
@echo " make docker-up # Lancer docker-compose (API + ELK + Keycloak)"
19+
@echo " make test # Lancer pytest complet"
20+
@echo " make test-quick # Lancer subset de tests rapides"
21+
@echo " make lint # Lancer flake8/isort/black checks (si installés)"
22+
@echo " make fmt # Formater le code (black/isort)"
23+
@echo " make newman # Lancer collection Postman via newman (requires npm/newman)"
24+
@echo " make newman-report # Lancer newman et générer rapport HTML"
25+
@echo " make clean # Nettoyer artefacts (pycache, venv caches, reports)"
26+
27+
install:
28+
$(PY) -m venv .venv
29+
. .venv/bin/activate && $(PIP) install --upgrade pip
30+
. .venv/bin/activate && $(PIP) install -r requirements.txt
31+
@echo "Virtualenv created at .venv — active with: source .venv/bin/activate"
32+
33+
start:
34+
UVICORN_WORKERS=$$([ -z "$${UVICORN_WORKERS}" ] && echo 1 || echo $$UVICORN_WORKERS) \
35+
. .venv/bin/activate && uvicorn $(UVICORN_MODULE) --host $(HOST) --port $(PORT) --workers $$UVICORN_WORKERS
36+
37+
dev:
38+
. .venv/bin/activate && uvicorn $(UVICORN_MODULE) --reload --host $(HOST) --port $(PORT)
39+
40+
docker-build:
41+
docker build -t ml-scoring-fraude-api .
42+
43+
docker-up:
44+
docker-compose up --build
45+
46+
test:
47+
. .venv/bin/activate && pytest -q --maxfail=1
48+
49+
test-quick:
50+
. .venv/bin/activate && pytest tests/test_score_endpoint.py tests/test_fraude_endpoint.py -q
51+
52+
lint:
53+
. .venv/bin/activate && (flake8 || true)
54+
55+
fmt:
56+
. .venv/bin/activate && (black . || true) && (isort . || true)
57+
58+
newman:
59+
# Requires npm and newman installed locally or via npx
60+
npx newman run ml_scoring_fraude_dynamic.postman_collection.json -e ml_scoring_env.postman_environment.json --iteration-count 100
61+
62+
newman-report:
63+
npx newman run ml_scoring_fraude_dynamic.postman_collection.json -e ml_scoring_env.postman_environment.json --iteration-count 100 --reporters cli,html --reporter-html-export newman-report.html
64+
65+
clean:
66+
find . -type d -name "__pycache__" -exec rm -rf {} +
67+
find . -type f -name "*.pyc" -delete
68+
rm -rf .pytest_cache .venv newman-report.html
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""
2+
Tests pour le logging ELK (app/logging/elk_logger.py).
3+
4+
Objectifs :
5+
- Vérifier que la fonction mapLogRecord du handler retourne du JSON structuré attendu.
6+
- Vérifier que le logger global appelle bien le handler.emit lors d'un logger.info(...)
7+
- Si l'implémentation expose un client Elasticsearch (es / es_client), vérifier qu'une indexation est tentée
8+
lorsqu'un dict est loggé (cas où le code module utiliserait es.index).
9+
10+
Ces tests utilisent monkeypatch pour éviter tout appel réseau réel.
11+
"""
12+
import logging
13+
import json
14+
import types
15+
import pytest
16+
17+
from app.logging import elk_logger
18+
19+
20+
def make_log_record(msg, level=logging.INFO, name="elk_logger.test"):
21+
"""
22+
Crée un LogRecord utilisable par mapLogRecord/emit.
23+
"""
24+
return logging.LogRecord(name=name, level=level, pathname=__file__, lineno=1, msg=msg, args=(), exc_info=None)
25+
26+
27+
def test_mapLogRecord_returns_json_when_msg_is_dict():
28+
"""
29+
Vérifie que mapLogRecord retourne une JSON string contenant les champs attendus
30+
quand record.msg est un dict.
31+
"""
32+
handler = None
33+
# Cherche un handler de type ELKHTTPHandler dans elk_logger module
34+
for h in getattr(elk_logger, "logger", logging.getLogger()).handlers:
35+
# On identifie par le nom de classe défini dans le module
36+
if h.__class__.__name__ == "ELKHTTPHandler" or hasattr(h, "mapLogRecord"):
37+
handler = h
38+
break
39+
40+
# Si le handler spécifique n'existe pas (implémentation différente), on crée une instance locale
41+
if handler is None and hasattr(elk_logger, "ELKHTTPHandler"):
42+
handler = elk_logger.ELKHTTPHandler(host="localhost:5044", url="/test/_doc", method="POST")
43+
assert handler is not None, "Impossible d'obtenir ELKHTTPHandler pour le test"
44+
45+
# Crée un LogRecord dont msg est un dict
46+
record = make_log_record({"event": "unit_test", "transaction_id": "T123", "client_id": "C1"})
47+
mapped = handler.mapLogRecord(record)
48+
# mapLogRecord retourne une chaîne JSON (selon implémentation)
49+
assert isinstance(mapped, str)
50+
# Parse JSON
51+
parsed = json.loads(mapped)
52+
# Champs attendus
53+
assert "message" in parsed or "event" in parsed
54+
assert parsed.get("event") == "unit_test" or parsed.get("message")
55+
# Vérifier présence hostname/service/level if provided by implementation
56+
assert "level" in parsed or "logger_name" in parsed or "service" in parsed
57+
58+
59+
def test_logger_emit_called_on_info(monkeypatch):
60+
"""
61+
Vérifie que le handler.emit est appelé quand on logge via elk_logger.logger.info(...)
62+
"""
63+
logger = getattr(elk_logger, "logger", None)
64+
assert logger is not None, "Module elk_logger n'expose pas 'logger'"
65+
66+
# Choisir un handler sur lequel patcher emit
67+
target_handler = None
68+
for h in logger.handlers:
69+
# On patchera le premier handler trouvable
70+
target_handler = h
71+
break
72+
73+
assert target_handler is not None, "Aucun handler disponible sur elk_logger.logger"
74+
75+
called = {"count": 0, "record": None}
76+
77+
def fake_emit(rec):
78+
called["count"] += 1
79+
called["record"] = rec
80+
81+
# Patch emit
82+
monkeypatch.setattr(target_handler, "emit", fake_emit, raising=True)
83+
84+
# Appel du logger avec un dict (structure attendue)
85+
logger.info({"event": "test_info_emit", "client_id": "Cxyz"})
86+
87+
assert called["count"] >= 1, "Le handler.emit n'a pas été appelé"
88+
# Vérifier que le record contient le message (getMessage) et que c'est un dict ou string
89+
rec = called["record"]
90+
assert hasattr(rec, "getMessage")
91+
msg = rec.getMessage()
92+
# msg peut être dict (selon implémentation) ou stringified JSON
93+
assert (isinstance(msg, dict) and msg.get("event") == "test_info_emit") or ("test_info_emit" in str(msg))
94+
95+
96+
def test_es_index_called_when_es_client_present(monkeypatch):
97+
"""
98+
Si le module expose un client Elasticsearch (ex: es or es_client), patcher sa méthode index()
99+
et vérifier qu'elle est appelée lorsque l'on logge un dict (implémentations qui utilisent es.index).
100+
Ce test est tolérant : si le module n'expose pas d'objet es/es_client, on le considère comme non applicable.
101+
"""
102+
# Cherche es or es_client
103+
es_obj = None
104+
if hasattr(elk_logger, "es"):
105+
es_obj = getattr(elk_logger, "es")
106+
es_name = "es"
107+
elif hasattr(elk_logger, "es_client"):
108+
es_obj = getattr(elk_logger, "es_client")
109+
es_name = "es_client"
110+
else:
111+
es_obj = None
112+
es_name = None
113+
114+
if es_obj is None:
115+
pytest.skip("Aucun client Elasticsearch exposé dans elk_logger (es / es_client) — test non applicable")
116+
117+
called = {"count": 0, "args": None, "kwargs": None}
118+
119+
def fake_index(index, document=None, **kwargs):
120+
called["count"] += 1
121+
called["args"] = (index, document)
122+
called["kwargs"] = kwargs
123+
# Simuler une réponse ES
124+
return {"_index": index, "_id": "1", "result": "created"}
125+
126+
# Patch la méthode index sur l'objet es
127+
monkeypatch.setattr(es_obj, "index", fake_index, raising=True)
128+
129+
# Logger un event structuré qu'une implémentation pourrait envoyer via es.index
130+
elk_logger.logger.info({"event": "test_es_index", "transaction_id": "T999", "client_id": "C999"})
131+
132+
assert called["count"] >= 1, f"es.index n'a pas été appelé sur {es_name}"
133+
index_arg, doc_arg = called["args"]
134+
assert isinstance(index_arg, str)
135+
# Document doit contenir les clés qu'on a loggées
136+
assert doc_arg is not None
137+
# doc_arg peut être dict ou json string ; gérer les deux cas
138+
if isinstance(doc_arg, str):
139+
parsed = json.loads(doc_arg)
140+
else:
141+
parsed = doc_arg
142+
assert parsed.get("event") == "test_es_index" or parsed.get("transaction_id") == "T999"

0 commit comments

Comments
 (0)