Skip to content

Commit 6783cfd

Browse files
committed
feat: add comprehensive logging across application
- Implement structured logging throughout the application to improve monitoring, debugging, and operational visibility. Signed-off-by: Shounak Dey <shounakdey@ymail.com>
1 parent d05c21c commit 6783cfd

File tree

14 files changed

+177
-8
lines changed

14 files changed

+177
-8
lines changed

fastapi_ecom/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from fastapi_ecom.config import config
66
from fastapi_ecom.router import business, customer, order, product
7+
from fastapi_ecom.utils.logging_setup import general
78

89
# Metadata for API tags
910
tags_metadata = [
@@ -36,6 +37,7 @@ def root() -> dict[str, str]:
3637
3738
:return: Metadata about the API, including title, description, and version.
3839
"""
40+
general("Root endpoint accessed")
3941
return{
4042
"title": "FastAPI ECOM",
4143
"description": "E-Commerce API for businesses and end users using FastAPI.",
@@ -58,6 +60,7 @@ def start_service():
5860
5961
:raises RuntimeError: If configuration parameters are missing or invalid.
6062
"""
63+
general("FastAPI server started")
6164
uvicorn.run(
6265
"fastapi_ecom.app:app",
6366
host=config.servhost,

fastapi_ecom/config/config.py.example

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from logging import getLogger
2+
from logging.config import dictConfig
3+
14
# The database name
25
database = "test_db"
36

@@ -33,3 +36,34 @@ GOOGLE_CLIENT_ID = "example.apps.googleusercontent.com"
3336

3437
# Google Client secret
3538
GOOGLE_CLIENT_SECRET = "example"
39+
40+
# The default configuration for service logging
41+
log_config = {
42+
"version": 1,
43+
"disable_existing_loggers": False,
44+
"formatters": {
45+
"standard": {
46+
"format": "%(message)s %(asctime)s",
47+
"datefmt": "[%Y-%m-%d %I:%M:%S %z]",
48+
},
49+
},
50+
"handlers": {
51+
"console": {
52+
"level": "INFO",
53+
"formatter": "standard",
54+
"class": "logging.StreamHandler",
55+
"stream": "ext://sys.stdout",
56+
},
57+
},
58+
"loggers": {
59+
"fastapi_ecom": {
60+
"level": "INFO",
61+
"handlers": ["console"],
62+
"propagate": False,
63+
},
64+
},
65+
}
66+
67+
dictConfig(log_config)
68+
69+
logger = getLogger(__name__)

fastapi_ecom/database/db_setup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
migrpath,
1313
models,
1414
)
15+
from fastapi_ecom.utils.logging_setup import general, success
1516

1617

1718
def make_database() -> None:
@@ -24,16 +25,21 @@ def make_database() -> None:
2425
- Marks the database as being at the latest migration version.
2526
"""
2627
# Use the synchronous engine to create the database schema.
28+
general("Creating database schema with synchronous engine")
2729
sync_engine = get_engine(engine="sync")
2830
baseobjc.metadata.create_all(bind=sync_engine)
31+
success("Database schema created successfully")
2932

3033
# Set up Alembic configuration for migration management.
34+
general("Setting up Alembic configuration")
3135
alembic_config = config.Config(alempath)
3236
alembic_config.set_main_option("script_location", migrpath)
3337
alembic_config.set_main_option("sqlalchemy.url", get_database_url().render_as_string(hide_password=False))
3438

3539
# Mark the database at the latest migration head.
40+
general("Marking database at latest migration head")
3641
command.stamp(alembic_config, "head")
42+
success("Database marked at migration head successfully")
3743

3844
async def get_db() -> AsyncGenerator[AsyncSession, None]:
3945
"""

fastapi_ecom/database/pydantic_schemas/customer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ class CustomerView(CustomerBase):
3535
"""
3636
email: EmailStr
3737
name: str
38-
addr_line_1: str
39-
addr_line_2: str
40-
city: str
41-
state: str
38+
addr_line_1: Optional[str]
39+
addr_line_2: Optional[str]
40+
city: Optional[str]
41+
state: Optional[str]
4242

4343

4444
class CustomerCreate(CustomerView):

fastapi_ecom/main.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from fastapi_ecom.app import start_service
44
from fastapi_ecom.database.db_setup import make_database
55
from fastapi_ecom.migrations.main import alembic_migration
6+
from fastapi_ecom.utils.logging_setup import general, success
67

78

89
@click.group(name="fastapi_ecom", help="E-Commerce API for businesses and end users using FastAPI.")
@@ -26,7 +27,9 @@ def setup() -> None:
2627
2728
:return: None
2829
"""
30+
general("Setting up database schema")
2931
make_database()
32+
success("Database schema setup completed")
3033

3134
@main.command(name="start", help="Start the FastAPI eComm application")
3235
def start() -> None:
@@ -37,6 +40,7 @@ def start() -> None:
3740
3841
:return: None
3942
"""
43+
general("Starting FastAPI eComm application")
4044
start_service()
4145

4246
@main.command(name="create-migration", help="Create a new migration script")
@@ -51,7 +55,9 @@ def create_migration(comment: str, autogenerate: bool) -> None:
5155
5256
:return: None
5357
"""
58+
general(f"Creating migration with comment: {comment}")
5459
alembic_migration.create(comment, autogenerate)
60+
success(f"Migration created successfully: {comment}")
5561

5662
@main.command(name="db-version", help="Show the current database version")
5763
def db_version() -> None:
@@ -62,6 +68,7 @@ def db_version() -> None:
6268
6369
:return: None
6470
"""
71+
general("Checking database version")
6572
alembic_migration.db_version()
6673

6774
@main.command(name="upgrade-db", help="Upgrade the database to a specific version")
@@ -75,7 +82,9 @@ def upgrade_db(version: str) -> None:
7582
7683
:return: None
7784
"""
85+
general(f"Upgrading database to version: {version}")
7886
alembic_migration.upgrade(version)
87+
success(f"Database upgraded to version: {version}")
7988

8089
@main.command(name="downgrade-db", help="Downgrade the database to a specific version")
8190
@click.argument("version", type=str)
@@ -88,4 +97,6 @@ def downgrade_db(version: str) -> None:
8897
8998
:return: None
9099
"""
100+
general(f"Downgrading database to version: {version}")
91101
alembic_migration.downgrade(version)
102+
success(f"Database downgraded to version: {version}")

fastapi_ecom/router/business.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
BusinessView,
2020
)
2121
from fastapi_ecom.utils.auth import verify_business_cred
22+
from fastapi_ecom.utils.logging_setup import failure, general, success, warning
2223

2324
router = APIRouter(prefix="/business")
2425

@@ -50,19 +51,23 @@ async def create_business(business: BusinessCreate, db: AsyncSession = Depends(g
5051
state=business.state.strip(),
5152
uuid=uuid4().hex[0:8] # Assign UUID manually; One UUID per transaction
5253
)
54+
general(f"Adding account for business in database: {business.email}")
5355
db.add(db_business)
5456
try:
5557
await db.flush()
5658
except IntegrityError as expt:
59+
failure(f"Business account creation failed - Uniqueness constraint violation for email: {business.email}")
5760
raise HTTPException(
5861
status_code=status.HTTP_409_CONFLICT,
5962
detail="Uniqueness constraint failed - Please try again"
6063
) from expt
6164
except Exception as expt:
65+
failure(f"Business account creation failed with unexpected error for email: {business.email}")
6266
raise HTTPException(
6367
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
6468
detail="An unexpected database error occurred."
6569
) from expt
70+
success(f"Business account created successfully with email: {business.email}")
6671
return {
6772
"action": "post",
6873
"business": BusinessView.model_validate(db_business).model_dump()
@@ -77,6 +82,7 @@ async def get_business_me(business_auth = Depends(verify_business_cred)) -> dict
7782
7883
:return: Dictionary containing the action type and the authenticated business's email.
7984
"""
85+
general(f"Business authentication successful for: {business_auth.email}")
8086
return {
8187
"action": "get",
8288
"email": business_auth.email
@@ -101,14 +107,17 @@ async def get_businesses(
101107
:raises HTTPException:
102108
- If no business exist in the database, it raises 404 Not Found.
103109
"""
110+
general(f"Searching businesses with skip={skip}, limit={limit}")
104111
query = select(Business).options(selectinload("*")).offset(skip).limit(limit)
105112
result = await db.execute(query)
106113
businesses = result.scalars().all()
107114
if not businesses:
115+
warning("No businesses found in database")
108116
raise HTTPException(
109117
status_code=status.HTTP_404_NOT_FOUND,
110118
detail="No business present in database"
111119
)
120+
success(f"Found {len(businesses)} businesses")
112121
return {
113122
"action": "get",
114123
"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
129138
- If a uniqueness constraint fails, it returns a 409 Conflict status.
130139
- If there are other database errors, it returns a 500 Internal Server Error.
131140
"""
141+
general(f"Deleting business account: {business_auth.email}")
132142
query = select(Business).where(Business.uuid == business_auth.uuid).options(selectinload("*"))
133143
result = await db.execute(query)
134144
business_to_delete = result.scalar_one_or_none()
@@ -142,10 +152,12 @@ async def delete_business(db: AsyncSession = Depends(get_db), business_auth = De
142152
interactions due to which mocking one part wont produce the desired result. Thus,
143153
we will keep it uncovered until a alternative can be made for testing this exception block.
144154
"""
155+
failure(f"Business account deletion failed for: {business_auth.email}")
145156
raise HTTPException(
146157
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
147158
detail="An unexpected database error occurred."
148159
) from expt
160+
success(f"Business account deleted successfully: {business_auth.email}")
149161
return {
150162
"action": "delete",
151163
"business": BusinessView.model_validate(business_to_delete).model_dump()
@@ -171,6 +183,8 @@ async def update_business(
171183
- If a uniqueness constraint fails, it returns a 409 Conflict status.
172184
- If there are other database errors, it returns a 500 Internal Server Error.
173185
"""
186+
business_email = business_auth.email # Capture email before potential database errors
187+
general(f"Updating details of business: {business_email}")
174188
query = select(Business).where(Business.uuid == business_auth.uuid).options(selectinload("*"))
175189
result = await db.execute(query)
176190
business_to_update = result.scalar_one_or_none()
@@ -190,6 +204,7 @@ async def update_business(
190204
try:
191205
await db.flush()
192206
except IntegrityError as expt:
207+
failure(f"Business update failed - Uniqueness constraint violation for: {business_email}")
193208
raise HTTPException(
194209
status_code=status.HTTP_409_CONFLICT,
195210
detail="Uniqueness constraint failed - Please try again"
@@ -200,10 +215,12 @@ async def update_business(
200215
interactions due to which mocking one part wont produce the desired result. Thus,
201216
we will keep it uncovered until a alternative can be made for testing this exception block.
202217
"""
218+
failure(f"Business update failed with unexpected error for: {business_email}")
203219
raise HTTPException(
204220
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
205221
detail="An unexpected database error occurred."
206222
) from expt
223+
success(f"Business details updated successfully: {business_email}")
207224
return {
208225
"action": "put",
209226
"business": BusinessView.model_validate(business_to_update).model_dump()

fastapi_ecom/router/customer.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
CustomerView,
2020
)
2121
from fastapi_ecom.utils.auth import verify_cust_cred
22+
from fastapi_ecom.utils.logging_setup import failure, general, success, warning
2223

2324
router = APIRouter(prefix="/customer")
2425

@@ -50,19 +51,23 @@ async def create_customer(customer: CustomerCreate, db: AsyncSession = Depends(g
5051
state = customer.state.strip(),
5152
uuid = uuid4().hex[0:8] # Assign UUID manually; One UUID per transaction
5253
)
54+
general(f"Adding account for customer in database: {customer.email}")
5355
db.add(db_customer)
5456
try:
5557
await db.flush()
5658
except IntegrityError as expt:
57-
raise HTTPException(
59+
failure(f"Customer account creation failed - Uniqueness constraint violation for email: {customer.email}")
60+
raise HTTPException(
5861
status_code=status.HTTP_409_CONFLICT,
5962
detail="Uniqueness constraint failed - Please try again"
6063
) from expt
6164
except Exception as expt:
65+
failure(f"Customer account creation failed with unexpected error for email: {customer.email}")
6266
raise HTTPException(
6367
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
6468
detail="An unexpected database error occurred."
6569
) from expt
70+
success(f"Customer account created successfully with email: {customer.email}")
6671
return {
6772
"action": "post",
6873
"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
7782
7883
:return: Dictionary containing the action type and the authenticated customer's email.
7984
"""
85+
general(f"Customer authentication successful for: {customer_auth.email}")
8086
return {
8187
"action": "get",
8288
"email": customer_auth.email
@@ -101,14 +107,17 @@ async def get_customers(
101107
:raises HTTPException:
102108
- If no customer exist in the database, it raises 404 Not Found.
103109
"""
110+
general(f"Searching customers with skip={skip}, limit={limit}")
104111
query = select(Customer).options(selectinload("*")).offset(skip).limit(limit)
105112
result = await db.execute(query)
106113
customers = result.scalars().all()
107114
if not customers:
115+
warning("No customers found in database")
108116
raise HTTPException(
109117
status_code=status.HTTP_404_NOT_FOUND,
110118
detail="No customer present in database"
111119
)
120+
success(f"Found {len(customers)} customers")
112121
return {
113122
"action": "get",
114123
"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
122131
:param db: Active asynchronous database session dependency.
123132
:param customer_auth: Authenticated customer object.
124133
125-
:return: Dictionary containing the action type and the deleted customer's data.
134+
:return: Dictionary containing the action type and the deleted customer's data, validated and
135+
serialized using the `CustomerView` schema.
126136
127137
:raises HTTPException:
128138
- If a uniqueness constraint fails, it returns a 409 Conflict status.
129139
- If there are other database errors, it returns a 500 Internal Server Error.
130140
"""
141+
general(f"Deleting customer account: {customer_auth.email}")
131142
query = select(Customer).where(Customer.uuid == customer_auth.uuid).options(selectinload("*"))
132143
result = await db.execute(query)
133144
customer_to_delete = result.scalar_one_or_none()
@@ -141,10 +152,12 @@ async def delete_customer(db: AsyncSession = Depends(get_db), customer_auth = De
141152
interactions due to which mocking one part wont produce the desired result. Thus,
142153
we will keep it uncovered until a alternative can be made for testing this exception block.
143154
"""
155+
failure(f"Customer account deletion failed for: {customer_auth.email}")
144156
raise HTTPException(
145157
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
146158
detail="An unexpected database error occurred."
147159
) from expt
160+
success(f"Customer account deleted successfully: {customer_auth.email}")
148161
return {
149162
"action": "delete",
150163
"customer": CustomerView.model_validate(customer_to_delete).model_dump()
@@ -165,6 +178,8 @@ async def update_customer(customer: CustomerUpdate, db: AsyncSession = Depends(g
165178
- If a uniqueness constraint fails, it returns a 409 Conflict status.
166179
- If there are other database errors, it returns a 500 Internal Server Error.
167180
"""
181+
customer_email = customer_auth.email # Capture email before potential database errors
182+
general(f"Updating details of customer: {customer_email}")
168183
query = select(Customer).where(Customer.uuid == customer_auth.uuid).options(selectinload("*"))
169184
result = await db.execute(query)
170185
customer_to_update = result.scalar_one_or_none()
@@ -184,6 +199,7 @@ async def update_customer(customer: CustomerUpdate, db: AsyncSession = Depends(g
184199
try:
185200
await db.flush()
186201
except IntegrityError as expt:
202+
failure(f"Customer update failed - Uniqueness constraint violation for: {customer_email}")
187203
raise HTTPException(
188204
status_code=status.HTTP_409_CONFLICT,
189205
detail="Uniqueness constraint failed - Please try again"
@@ -194,10 +210,12 @@ async def update_customer(customer: CustomerUpdate, db: AsyncSession = Depends(g
194210
interactions due to which mocking one part wont produce the desired result. Thus,
195211
we will keep it uncovered until a alternative can be made for testing this exception block.
196212
"""
213+
failure(f"Customer update failed with unexpected error for: {customer_email}")
197214
raise HTTPException(
198215
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
199216
detail="An unexpected database error occurred."
200217
) from expt
218+
success(f"Customer details updated successfully: {customer_email}")
201219
return {
202220
"action": "put",
203221
"customer": CustomerView.model_validate(customer_to_update).model_dump()

0 commit comments

Comments
 (0)