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