feat(5.6): observabilitate + jurnal aplicatie + lifecycle trimiteri blocate
Implementeaza PRD 5.6 complet (14 stories, TDD). Doua axe:
Lifecycle trimiteri blocate (Val A):
- submissions_admin.py: sterge/repune scoped (404 cross-account inaintea lui 409 stare)
- reactivare dedup peste `error` cu CAS (WHERE id=? AND status='error'), creds noi in
submissions + accounts.rar_creds_enc; worker invalideaza sesiunea RAR la creds proaspete
(JWT 30h vechi nu mai trimite cu parola gresita); camp aditiv `reactivated:true`
- retentie randuri blocate 30z; purge_expired exclude queued/sending; purge_after curatat
la reactivare/requeue
- API DELETE /v1/prezentari/{id} + /repune (200+JSON); UI butoane + bulk + banner actionabil
Observabilitate:
- app/observ.py log_event: dublu canal app_events (DB) + RotatingFileHandler per-proces,
redactare creds/PII la scriere (redact_pii/vin_partial)
- request_id middleware + X-Request-ID pe toate raspunsurile
- handler global excepții -> 500 envelope 6-chei + request_id (traceback doar in jurnal)
- audit cerere API (api_prezentari/api_auth_esuat) + audit worker (rar_login/tranzitii)
- tab "Jurnal" filtrabil scoped (non-admin doar contul sau); retentie jurnal 90z
- rar_error expus in GET /v1/prezentari/{id} (recovery observabil)
pytest -q: 741 passed, 0 failed. Docs: PRD raport VERIFY, contract endpointuri noi, ROADMAP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
41
app/main.py
41
app/main.py
@@ -20,14 +20,19 @@ from fastapi.staticfiles import StaticFiles
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import RedirectResponse
|
||||
|
||||
import traceback
|
||||
|
||||
from . import __version__
|
||||
from . import errors
|
||||
from .api.v1.import_router import router as import_v1_router
|
||||
from .api.v1.integrare_router import router as integrare_v1_router
|
||||
from .api.v1.router import router as api_v1_router
|
||||
from .config import get_settings
|
||||
from .crypto import validate_creds_key
|
||||
from .db import get_connection, init_db, queue_depth, read_heartbeat
|
||||
from .security import install_log_redaction
|
||||
from .observ import log_event, request_id_var
|
||||
from .security import install_log_redaction, scrub_text
|
||||
from .web.middleware import RequestIDMiddleware
|
||||
from .web.routes import router as web_router
|
||||
from .web.auth_routes import router as auth_router
|
||||
from .web.admin_routes import router as admin_router
|
||||
@@ -56,6 +61,10 @@ app.add_middleware(
|
||||
https_only=settings.session_https_only,
|
||||
same_site="strict",
|
||||
)
|
||||
# US-002: request_id pe fiecare cerere. Adaugat dupa SessionMiddleware -> ruleaza
|
||||
# OUTERMOST (add_middleware prepend), deci `X-Request-ID` se pune pe TOATE raspunsurile,
|
||||
# inclusiv 401/404/422/500 produse mai in interior.
|
||||
app.add_middleware(RequestIDMiddleware)
|
||||
|
||||
|
||||
@app.exception_handler(LoginRequired)
|
||||
@@ -86,6 +95,36 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
|
||||
return JSONResponse(status_code=422, content={"detail": cleaned})
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""Orice excepție neprinsa -> 500 STRUCTURAT (3 niveluri, PRD 5.4) in loc de 500 brut.
|
||||
|
||||
Body = envelope-ul standard din catalog (6 chei: field/cod/problema/cauza/fix/message)
|
||||
+ `request_id` — fara traceback, fara mesaj de excepție brut, fara creds. Traceback-ul
|
||||
complet + ruta + cont + request_id se scriu DOAR in jurnal (redactate prin scrub_text).
|
||||
Handlerele specifice (LoginRequired/AdminRequired/CSRF/RequestValidationError/HTTPException)
|
||||
raman neatinse — acesta prinde doar ce nu are handler dedicat.
|
||||
"""
|
||||
request_id = getattr(request.state, "request_id", None) or request_id_var.get()
|
||||
try:
|
||||
account_id = request.session.get("account_id")
|
||||
except (AssertionError, KeyError, AttributeError):
|
||||
account_id = None
|
||||
tb = scrub_text("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
|
||||
log_event(
|
||||
"eroare_interna",
|
||||
nivel="ERROR",
|
||||
account_id=account_id,
|
||||
cod="EROARE_INTERNA",
|
||||
mesaj=f"{request.method} {request.url.path}: {type(exc).__name__}",
|
||||
context={"path": request.url.path, "method": request.method, "traceback": tb},
|
||||
request_id=request_id,
|
||||
)
|
||||
body = errors.eroare("EROARE_INTERNA")
|
||||
body["request_id"] = request_id
|
||||
return JSONResponse(status_code=500, content=body, headers={"X-Request-ID": request_id or ""})
|
||||
|
||||
|
||||
# Assets servite local (htmx vendorizat), NU din CDN: gateway-ul ruleaza
|
||||
# offline (LXC/VPS, Cloudflare Tunnel). Fara asta, dashboard-ul ramane static
|
||||
# (zero polling banner/coada) cand unpkg e blocat/inaccesibil. Aceeasi decizie
|
||||
|
||||
Reference in New Issue
Block a user