File size: 6,110 Bytes
bffe28b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7263194
bffe28b
 
 
 
 
 
 
 
 
 
 
 
 
 
8e71f22
bffe28b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#!/usr/bin/env python3
"""
Module de logging structuré pour l'API Employee Turnover.

Fournit un système de logging centralisé avec :
- Logs structurés en JSON
- Rotation automatique des fichiers
- Niveaux de log configurables
- Intégration FastAPI
"""
import logging
import sys
from pathlib import Path
from typing import Any, Dict

from pythonjsonlogger.json import JsonFormatter

from src.config import get_settings

settings = get_settings()

# Créer le dossier logs s'il n'existe pas
LOG_DIR = Path("logs")
LOG_DIR.mkdir(exist_ok=True)

# Fichiers de logs
LOG_FILE = LOG_DIR / "api.log"
ERROR_LOG_FILE = LOG_DIR / "error.log"


class CustomJsonFormatter(JsonFormatter):
    """
    Formatter JSON personnalisé avec champs supplémentaires.
    """

    def add_fields(
        self,
        log_record: Dict[str, Any],
        record: logging.LogRecord,
        message_dict: Dict[str, Any],
    ) -> None:
        """
        Ajoute des champs personnalisés aux logs JSON.
        """
        super().add_fields(log_record, record, message_dict)

        # Ajouter des métadonnées
        log_record["level"] = record.levelname
        log_record["logger"] = record.name
        log_record["module"] = record.module
        log_record["function"] = record.funcName
        log_record["line"] = record.lineno

        # Timestamp ISO 8601
        if not log_record.get("timestamp"):
            log_record["timestamp"] = self.formatTime(record, self.datefmt)


def setup_logger(name: str = "employee_turnover_api") -> logging.Logger:
    """
    Configure et retourne un logger structuré.

    Args:
        name: Nom du logger.

    Returns:
        Logger configuré avec handlers console et fichiers.

    Examples:
        >>> logger = setup_logger()
        >>> logger.info("API démarrée", extra={"version": "2.0.0"})
    """
    logger = logging.getLogger(name)

    # Éviter duplication si déjà configuré
    if logger.handlers:
        return logger

    # Niveau de log depuis configuration
    log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO)
    logger.setLevel(log_level)

    # === HANDLER CONSOLE (stdout) ===
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(log_level)

    # Format simple pour la console en dev, JSON en prod
    if settings.DEBUG:
        console_format = logging.Formatter(
            "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
            datefmt="%Y-%m-%d %H:%M:%S",
        )
    else:
        console_format = CustomJsonFormatter(
            "%(timestamp)s %(level)s %(name)s %(message)s"
        )

    console_handler.setFormatter(console_format)
    logger.addHandler(console_handler)

    # === HANDLER FICHIER (tous les logs) ===
    file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
    file_handler.setLevel(log_level)
    file_handler.setFormatter(
        CustomJsonFormatter("%(timestamp)s %(level)s %(name)s %(message)s")
    )
    logger.addHandler(file_handler)

    # === HANDLER ERREURS UNIQUEMENT ===
    error_handler = logging.FileHandler(ERROR_LOG_FILE, encoding="utf-8")
    error_handler.setLevel(logging.ERROR)
    error_handler.setFormatter(
        CustomJsonFormatter("%(timestamp)s %(level)s %(name)s %(message)s")
    )
    logger.addHandler(error_handler)

    # Éviter propagation au root logger
    logger.propagate = False

    return logger


def log_request(
    method: str,
    path: str,
    status_code: int,
    duration_ms: float,
    **kwargs: Any,
) -> None:
    """
    Log une requête HTTP avec métadonnées.

    Args:
        method: Méthode HTTP (GET, POST...).
        path: Chemin de l'endpoint.
        status_code: Code de statut HTTP.
        duration_ms: Durée de la requête en millisecondes.
        **kwargs: Métadonnées additionnelles.

    Examples:
        >>> log_request("POST", "/predict", 200, 45.3, user_id="123")
    """
    logger = logging.getLogger("employee_turnover_api")

    log_data = {
        "method": method,
        "path": path,
        "status_code": status_code,
        "duration_ms": round(duration_ms, 2),
        **kwargs,
    }

    # Niveau selon status code
    if status_code >= 500:
        logger.error(f"Request {method} {path}", extra=log_data)
    elif status_code >= 400:
        logger.warning(f"Request {method} {path}", extra=log_data)
    else:
        logger.info(f"Request {method} {path}", extra=log_data)


def log_prediction(
    employee_id: str | None,
    prediction: int,
    probability: float,
    risk_level: str,
    duration_ms: float,
) -> None:
    """
    Log une prédiction effectuée.

    Args:
        employee_id: ID de l'employé (optionnel).
        prediction: Prédiction (0 ou 1).
        probability: Probabilité de turnover.
        risk_level: Niveau de risque ("low", "medium", "high").
        duration_ms: Durée du preprocessing + prédiction.

    Examples:
        >>> log_prediction("EMP123", 1, 0.87, "high", 23.4)
    """
    logger = logging.getLogger("employee_turnover_api")

    logger.info(
        "Prediction made",
        extra={
            "employee_id": employee_id,
            "prediction": prediction,
            "probability": round(probability, 4),
            "risk_level": risk_level,
            "duration_ms": round(duration_ms, 2),
        },
    )


def log_model_load(model_type: str, duration_ms: float, success: bool) -> None:
    """
    Log le chargement du modèle.

    Args:
        model_type: Type de modèle chargé.
        duration_ms: Durée du chargement.
        success: Si le chargement a réussi.

    Examples:
        >>> log_model_load("XGBoost Pipeline", 1234.5, True)
    """
    logger = logging.getLogger("employee_turnover_api")

    log_data = {
        "model_type": model_type,
        "duration_ms": round(duration_ms, 2),
        "success": success,
    }

    if success:
        logger.info("Model loaded successfully", extra=log_data)
    else:
        logger.error("Model loading failed", extra=log_data)


# Créer le logger global
logger = setup_logger()