Eliminat zgomotul de trasabilitate (US-xxx, PRD x.x, Rn, OV-x, Tn, decizii/naratiune istorica) din 41 fisiere app/ + template-uri. Pastrate comentariile care documenteaza invarianti si logica ne-evidenta (idempotenta/hash, reconciliere anti-duplicat, RAR 500 esec definitiv, creds per cont, WAF User-Agent, 422 fara echo de parola, scope NULL->1), curatate doar de tokeni. Verificare: pentru cele 27 module .py curatate, structura de cod (tokeni non-comentariu/ non-string) e IDENTICA fata de HEAD -> doar comentarii/docstring-uri schimbate. Singura schimbare de cod e in tests/test_web_responsive.py (scos 3 assert pe markeri US-006/007/008, inlocuite de asertiunile structurale alaturate). 0 tokeni US/PRD reziduali in app/. Regresie: 896 passed, 1 deselected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
147 lines
5.1 KiB
Python
147 lines
5.1 KiB
Python
"""Logger structurat central.
|
|
|
|
Singurul punct prin care se emit evenimente de aplicatie: garanteaza format,
|
|
redactare si dublul canal (app_events in DB + log text rotativ) consistente si
|
|
imposibil de ocolit. Best-effort: o cadere a jurnalului NU doboara cererea/worker-ul.
|
|
|
|
Redactare la SCRIERE (nu la afisare): toate valorile trec prin `redact_pii`
|
|
(creds/token mascate integral, VIN/nr partial) inainte de persistare.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextvars
|
|
import json
|
|
import logging
|
|
import logging.handlers
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Any
|
|
|
|
from .config import get_settings
|
|
from .db import get_connection, insert_app_event
|
|
from .security import redact_pii, scrub_text
|
|
|
|
# request_id al cererii curente. Setat de middleware-ul HTTP; disponibil in
|
|
# handlerul de erori si aici, fara a polua semnaturile de functii.
|
|
request_id_var: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
|
"request_id", default=None
|
|
)
|
|
|
|
_LEVELS = {"DEBUG": 10, "INFO": 20, "WARNING": 30, "WARN": 30, "ERROR": 40, "CRITICAL": 50}
|
|
|
|
# Sursa implicita a evenimentelor pentru procesul curent. API = 'api' (default);
|
|
# worker-ul cheama set_source('worker') la pornire (fisier per-proces).
|
|
_DEFAULT_SOURCE = "api"
|
|
|
|
_loggers: dict[str, logging.Logger] = {}
|
|
|
|
|
|
def set_source(sursa: str) -> None:
|
|
"""Fixeaza sursa implicita a evenimentelor (apelata o data de worker la start)."""
|
|
global _DEFAULT_SOURCE
|
|
_DEFAULT_SOURCE = sursa
|
|
|
|
|
|
def _text_logger(sursa: str) -> logging.Logger:
|
|
"""Logger cu RotatingFileHandler pe fisier per-proces (app-<sursa>.log).
|
|
|
|
Rotatia pe dimensiune e in aplicatie — nu depindem de deploy. Cheia de cache
|
|
include calea: la schimbarea log_dir (teste) se creeaza un logger nou, fara a
|
|
acumula handlere duplicate pe acelasi fisier.
|
|
"""
|
|
settings = get_settings()
|
|
path = settings.log_dir / f"app-{sursa}.log"
|
|
key = str(path)
|
|
lg = _loggers.get(key)
|
|
if lg is not None:
|
|
return lg
|
|
lg = logging.getLogger(f"autopass.events::{key}")
|
|
lg.setLevel(logging.DEBUG)
|
|
lg.propagate = False
|
|
try:
|
|
settings.log_dir.mkdir(parents=True, exist_ok=True)
|
|
handler = logging.handlers.RotatingFileHandler(
|
|
path,
|
|
maxBytes=settings.log_file_max_bytes,
|
|
backupCount=settings.log_file_backup_count,
|
|
encoding="utf-8",
|
|
)
|
|
handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
|
|
lg.addHandler(handler)
|
|
except Exception: # noqa: BLE001 — fisier indisponibil nu trebuie sa doboare logul DB
|
|
pass
|
|
_loggers[key] = lg
|
|
return lg
|
|
|
|
|
|
def _purge_after(days: int) -> str:
|
|
"""now (UTC) + days, in formatul SQLite datetime('now') ('YYYY-MM-DD HH:MM:SS')."""
|
|
return (datetime.now(timezone.utc) + timedelta(days=days)).strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
def log_event(
|
|
tip: str,
|
|
*,
|
|
nivel: str = "INFO",
|
|
account_id: int | None = None,
|
|
cod: str | None = None,
|
|
mesaj: str | None = None,
|
|
context: dict | None = None,
|
|
sursa: str | None = None,
|
|
request_id: str | None = None,
|
|
conn: Any = None,
|
|
) -> None:
|
|
"""Emite un eveniment: un rand `app_events` + o linie in logul text (acelasi continut redactat).
|
|
|
|
- `tip`: text liber documentat (lista extensibila).
|
|
- `nivel`: DEBUG|INFO|WARNING|ERROR|CRITICAL. Sub `AUTOPASS_LOG_LEVEL` -> ignorat.
|
|
- `context`: metadate (submission_id, count, status...) — NU payload PII integral.
|
|
- `conn`: reutilizeaza conexiunea apelantului pe calea fierbinte (evita contentie WAL);
|
|
None -> deschide/inchide o conexiune proprie.
|
|
Best-effort: orice exceptie e inghitita (jurnalul nu trebuie sa rupa fluxul).
|
|
"""
|
|
try:
|
|
settings = get_settings()
|
|
min_lvl = _LEVELS.get((settings.log_level or "INFO").upper(), 20)
|
|
lvl = (nivel or "INFO").upper()
|
|
if _LEVELS.get(lvl, 20) < min_lvl:
|
|
return
|
|
|
|
src = sursa or _DEFAULT_SOURCE
|
|
rid = request_id if request_id is not None else request_id_var.get()
|
|
mesaj_red = scrub_text(mesaj) if isinstance(mesaj, str) else mesaj
|
|
ctx_red = redact_pii(context) if context else None
|
|
ctx_json = (
|
|
json.dumps(ctx_red, ensure_ascii=False, default=str) if ctx_red is not None else None
|
|
)
|
|
purge_after = _purge_after(int(settings.log_retention_days))
|
|
|
|
own = conn is None
|
|
c = conn or get_connection()
|
|
try:
|
|
insert_app_event(
|
|
c,
|
|
request_id=rid,
|
|
account_id=account_id,
|
|
sursa=src,
|
|
tip=tip,
|
|
nivel=lvl,
|
|
cod=cod,
|
|
mesaj=mesaj_red,
|
|
context_json=ctx_json,
|
|
purge_after=purge_after,
|
|
)
|
|
finally:
|
|
if own:
|
|
c.close()
|
|
|
|
line = (
|
|
f"[{src}] tip={tip} nivel={lvl} cont={account_id} cod={cod} "
|
|
f"rid={rid} {mesaj_red or ''}"
|
|
)
|
|
if ctx_json:
|
|
line += f" ctx={ctx_json}"
|
|
_text_logger(src).info(scrub_text(line))
|
|
except Exception: # noqa: BLE001 — jurnal best-effort (ca notify_signup)
|
|
pass
|