From 5d969709de939f29c16ee1db87aff5a84245576d Mon Sep 17 00:00:00 2001 From: Anamandla Date: Mon, 8 Jun 2026 14:16:39 +0200 Subject: [PATCH] feat: add structured JSON logging using structlog (closes #19) --- Assignment 12/api/logging_config.py | 23 +++++++++++++++++++ Assignment 12/handlers/routes/transactions.py | 15 ++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 Assignment 12/api/logging_config.py diff --git a/Assignment 12/api/logging_config.py b/Assignment 12/api/logging_config.py new file mode 100644 index 0000000..ff0fb73 --- /dev/null +++ b/Assignment 12/api/logging_config.py @@ -0,0 +1,23 @@ +import structlog +import logging +import sys + +def configure_logging(): + """Configure structlog for JSON output to stdout.""" + + structlog.configure( + processors=[ + structlog.stdlib.add_log_level, + structlog.stdlib.add_logger_name, + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.JSONRenderer(), + ], + wrapper_class=structlog.stdlib.BoundLogger, + context_class=dict, + logger_factory=structlog.PrintLoggerFactory(file=sys.stdout), + ) + +def get_logger(name: str = __name__): + return structlog.get_logger(name) \ No newline at end of file diff --git a/Assignment 12/handlers/routes/transactions.py b/Assignment 12/handlers/routes/transactions.py index 5707a79..d6d935e 100644 --- a/Assignment 12/handlers/routes/transactions.py +++ b/Assignment 12/handlers/routes/transactions.py @@ -32,6 +32,9 @@ DuplicateEntityError, BusinessRuleViolationError, InvalidStateTransitionError, ) +from api.logging_config import get_logger + +logger = get_logger("sentinelpay.transactions") router = APIRouter(prefix="/api/transactions", tags=["Transactions"]) @@ -45,6 +48,7 @@ def _handle_service_errors(exc: Exception) -> HTTPException: return HTTPException(status_code=422, detail=str(exc)) if isinstance(exc, InvalidStateTransitionError): return HTTPException(status_code=409, detail=str(exc)) + logger.error("unhandled_error", error=str(exc), exc_info=True) return HTTPException(status_code=500, detail=f"Internal error: {exc}") @@ -82,6 +86,12 @@ def submit_transaction( longitude=body.longitude, is_international=body.is_international, ) + logger.info( + "transaction_submitted", + transaction_id=str(txn.transaction_id), + amount=body.amount, + currency=body.currency, + ) return TransactionResponse.from_domain(txn) except Exception as e: raise _handle_service_errors(e) @@ -166,6 +176,11 @@ def apply_decision( model_scores=[s.model_dump() for s in body.model_scores], account_tier=body.account_tier, ) + logger.info( + "fraud_decision_applied", + transaction_id=transaction_id, + fraud_score=body.fraud_score, + ) return TransactionResponse.from_domain(txn) except Exception as e: raise _handle_service_errors(e)