From 0637fc2a6322fdf3b40e750de9155322d9657a15 Mon Sep 17 00:00:00 2001 From: HotSwapp <47397945+HotSwapp@users.noreply.github.com> Date: Mon, 6 Oct 2025 22:22:04 -0500 Subject: [PATCH] chore: add structured logging with structlog; add request_id middleware; replace std logging --- Dockerfile | 1 + .../logging_config.cpython-313.pyc | Bin 0 -> 2543 bytes app/__pycache__/main.cpython-313.pyc | Bin 54904 -> 57563 bytes app/logging_config.py | 57 +++++++ app/main.py | 149 ++++++++++++++---- cookies.txt | 2 +- delphi.db | Bin 81920 -> 81920 bytes requirements.txt | 2 + 8 files changed, 177 insertions(+), 34 deletions(-) create mode 100644 app/__pycache__/logging_config.cpython-313.pyc create mode 100644 app/logging_config.py diff --git a/Dockerfile b/Dockerfile index 0d44b75..d95641c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ WORKDIR /app # Install system dependencies RUN apt-get update && apt-get install -y \ gcc \ + curl \ && rm -rf /var/lib/apt/lists/* # Copy requirements first for better layer caching diff --git a/app/__pycache__/logging_config.cpython-313.pyc b/app/__pycache__/logging_config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45d1acbf3f349ed7b33767cb1f50a0f384db2167 GIT binary patch literal 2543 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 Dict[str, Any]: + """ + Ensure all required fields exist on every log entry so downstream + consumers receive a consistent schema. + """ + # Required fields per project requirements + event_dict.setdefault("request_id", None) + event_dict.setdefault("http.method", None) + event_dict.setdefault("http.path", None) + event_dict.setdefault("status_code", None) + event_dict.setdefault("user.id", None) + event_dict.setdefault("duration_ms", None) + return event_dict + + +def setup_logging(log_level: int = logging.INFO) -> None: + """ + Configure structlog for JSON logging with contextvars support. + + Args: + log_level: Minimum log level for application logs. + """ + # Configure stdlib logging basic config for third-party libs (uvicorn, etc.) + logging.basicConfig(level=log_level) + + structlog.configure( + processors=[ + structlog.contextvars.merge_contextvars, + structlog.stdlib.add_log_level, + _add_required_defaults, + structlog.processors.TimeStamper(fmt="iso", key="timestamp"), + structlog.processors.dict_tracebacks, + structlog.processors.JSONRenderer(), + ], + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.make_filtering_bound_logger(log_level), + cache_logger_on_first_use=True, + ) + + diff --git a/app/main.py b/app/main.py index 8e6a9b6..c8cc057 100644 --- a/app/main.py +++ b/app/main.py @@ -6,7 +6,7 @@ and provides the main application instance. """ import os -import logging +import time import csv import json import uuid @@ -25,10 +25,13 @@ from sqlalchemy.orm import Session, joinedload from sqlalchemy import or_ from dotenv import load_dotenv from starlette.middleware.base import BaseHTTPMiddleware +import structlog +from structlog import contextvars as structlog_contextvars from .database import create_tables, get_db, get_database_url from .models import User, Case, Client, Phone, Transaction, Document, Payment, ImportLog from .auth import authenticate_user, get_current_user_from_session +from .logging_config import setup_logging # Load environment variables load_dotenv() @@ -38,9 +41,9 @@ SECRET_KEY = os.getenv("SECRET_KEY") if not SECRET_KEY: raise ValueError("SECRET_KEY environment variable must be set") -# Configure logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +# Configure structured logging +setup_logging() +logger = structlog.get_logger(__name__) # Configure Jinja2 templates templates = Jinja2Templates(directory="app/templates") @@ -74,6 +77,61 @@ class AuthMiddleware(BaseHTTPMiddleware): return await call_next(request) + +class RequestIdMiddleware(BaseHTTPMiddleware): + """ + Middleware that assigns a request_id and binds request context for logging. + + Adds: request_id, http.method, http.path, user.id to the structlog context. + Emits a JSON access log with status_code and duration_ms after response. + """ + + async def dispatch(self, request: Request, call_next): + start_time = time.perf_counter() + + request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4()) + method = request.method + path = request.url.path + + # user id from session if available (SessionMiddleware runs first) + user_id = request.session.get("user_id") if hasattr(request, "session") else None + + structlog_contextvars.bind_contextvars( + request_id=request_id, + **{"http.method": method, "http.path": path, "user.id": user_id}, + ) + + try: + response = await call_next(request) + status_code = response.status_code + except Exception as exc: # noqa: BLE001 - we re-raise after logging + status_code = 500 + duration_ms = int((time.perf_counter() - start_time) * 1000) + logger.error( + "request", + status_code=status_code, + duration_ms=duration_ms, + exc_info=True, + ) + structlog_contextvars.unbind_contextvars("request_id", "http.method", "http.path", "user.id") + raise + + # Ensure response header has request id + try: + response.headers["X-Request-ID"] = request_id + except Exception: + pass + + duration_ms = int((time.perf_counter() - start_time) * 1000) + logger.info( + "request", + status_code=status_code, + duration_ms=duration_ms, + ) + + structlog_contextvars.unbind_contextvars("request_id", "http.method", "http.path", "user.id") + return response + @asynccontextmanager async def lifespan(app: FastAPI): """ @@ -84,20 +142,20 @@ async def lifespan(app: FastAPI): - Logs database connection info """ # Startup - logger.info("Starting Delphi Database application...") + logger.info("app_start") # Create database tables create_tables() - logger.info("Database tables created/verified") + logger.info("db_tables_verified") # Log database connection info db_url = get_database_url() - logger.info(f"Database connected: {db_url}") + logger.info("db_connected", database_url=db_url) yield # Shutdown - logger.info("Shutting down Delphi Database application...") + logger.info("app_shutdown") # Create FastAPI application with lifespan management @@ -117,8 +175,9 @@ app.add_middleware( allow_headers=["*"], ) -# Register authentication middleware with exempt paths +# Register request logging and authentication middleware with exempt paths EXEMPT_PATHS = ["/", "/health", "/login", "/logout"] +app.add_middleware(RequestIdMiddleware) app.add_middleware(AuthMiddleware, exempt_paths=EXEMPT_PATHS) # Add SessionMiddleware for session management (must be added LAST so it runs FIRST) @@ -227,7 +286,7 @@ def parse_date(date_str: str) -> Optional[datetime]: except ValueError: continue - logger.warning(f"Could not parse date: '{date_str}'") + logger.warning("parse_date_failed", value=date_str) return None @@ -239,7 +298,7 @@ def parse_float(value: str) -> Optional[float]: try: return float(value.strip()) except ValueError: - logger.warning(f"Could not parse float: '{value}'") + logger.warning("parse_float_failed", value=value) return None @@ -251,7 +310,7 @@ def parse_int(value: str) -> Optional[int]: try: return int(value.strip()) except ValueError: - logger.warning(f"Could not parse int: '{value}'") + logger.warning("parse_int_failed", value=value) return None @@ -754,7 +813,7 @@ async def health_check(db: Session = Depends(get_db)): "users": user_count } except Exception as e: - logger.error(f"Health check failed: {e}") + logger.error("health_check_failed", error=str(e)) return { "status": "unhealthy", "database": "error", @@ -790,6 +849,7 @@ async def login_submit(request: Request, db: Session = Depends(get_db)): if not username or not password: error_message = "Username and password are required" + logger.warning("login_failed", username=username, reason="missing_credentials") return templates.TemplateResponse("login.html", { "request": request, "error": error_message @@ -799,6 +859,7 @@ async def login_submit(request: Request, db: Session = Depends(get_db)): user = authenticate_user(username, password) if not user: error_message = "Invalid username or password" + logger.warning("login_failed", username=username, reason="invalid_credentials") return templates.TemplateResponse("login.html", { "request": request, "error": error_message @@ -808,7 +869,9 @@ async def login_submit(request: Request, db: Session = Depends(get_db)): request.session["user_id"] = user.id request.session["user"] = {"id": user.id, "username": user.username} - logger.info(f"User '{username}' logged in successfully") + # Update bound context with authenticated user id + structlog_contextvars.bind_contextvars(**{"user.id": user.id}) + logger.info("login_success", username=username, **{"user.id": user.id}) # Redirect to dashboard after successful login return RedirectResponse(url="/dashboard", status_code=302) @@ -823,7 +886,7 @@ async def logout(request: Request): """ username = request.session.get("user", {}).get("username", "unknown") request.session.clear() - logger.info(f"User '{username}' logged out") + logger.info("logout", username=username) return RedirectResponse(url="/", status_code=302) @@ -883,11 +946,11 @@ async def dashboard( page_numbers = list(range(start_page, end_page + 1)) logger.info( - "Rendering dashboard: q='%s', page=%s, page_size=%s, total=%s", - q, - page, - page_size, - total, + "dashboard_render", + query=q, + page=page, + page_size=page_size, + total=total, ) return templates.TemplateResponse( @@ -971,7 +1034,12 @@ async def admin_upload_files( continue # Log the upload operation - logger.info(f"Admin upload: {len(results)} files uploaded, {len(errors)} errors by user '{user.username}'") + logger.info( + "admin_upload", + uploaded_count=len(results), + error_count=len(errors), + username=user.username, + ) return templates.TemplateResponse("admin.html", { "request": request, @@ -1085,7 +1153,13 @@ async def admin_import_data( total_errors += 1 # Log the import operation - logger.info(f"Admin import: {data_type}, {total_success} success, {total_errors} errors by user '{user.username}'") + logger.info( + "admin_import", + import_type=data_type, + success_count=total_success, + error_count=total_errors, + username=user.username, + ) return templates.TemplateResponse("admin.html", { "request": request, @@ -1181,7 +1255,7 @@ async def case_detail( ) if not case_obj: - logger.warning("Case not found: id=%s", case_id) + logger.warning("case_not_found", case_id=case_id) # Get any errors from session and clear them errors = request.session.pop("case_update_errors", None) @@ -1198,7 +1272,7 @@ async def case_detail( status_code=404, ) - logger.info("Rendering case detail: id=%s, file_no='%s'", case_obj.id, case_obj.file_no) + logger.info("case_detail", case_id=case_obj.id, file_no=case_obj.file_no) # Get any errors from session and clear them errors = request.session.pop("case_update_errors", None) @@ -1237,7 +1311,7 @@ async def case_update( # Fetch the case case_obj = db.query(Case).filter(Case.id == case_id).first() if not case_obj: - logger.warning("Case not found for update: id=%s", case_id) + logger.warning("case_not_found_update", case_id=case_id) return RedirectResponse(url=f"/case/{case_id}", status_code=302) # Validate and process fields @@ -1290,11 +1364,20 @@ async def case_update( # Apply updates try: + changed_fields = {} for field, value in update_data.items(): + old_value = getattr(case_obj, field) + if old_value != value: + changed_fields[field] = {"old": old_value, "new": value} setattr(case_obj, field, value) db.commit() - logger.info("Case updated successfully: id=%s, fields=%s", case_id, list(update_data.keys())) + logger.info( + "case_update", + case_id=case_id, + changed_fields=list(update_data.keys()), + changed_details=changed_fields, + ) # Clear any previous errors from session request.session.pop("case_update_errors", None) @@ -1303,7 +1386,7 @@ async def case_update( except Exception as e: db.rollback() - logger.error("Failed to update case id=%s: %s", case_id, str(e)) + logger.error("case_update_failed", case_id=case_id, error=str(e)) # Store error in session for display request.session["case_update_errors"] = ["Failed to save changes. Please try again."] @@ -1330,7 +1413,7 @@ async def case_close( # Fetch the case case_obj = db.query(Case).filter(Case.id == case_id).first() if not case_obj: - logger.warning("Case not found for close: id=%s", case_id) + logger.warning("case_not_found_close", case_id=case_id) return RedirectResponse(url=f"/case/{case_id}", status_code=302) # Update case @@ -1341,13 +1424,13 @@ async def case_close( case_obj.close_date = datetime.now() db.commit() - logger.info("Case closed: id=%s, close_date=%s", case_id, case_obj.close_date) + logger.info("case_closed", case_id=case_id, close_date=case_obj.close_date.isoformat() if case_obj.close_date else None) return RedirectResponse(url=f"/case/{case_id}?saved=1", status_code=302) except Exception as e: db.rollback() - logger.error("Failed to close case id=%s: %s", case_id, str(e)) + logger.error("case_close_failed", case_id=case_id, error=str(e)) # Store error in session for display request.session["case_update_errors"] = ["Failed to close case. Please try again."] @@ -1374,7 +1457,7 @@ async def case_reopen( # Fetch the case case_obj = db.query(Case).filter(Case.id == case_id).first() if not case_obj: - logger.warning("Case not found for reopen: id=%s", case_id) + logger.warning("case_not_found_reopen", case_id=case_id) return RedirectResponse(url=f"/case/{case_id}", status_code=302) # Update case @@ -1383,13 +1466,13 @@ async def case_reopen( case_obj.close_date = None db.commit() - logger.info("Case reopened: id=%s", case_id) + logger.info("case_reopened", case_id=case_id) return RedirectResponse(url=f"/case/{case_id}?saved=1", status_code=302) except Exception as e: db.rollback() - logger.error("Failed to reopen case id=%s: %s", case_id, str(e)) + logger.error("case_reopen_failed", case_id=case_id, error=str(e)) # Store error in session for display request.session["case_update_errors"] = ["Failed to reopen case. Please try again."] diff --git a/cookies.txt b/cookies.txt index f5452af..7fa9466 100644 --- a/cookies.txt +++ b/cookies.txt @@ -2,4 +2,4 @@ # https://curl.se/docs/http-cookies.html # This file was generated by libcurl! Edit at your own risk. -#HttpOnly_localhost FALSE / FALSE 1761009191 session eyJ1c2VyX2lkIjogMSwgInVzZXIiOiB7ImlkIjogMSwgInVzZXJuYW1lIjogImFkbWluIn19.aORpJw.oMEiA8ZMjrlLoJlYpDsM_T5EMpk +#HttpOnly_localhost FALSE / FALSE 1761016563 session eyJ1c2VyX2lkIjogMSwgInVzZXIiOiB7ImlkIjogMSwgInVzZXJuYW1lIjogImFkbWluIn19.aOSF8w.gmvSLjQ8LTg_OFCZNUZppoDIjrY diff --git a/delphi.db b/delphi.db index 75208c5c8d0a3765ccf75a80f5b497842e97c519..e233fb6e3d27915172caf3f3b9dc0db1c848d03f 100644 GIT binary patch delta 250 zcmZo@U~On%ogmGqJ5k1&RhL1ps<1I-YYO86c@YklM-03Vd1Sc@xt_7DWz%3?%<^cn zpui~>{zf|veg;uZM|n;T4$el#=^IrU^{2m9W8|(6$}d+ivQltPEKXG@&n(GMNXyJg zRmdyNO-e0NP!Dkp4$(C*FjQAa%qdDuOsP~zt;j4cDOQR#GB7gLH8jvQFjp`zwlXxc zGBCy>q-SnmVq#{3MW&I7oe$`u?WWG5v-;qXqzv%tJW< delta 85 zcmV-b0IL6hfCYen1&|v7E0G*S1uFn9hADw$wPXRH9}))zqW}%z3>^z`3g!l_1}Fui rvk?%D1-GLm0bvRf2M^c)5Bv}I5AYA_59ANt57@I2Ai@uq*gpX%Ei@Yz diff --git a/requirements.txt b/requirements.txt index c5dee4f..dcd7a64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,5 @@ python-dotenv==1.0.0 uvicorn[standard]==0.24.0 jinja2==3.1.2 aiofiles==23.2.1 +structlog==24.1.0 +itsdangerous==2.2.0