4.0 KiB
Diseño de Logging
Nota: este documento describía originalmente un plan pendiente. Ya está implementado tal como se describe abajo; se conserva el nombre del archivo por los enlaces existentes desde
index.md.
Objetivo
Cada línea de log queda etiquetada automáticamente con el request_id de
la petición HTTP y con el empleado conectado que la originó (webUser,
emp_id, full_name), en formato JSON, sin tener que pasar esos datos a
mano en cada llamada a logger.info/warning/error.
Piezas
context.py
Dos ContextVar por request (aisladas entre corrutinas concurrentes de
FastAPI):
request_id_ctx— expuesta viaget_request_id()/set_request_id().current_user_ctx— dict{"webUser", "emp_id", "full_name"}, expuesta viaget_current_user()/set_current_user(webUser, full_name="", emp_id="-"). Por defecto ambas quedan en"-"cuando no hay usuario autenticado (por ejemplo, en el propioPOST /api/Loginantes de validar credenciales).
main.py
correlation_id_middlewarellama aset_request_id()con el headerX-Request-IDentrante o un UUID generado si no viene.session_middleware, tras validar el token convalidate_and_refresh_session(), llama aset_current_user(webUser, get_employee_name(webUser), get_emp_id(webUser))para que el resto de la petición (incluyendo las llamadas aforward_to_sapb1) quede etiquetada con ese usuario.routers/auth.pytambién llama aset_current_user(...)en un login exitoso, porque/api/Loginno pasa por elsession_middleware(todavía no hay token) y de otro modo su propio log quedaría sin usuario.LOG_ENVIRONMENT(variable de entorno, default"production") decide el formato de salida ensetup_logging().
logger_config.py — JsonFormatter
En modo "production" (default), cada línea de log —tanto en consola como
en logs/backend.log— es un JSON con esta forma:
{"timestamp": "2026-08-14T15:32:10.123456+00:00", "level": "INFO", "request_id": "3f2a...", "webUser": "jgarcia", "emp_id": "11", "full_name": "Juan García", "http_method": "GET", "endpoint": "Items", "status_code": 200, "message": "[200] GET Items", "module": "sap", "function": "forward_to_sapb1", "line": 141}
Campos siempre presentes: timestamp, level, request_id, webUser,
emp_id, full_name, message, module, function, line. Campos
opcionales (http_method, endpoint, status_code) se agregan solo si el
logger.info/warning/error(...) que generó la línea los pasó vía
extra={...} — este es el patrón que usan core/sap.py,
core/api_gateway.py y routers/reports.py para dejar trazabilidad de
cada llamada saliente. Si el log viene de una excepción (exc_info), se
agrega también exception con el traceback formateado.
En cualquier otro valor de LOG_ENVIRONMENT (por ejemplo "development"),
la consola usa un formato de texto plano legible
(%(asctime)s [%(levelname)s] %(module)s.%(funcName)s:%(lineno)d - %(message)s)
en vez de JSON — pensado para desarrollo local, no para buscar en el
archivo. El archivo logs/backend.log (rotativo, 5 MB x 3 backups vía
RotatingFileHandler) siempre se escribe en JSON sin importar el entorno.
Qué NO se loguea
routers/auth.py nunca escribe Password / U_WEB_PASS en los logs, ni en
éxito ni en fallo de login — solo el webUser, el EmployeeID y si el
intento fue exitoso o el motivo del rechazo ("Usuario no registrado",
"Usuario inactivo", contraseña incorrecta). Al auditar un cambio en el
flujo de login, verificar que esta regla se mantenga.
Verificación
- Levantar el backend (local o Docker).
- Loguearse con un usuario de prueba y hacer un par de llamadas reales (GET a algún catálogo, POST/PATCH a algo).
- Revisar
logs/backend.log: cada línea debe traerwebUser,emp_id,full_name, y las líneas de proxy SAP tambiénhttp_method,endpoint,status_code. grepde la contraseña de prueba sobre todologs/backend.log(y la salida de consola) para confirmar que no aparece en ningún lado.