From 6783cfdcbf443d21aca3edfa7976db563bdc950a Mon Sep 17 00:00:00 2001 From: Shounak Dey Date: Wed, 13 Aug 2025 00:00:01 +0530 Subject: [PATCH] feat: add comprehensive logging across application - Implement structured logging throughout the application to improve monitoring, debugging, and operational visibility. Signed-off-by: Shounak Dey --- fastapi_ecom/app.py | 3 ++ fastapi_ecom/config/config.py.example | 34 +++++++++++++++++++ fastapi_ecom/database/db_setup.py | 6 ++++ .../database/pydantic_schemas/customer.py | 8 ++--- fastapi_ecom/main.py | 11 ++++++ fastapi_ecom/router/business.py | 17 ++++++++++ fastapi_ecom/router/customer.py | 22 ++++++++++-- fastapi_ecom/router/order.py | 9 +++++ fastapi_ecom/router/product.py | 24 +++++++++++++ fastapi_ecom/utils/auth.py | 3 ++ fastapi_ecom/utils/basic_auth.py | 7 ++++ fastapi_ecom/utils/logging_setup.py | 21 ++++++++++++ fastapi_ecom/utils/oauth.py | 19 +++++++++-- poetry.lock | 1 + 14 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 fastapi_ecom/utils/logging_setup.py diff --git a/fastapi_ecom/app.py b/fastapi_ecom/app.py index 1aaae8a..9bf3591 100644 --- a/fastapi_ecom/app.py +++ b/fastapi_ecom/app.py @@ -4,6 +4,7 @@ from fastapi_ecom.config import config from fastapi_ecom.router import business, customer, order, product +from fastapi_ecom.utils.logging_setup import general # Metadata for API tags tags_metadata = [ @@ -36,6 +37,7 @@ def root() -> dict[str, str]: :return: Metadata about the API, including title, description, and version. """ + general("Root endpoint accessed") return{ "title": "FastAPI ECOM", "description": "E-Commerce API for businesses and end users using FastAPI.", @@ -58,6 +60,7 @@ def start_service(): :raises RuntimeError: If configuration parameters are missing or invalid. """ + general("FastAPI server started") uvicorn.run( "fastapi_ecom.app:app", host=config.servhost, diff --git a/fastapi_ecom/config/config.py.example b/fastapi_ecom/config/config.py.example index bdebb88..c7a1db7 100644 --- a/fastapi_ecom/config/config.py.example +++ b/fastapi_ecom/config/config.py.example @@ -1,3 +1,6 @@ +from logging import getLogger +from logging.config import dictConfig + # The database name database = "test_db" @@ -33,3 +36,34 @@ GOOGLE_CLIENT_ID = "example.apps.googleusercontent.com" # Google Client secret GOOGLE_CLIENT_SECRET = "example" + +# The default configuration for service logging +log_config = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "standard": { + "format": "%(message)s %(asctime)s", + "datefmt": "[%Y-%m-%d %I:%M:%S %z]", + }, + }, + "handlers": { + "console": { + "level": "INFO", + "formatter": "standard", + "class": "logging.StreamHandler", + "stream": "ext://sys.stdout", + }, + }, + "loggers": { + "fastapi_ecom": { + "level": "INFO", + "handlers": ["console"], + "propagate": False, + }, + }, +} + +dictConfig(log_config) + +logger = getLogger(__name__) diff --git a/fastapi_ecom/database/db_setup.py b/fastapi_ecom/database/db_setup.py index 0bb840a..d3a38c0 100644 --- a/fastapi_ecom/database/db_setup.py +++ b/fastapi_ecom/database/db_setup.py @@ -12,6 +12,7 @@ migrpath, models, ) +from fastapi_ecom.utils.logging_setup import general, success def make_database() -> None: @@ -24,16 +25,21 @@ def make_database() -> None: - Marks the database as being at the latest migration version. """ # Use the synchronous engine to create the database schema. + general("Creating database schema with synchronous engine") sync_engine = get_engine(engine="sync") baseobjc.metadata.create_all(bind=sync_engine) + success("Database schema created successfully") # Set up Alembic configuration for migration management. + general("Setting up Alembic configuration") alembic_config = config.Config(alempath) alembic_config.set_main_option("script_location", migrpath) alembic_config.set_main_option("sqlalchemy.url", get_database_url().render_as_string(hide_password=False)) # Mark the database at the latest migration head. + general("Marking database at latest migration head") command.stamp(alembic_config, "head") + success("Database marked at migration head successfully") async def get_db() -> AsyncGenerator[AsyncSession, None]: """ diff --git a/fastapi_ecom/database/pydantic_schemas/customer.py b/fastapi_ecom/database/pydantic_schemas/customer.py index 07c6bf0..d2946c3 100644 --- a/fastapi_ecom/database/pydantic_schemas/customer.py +++ b/fastapi_ecom/database/pydantic_schemas/customer.py @@ -35,10 +35,10 @@ class CustomerView(CustomerBase): """ email: EmailStr name: str - addr_line_1: str - addr_line_2: str - city: str - state: str + addr_line_1: Optional[str] + addr_line_2: Optional[str] + city: Optional[str] + state: Optional[str] class CustomerCreate(CustomerView): diff --git a/fastapi_ecom/main.py b/fastapi_ecom/main.py index 7565a59..19f09be 100644 --- a/fastapi_ecom/main.py +++ b/fastapi_ecom/main.py @@ -3,6 +3,7 @@ from fastapi_ecom.app import start_service from fastapi_ecom.database.db_setup import make_database from fastapi_ecom.migrations.main import alembic_migration +from fastapi_ecom.utils.logging_setup import general, success @click.group(name="fastapi_ecom", help="E-Commerce API for businesses and end users using FastAPI.") @@ -26,7 +27,9 @@ def setup() -> None: :return: None """ + general("Setting up database schema") make_database() + success("Database schema setup completed") @main.command(name="start", help="Start the FastAPI eComm application") def start() -> None: @@ -37,6 +40,7 @@ def start() -> None: :return: None """ + general("Starting FastAPI eComm application") start_service() @main.command(name="create-migration", help="Create a new migration script") @@ -51,7 +55,9 @@ def create_migration(comment: str, autogenerate: bool) -> None: :return: None """ + general(f"Creating migration with comment: {comment}") alembic_migration.create(comment, autogenerate) + success(f"Migration created successfully: {comment}") @main.command(name="db-version", help="Show the current database version") def db_version() -> None: @@ -62,6 +68,7 @@ def db_version() -> None: :return: None """ + general("Checking database version") alembic_migration.db_version() @main.command(name="upgrade-db", help="Upgrade the database to a specific version") @@ -75,7 +82,9 @@ def upgrade_db(version: str) -> None: :return: None """ + general(f"Upgrading database to version: {version}") alembic_migration.upgrade(version) + success(f"Database upgraded to version: {version}") @main.command(name="downgrade-db", help="Downgrade the database to a specific version") @click.argument("version", type=str) @@ -88,4 +97,6 @@ def downgrade_db(version: str) -> None: :return: None """ + general(f"Downgrading database to version: {version}") alembic_migration.downgrade(version) + success(f"Database downgraded to version: {version}") diff --git a/fastapi_ecom/router/business.py b/fastapi_ecom/router/business.py index e2d12b8..5772fd4 100644 --- a/fastapi_ecom/router/business.py +++ b/fastapi_ecom/router/business.py @@ -19,6 +19,7 @@ BusinessView, ) from fastapi_ecom.utils.auth import verify_business_cred +from fastapi_ecom.utils.logging_setup import failure, general, success, warning router = APIRouter(prefix="/business") @@ -50,19 +51,23 @@ async def create_business(business: BusinessCreate, db: AsyncSession = Depends(g state=business.state.strip(), uuid=uuid4().hex[0:8] # Assign UUID manually; One UUID per transaction ) + general(f"Adding account for business in database: {business.email}") db.add(db_business) try: await db.flush() except IntegrityError as expt: + failure(f"Business account creation failed - Uniqueness constraint violation for email: {business.email}") raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Uniqueness constraint failed - Please try again" ) from expt except Exception as expt: + failure(f"Business account creation failed with unexpected error for email: {business.email}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Business account created successfully with email: {business.email}") return { "action": "post", "business": BusinessView.model_validate(db_business).model_dump() @@ -77,6 +82,7 @@ async def get_business_me(business_auth = Depends(verify_business_cred)) -> dict :return: Dictionary containing the action type and the authenticated business's email. """ + general(f"Business authentication successful for: {business_auth.email}") return { "action": "get", "email": business_auth.email @@ -101,14 +107,17 @@ async def get_businesses( :raises HTTPException: - If no business exist in the database, it raises 404 Not Found. """ + general(f"Searching businesses with skip={skip}, limit={limit}") query = select(Business).options(selectinload("*")).offset(skip).limit(limit) result = await db.execute(query) businesses = result.scalars().all() if not businesses: + warning("No businesses found in database") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No business present in database" ) + success(f"Found {len(businesses)} businesses") return { "action": "get", "businesses": [BusinessView.model_validate(business).model_dump() for business in businesses] @@ -129,6 +138,7 @@ async def delete_business(db: AsyncSession = Depends(get_db), business_auth = De - If a uniqueness constraint fails, it returns a 409 Conflict status. - If there are other database errors, it returns a 500 Internal Server Error. """ + general(f"Deleting business account: {business_auth.email}") query = select(Business).where(Business.uuid == business_auth.uuid).options(selectinload("*")) result = await db.execute(query) business_to_delete = result.scalar_one_or_none() @@ -142,10 +152,12 @@ async def delete_business(db: AsyncSession = Depends(get_db), business_auth = De interactions due to which mocking one part wont produce the desired result. Thus, we will keep it uncovered until a alternative can be made for testing this exception block. """ + failure(f"Business account deletion failed for: {business_auth.email}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Business account deleted successfully: {business_auth.email}") return { "action": "delete", "business": BusinessView.model_validate(business_to_delete).model_dump() @@ -171,6 +183,8 @@ async def update_business( - If a uniqueness constraint fails, it returns a 409 Conflict status. - If there are other database errors, it returns a 500 Internal Server Error. """ + business_email = business_auth.email # Capture email before potential database errors + general(f"Updating details of business: {business_email}") query = select(Business).where(Business.uuid == business_auth.uuid).options(selectinload("*")) result = await db.execute(query) business_to_update = result.scalar_one_or_none() @@ -190,6 +204,7 @@ async def update_business( try: await db.flush() except IntegrityError as expt: + failure(f"Business update failed - Uniqueness constraint violation for: {business_email}") raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Uniqueness constraint failed - Please try again" @@ -200,10 +215,12 @@ async def update_business( interactions due to which mocking one part wont produce the desired result. Thus, we will keep it uncovered until a alternative can be made for testing this exception block. """ + failure(f"Business update failed with unexpected error for: {business_email}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Business details updated successfully: {business_email}") return { "action": "put", "business": BusinessView.model_validate(business_to_update).model_dump() diff --git a/fastapi_ecom/router/customer.py b/fastapi_ecom/router/customer.py index ffed86c..275798e 100644 --- a/fastapi_ecom/router/customer.py +++ b/fastapi_ecom/router/customer.py @@ -19,6 +19,7 @@ CustomerView, ) from fastapi_ecom.utils.auth import verify_cust_cred +from fastapi_ecom.utils.logging_setup import failure, general, success, warning router = APIRouter(prefix="/customer") @@ -50,19 +51,23 @@ async def create_customer(customer: CustomerCreate, db: AsyncSession = Depends(g state = customer.state.strip(), uuid = uuid4().hex[0:8] # Assign UUID manually; One UUID per transaction ) + general(f"Adding account for customer in database: {customer.email}") db.add(db_customer) try: await db.flush() except IntegrityError as expt: - raise HTTPException( + failure(f"Customer account creation failed - Uniqueness constraint violation for email: {customer.email}") + raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Uniqueness constraint failed - Please try again" ) from expt except Exception as expt: + failure(f"Customer account creation failed with unexpected error for email: {customer.email}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Customer account created successfully with email: {customer.email}") return { "action": "post", "customer": CustomerView.model_validate(db_customer).model_dump() @@ -77,6 +82,7 @@ async def get_customer_me(customer_auth = Depends(verify_cust_cred)) -> dict[str :return: Dictionary containing the action type and the authenticated customer's email. """ + general(f"Customer authentication successful for: {customer_auth.email}") return { "action": "get", "email": customer_auth.email @@ -101,14 +107,17 @@ async def get_customers( :raises HTTPException: - If no customer exist in the database, it raises 404 Not Found. """ + general(f"Searching customers with skip={skip}, limit={limit}") query = select(Customer).options(selectinload("*")).offset(skip).limit(limit) result = await db.execute(query) customers = result.scalars().all() if not customers: + warning("No customers found in database") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No customer present in database" ) + success(f"Found {len(customers)} customers") return { "action": "get", "customers": [CustomerView.model_validate(customer).model_dump() for customer in customers] @@ -122,12 +131,14 @@ async def delete_customer(db: AsyncSession = Depends(get_db), customer_auth = De :param db: Active asynchronous database session dependency. :param customer_auth: Authenticated customer object. - :return: Dictionary containing the action type and the deleted customer's data. + :return: Dictionary containing the action type and the deleted customer's data, validated and + serialized using the `CustomerView` schema. :raises HTTPException: - If a uniqueness constraint fails, it returns a 409 Conflict status. - If there are other database errors, it returns a 500 Internal Server Error. """ + general(f"Deleting customer account: {customer_auth.email}") query = select(Customer).where(Customer.uuid == customer_auth.uuid).options(selectinload("*")) result = await db.execute(query) customer_to_delete = result.scalar_one_or_none() @@ -141,10 +152,12 @@ async def delete_customer(db: AsyncSession = Depends(get_db), customer_auth = De interactions due to which mocking one part wont produce the desired result. Thus, we will keep it uncovered until a alternative can be made for testing this exception block. """ + failure(f"Customer account deletion failed for: {customer_auth.email}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Customer account deleted successfully: {customer_auth.email}") return { "action": "delete", "customer": CustomerView.model_validate(customer_to_delete).model_dump() @@ -165,6 +178,8 @@ async def update_customer(customer: CustomerUpdate, db: AsyncSession = Depends(g - If a uniqueness constraint fails, it returns a 409 Conflict status. - If there are other database errors, it returns a 500 Internal Server Error. """ + customer_email = customer_auth.email # Capture email before potential database errors + general(f"Updating details of customer: {customer_email}") query = select(Customer).where(Customer.uuid == customer_auth.uuid).options(selectinload("*")) result = await db.execute(query) customer_to_update = result.scalar_one_or_none() @@ -184,6 +199,7 @@ async def update_customer(customer: CustomerUpdate, db: AsyncSession = Depends(g try: await db.flush() except IntegrityError as expt: + failure(f"Customer update failed - Uniqueness constraint violation for: {customer_email}") raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Uniqueness constraint failed - Please try again" @@ -194,10 +210,12 @@ async def update_customer(customer: CustomerUpdate, db: AsyncSession = Depends(g interactions due to which mocking one part wont produce the desired result. Thus, we will keep it uncovered until a alternative can be made for testing this exception block. """ + failure(f"Customer update failed with unexpected error for: {customer_email}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Customer details updated successfully: {customer_email}") return { "action": "put", "customer": CustomerView.model_validate(customer_to_update).model_dump() diff --git a/fastapi_ecom/router/order.py b/fastapi_ecom/router/order.py index 9cfaa92..a0287ee 100644 --- a/fastapi_ecom/router/order.py +++ b/fastapi_ecom/router/order.py @@ -24,6 +24,7 @@ OrderDetailsViewInternal, ) from fastapi_ecom.utils.auth import verify_cust_cred +from fastapi_ecom.utils.logging_setup import failure, general, warning router = APIRouter(prefix="/order") @@ -57,6 +58,7 @@ async def create_order(order: OrderCreate, db: AsyncSession = Depends(get_db), c interactions due to which mocking one part wont produce the desired result. Thus, we will keep it uncovered until a alternative can be made for testing this exception block. """ + failure(f"Order creation failed with unexpected error for customer: {customer_auth.email}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." @@ -95,6 +97,7 @@ async def create_order(order: OrderCreate, db: AsyncSession = Depends(get_db), c interactions due to which mocking one part wont produce the desired result. Thus, we will keep it uncovered until a alternative can be made for testing this exception block. """ + failure(f"Order creation failed with unexpected error for customer: {customer_auth.email}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." @@ -135,10 +138,12 @@ async def get_orders( :raises HTTPException: If no products are associated with the authenticated customer, it raises 404 Not Found. """ + general(f"Searching orders for customer {customer_auth.email} with skip={skip}, limit={limit}") query = select(Order).where(Order.user_id == customer_auth.uuid).options(selectinload(Order.order_details)).offset(skip).limit(limit) result = await db.execute(query) orders = result.scalars().unique().all() if not orders: + warning(f"No order found in database for customer {customer_auth.email}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No orders in database" @@ -196,6 +201,7 @@ async def get_orders_internal( result = await db.execute(query) orders = result.scalars().unique().all() if not orders: + warning("No order found in database") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No orders in database" @@ -244,6 +250,7 @@ async def get_order_by_uuid(order_id: str, db: AsyncSession = Depends(get_db), c result = await db.execute(query) order = result.scalar_one_or_none() if not order: + warning(f"Order {order_id} no present in database") raise HTTPException( status_code = status.HTTP_404_NOT_FOUND, detail = "Order not present in database" @@ -294,6 +301,7 @@ async def delete_order(order_id: str, db: AsyncSession = Depends(get_db), custom result = await db.execute(query) order_to_delete = result.scalar_one_or_none() if not order_to_delete: + warning(f"Order {order_id} no present in database") raise HTTPException( status_code = status.HTTP_404_NOT_FOUND, detail = "Order not present in database" @@ -322,6 +330,7 @@ async def delete_order(order_id: str, db: AsyncSession = Depends(get_db), custom interactions due to which mocking one part wont produce the desired result. Thus, we will keep it uncovered until a alternative can be made for testing this exception block. """ + failure(f"Order deletion failed with unexpected error for customer: {customer_auth.email}") raise HTTPException( status_code = status.HTTP_500_INTERNAL_SERVER_ERROR, detail = "Failed while deleting" diff --git a/fastapi_ecom/router/product.py b/fastapi_ecom/router/product.py index 3464cfa..2f1f383 100644 --- a/fastapi_ecom/router/product.py +++ b/fastapi_ecom/router/product.py @@ -20,6 +20,7 @@ ProductViewInternal, ) from fastapi_ecom.utils.auth import verify_business_cred +from fastapi_ecom.utils.logging_setup import failure, general, success, warning router = APIRouter(prefix="/product") @@ -53,6 +54,7 @@ async def add_product( business_id = business_auth.uuid, uuid=uuid4().hex[0:8] # Assign UUID manually; One UUID per transaction ) + general(f"Adding product '{product.name}' to database by {business_auth.email}") db.add(db_product) try: await db.flush() @@ -62,10 +64,12 @@ async def add_product( interactions due to which mocking one part wont produce the desired result. Thus, we will keep it uncovered until a alternative can be made for testing this exception block. """ + failure(f"Product creation failed for '{product.name}' with unexpected error") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Product '{product.name}' created successfully by business: {business_auth.uuid}") return { "action": "post", "product": ProductViewInternal.model_validate(db_product).model_dump() @@ -91,14 +95,17 @@ async def get_products( - If no products for the currently authenticated business exists in the database, it raises 404 Not Found. """ + general(f"Searching all products with skip={skip}, limit={limit}") query = select(Product).options(selectinload("*")).offset(skip).limit(limit) result = await db.execute(query) products = result.scalars().all() if not products: + warning("No products found in database") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No product present in database" ) + success(f"Found {len(products)} products") return { "action": "get", "products": [ProductView.model_validate(product).model_dump() for product in products] @@ -125,16 +132,19 @@ async def get_product_by_text( :raises HTTPException: If no matching products exists in the database, it raises 404 Not Found. """ + general(f"Searching products by text '{text}' with skip={skip}, limit={limit}") query = select(Product).where( or_(Product.name.ilike(f"%{text}%"), Product.description.ilike(f"%{text}%")) ).options(selectinload("*")).offset(skip).limit(limit) result = await db.execute(query) products = result.scalars().all() if not products: + warning(f"No products found matching text '{text}'") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No such product present in database" ) + success(f"Found {len(products)} products matching text '{text}'") return { "action": "get", "products": [ProductView.model_validate(product).model_dump() for product in products] @@ -161,14 +171,17 @@ async def get_products_internal( :raises HTTPException: If no products are associated with the authenticated business, it raises 404 Not Found. """ + general(f"Searching products for business {business_auth.uuid} with skip={skip}, limit={limit}") query = select(Product).where(Product.business_id == business_auth.uuid).options(selectinload("*")).offset(skip).limit(limit) result = await db.execute(query) products = result.scalars().all() if not products: + warning(f"No products found for business {business_auth.email}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No product present in database" ) + success(f"Found {len(products)} products for business {business_auth.email}") return { "action": "get", "products": [ProductViewInternal.model_validate(product).model_dump() for product in products] @@ -194,14 +207,17 @@ async def get_product_by_uuid( If no products with the given UUID is associated with the authenticated business, it raises 404 Not Found. """ + general(f"Searching for product {product_id} for business {business_auth.uuid}") query = select(Product).where(and_(Product.uuid == product_id, Product.business_id == business_auth.uuid)).options(selectinload("*")) result = await db.execute(query) product_by_uuid = result.scalar_one_or_none() if not product_by_uuid: + warning(f"Product {product_id} not found for business {business_auth.uuid}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Product not present in database" ) + success(f"Found product {product_id} for business {business_auth.uuid}") return { "action": "get", "product": ProductViewInternal.model_validate(product_by_uuid) @@ -228,10 +244,12 @@ async def delete_product( raises 404 Not Found. - If there are other database errors, it returns a 500 Internal Server Error. """ + general(f"Deleting product {product_id} for business {business_auth.uuid}") query = select(Product).where(and_(Product.uuid == product_id, Product.business_id == business_auth.uuid)).options(selectinload("*")) result = await db.execute(query) product_to_delete = result.scalar_one_or_none() if not product_to_delete: + warning(f"Product {product_id} not found for deletion for business {business_auth.uuid}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Product not present in database" @@ -246,10 +264,12 @@ async def delete_product( interactions due to which mocking one part wont produce the desired result. Thus, we will keep it uncovered until a alternative can be made for testing this exception block. """ + failure(f"Product deletion failed for {product_id} for business {business_auth.uuid}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Product {product_id} deleted successfully for business {business_auth.uuid}") return { "action": "delete", "product": ProductViewInternal.model_validate(product_to_delete).model_dump() @@ -277,10 +297,12 @@ async def update_product( raises 404 Not Found. - If there are other database errors, it returns a 500 Internal Server Error. """ + general(f"Updating product {product_id} for business {business_auth.uuid}") query = select(Product).where(and_(Product.uuid == product_id, Product.business_id == business_auth.uuid)).options(selectinload("*")) result = await db.execute(query) product_to_update = result.scalar_one_or_none() if not product_to_update: + warning(f"Product {product_id} not found for update for business {business_auth.uuid}") raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Product not present in database" @@ -314,10 +336,12 @@ async def update_product( interactions due to which mocking one part wont produce the desired result. Thus, we will keep it uncovered until a alternative can be made for testing this exception block. """ + failure(f"Product update failed for {product_id} for business {business_auth.uuid} with unexpected error") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Product {product_id} updated successfully for business {business_auth.uuid}") return { "action": "put", "product": ProductViewInternal.model_validate(product_to_update).model_dump() diff --git a/fastapi_ecom/utils/auth.py b/fastapi_ecom/utils/auth.py index bc80b83..783beb7 100644 --- a/fastapi_ecom/utils/auth.py +++ b/fastapi_ecom/utils/auth.py @@ -5,6 +5,7 @@ from fastapi_ecom.database.models.business import Business from fastapi_ecom.database.models.customer import Customer from fastapi_ecom.utils.basic_auth import verify_basic_business_cred, verify_basic_customer_cred +from fastapi_ecom.utils.logging_setup import failure from fastapi_ecom.utils.oauth import verify_oauth_business_cred, verify_oauth_customer_cred @@ -32,6 +33,7 @@ async def verify_cust_cred( if oauth_customer: return oauth_customer + failure("Customer authentication failed") raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, detail = "Not Authenticated", @@ -61,6 +63,7 @@ async def verify_business_cred( if oauth_business: return oauth_business + failure("Business authentication failed") raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, detail = "Not Authenticated", diff --git a/fastapi_ecom/utils/basic_auth.py b/fastapi_ecom/utils/basic_auth.py index c5f401b..7c9123b 100644 --- a/fastapi_ecom/utils/basic_auth.py +++ b/fastapi_ecom/utils/basic_auth.py @@ -8,6 +8,7 @@ from fastapi_ecom.database.db_setup import get_db from fastapi_ecom.database.models.business import Business from fastapi_ecom.database.models.customer import Customer +from fastapi_ecom.utils.logging_setup import success, warning # Initialize HTTP Basic Authentication. # This will prompt users for a username and password when accessing secured endpoints. @@ -33,10 +34,13 @@ async def verify_basic_customer_cred(credentials: HTTPBasicCredentials | None = customer_by_email = result.scalar_one_or_none() if not customer_by_email: + warning(f"No customer account found for email: {credentials.username}") return None elif not bcrypt.checkpw(credentials.password.encode('utf-8'), customer_by_email.password.encode('utf-8')): + warning(f"Invalid password for customer: {credentials.username}") return None else: + success(f"Customer authenticated via basic auth: {credentials.username}") return customer_by_email async def verify_basic_business_cred(credentials: HTTPBasicCredentials | None = Depends(security), db: AsyncSession = Depends(get_db)) -> Business: @@ -59,8 +63,11 @@ async def verify_basic_business_cred(credentials: HTTPBasicCredentials | None = business_by_email = result.scalar_one_or_none() if not business_by_email: + warning(f"No business account found for email: {credentials.username}") return None elif not bcrypt.checkpw(credentials.password.encode('utf-8'), business_by_email.password.encode('utf-8')): + warning(f"Invalid password for business: {credentials.username}") return None else: + success(f"Business authenticated via basic auth: {credentials.username}") return business_by_email diff --git a/fastapi_ecom/utils/logging_setup.py b/fastapi_ecom/utils/logging_setup.py new file mode 100644 index 0000000..98cc26b --- /dev/null +++ b/fastapi_ecom/utils/logging_setup.py @@ -0,0 +1,21 @@ +from click import style + +from fastapi_ecom.config.config import logger + +SUCCESS = style("PASS:", fg="green", bold=True) +FAILURE = style("FAIL:", fg="red", bold=True) +WARNING = style("WARN:", fg="yellow", bold=True) +GENERAL = style("INFO:", fg="white", bold=True) +STDS = " " + +def success(message): + logger.info(SUCCESS + STDS + style(message, fg="green", bold=True)) + +def failure(message): + logger.error(FAILURE + STDS + style(message, fg="red", bold=True)) + +def warning(message): + logger.warning(WARNING + STDS + style(message, fg="yellow", bold=True)) + +def general(message): + logger.info(GENERAL + STDS + message) diff --git a/fastapi_ecom/utils/oauth.py b/fastapi_ecom/utils/oauth.py index d86d724..3e9308c 100644 --- a/fastapi_ecom/utils/oauth.py +++ b/fastapi_ecom/utils/oauth.py @@ -14,6 +14,7 @@ from fastapi_ecom.database.db_setup import get_db from fastapi_ecom.database.models.business import Business from fastapi_ecom.database.models.customer import Customer +from fastapi_ecom.utils.logging_setup import failure, general, success, warning server_metadata_url = "https://accounts.google.com/.well-known/openid-configuration" @@ -71,12 +72,16 @@ async def current_user(token: str = Depends(oidc)) -> OIDCUser | None: try: token_type, token = token.split(" ", 1) except ValueError: + warning("Invalid OAuth token - Token Issue") return None if token_type.lower() != "bearer": + warning("Invalid OAuth token - Non Bearer") return None try: userinfo = await oauth.google.userinfo(token={"access_token": token}) + success(f"OAuth token validated successfully for user: {userinfo.get('email')}") except Exception: + warning("OAuth token validation failed") return None return OIDCUser.from_userinfo(userinfo) @@ -103,6 +108,7 @@ async def verify_oauth_customer_cred(oidc: OIDCUser = Depends(current_user), db: result = await db.execute(query) customer_by_email = result.scalar_one_or_none() if customer_by_email: + general(f"Updating existing customer with OAuth info: {oidc.email}") customer_by_email.oauth_provider = "google" customer_by_email.oauth_id = oidc.sub customer_by_email.oauth_email = oidc.email @@ -113,6 +119,7 @@ async def verify_oauth_customer_cred(oidc: OIDCUser = Depends(current_user), db: await db.flush() except Exception as expt: #pragma: no cover # HTTP status code 500 is already tested in other parts of the codebase + failure("Failed to update customer details in database due to unexpected error") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." @@ -122,6 +129,7 @@ async def verify_oauth_customer_cred(oidc: OIDCUser = Depends(current_user), db: result = await db.execute(query) customer_by_email = result.scalar_one_or_none() if not customer_by_email: + general(f"Creating new customer via OAuth: {oidc.email}") customer_by_email = Customer( email = oidc.email, name = oidc.name, @@ -136,11 +144,13 @@ async def verify_oauth_customer_cred(oidc: OIDCUser = Depends(current_user), db: try: await db.flush() except Exception as expt: #pragma: no cover - # HTTP status code 500 is already tested in other parts of the codebase + # HTTP status code 500 is already tested in other parts of the codebase + failure("Failed to create customer account in database due to unexpected error") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Customer OAuth authentication successful: {oidc.email}") return customer_by_email async def verify_oauth_business_cred(oidc: OIDCUser = Depends(current_user), db: AsyncSession = Depends(get_db)) -> Business: @@ -166,6 +176,7 @@ async def verify_oauth_business_cred(oidc: OIDCUser = Depends(current_user), db: result = await db.execute(query) business_by_email = result.scalar_one_or_none() if business_by_email: + general(f"Updating existing business with OAuth info: {oidc.email}") business_by_email.oauth_provider = "google" business_by_email.oauth_id = oidc.sub business_by_email.oauth_email = oidc.email @@ -176,6 +187,7 @@ async def verify_oauth_business_cred(oidc: OIDCUser = Depends(current_user), db: await db.flush() except Exception as expt: #pragma: no cover # HTTP status code 500 is already tested in other parts of the codebase + failure("Failed to update business details in database due to unexpected error") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." @@ -185,6 +197,7 @@ async def verify_oauth_business_cred(oidc: OIDCUser = Depends(current_user), db: result = await db.execute(query) business_by_email = result.scalar_one_or_none() if not business_by_email: + general(f"Creating new business via OAuth: {oidc.email}") business_by_email = Business( email = oidc.email, name = oidc.name, @@ -199,9 +212,11 @@ async def verify_oauth_business_cred(oidc: OIDCUser = Depends(current_user), db: try: await db.flush() except Exception as expt: #pragma: no cover - # HTTP status code 500 is already tested in other parts of the codebase + # HTTP status code 500 is already tested in other parts of the codebase + failure("Failed to create business account in database due to unexpected error") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An unexpected database error occurred." ) from expt + success(f"Business OAuth authentication successful: {oidc.email}") return business_by_email diff --git a/poetry.lock b/poetry.lock index 332388c..5a192e7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1271,6 +1271,7 @@ description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ + {file = "sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc"}, {file = "sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417"}, ]