Skip to content

Commit 6418a16

Browse files
feat: add request logging via Postgres (#166)
* refactor: use context manager for request logging * fix: tolerate missing database on startup * fix: enable model preloading by default
1 parent c7fecb8 commit 6418a16

File tree

12 files changed

+123
-7
lines changed

12 files changed

+123
-7
lines changed

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Always run `pytest {{cookiecutter.project_slug}}/tests` and ensure all tests pass after any code changes, even if not explicitly requested.
2+
- Always add tests, keep your branch rebased instead of merged, and adhere to the commit message recommendations from https://cbea.ms/git-commit/.

{{cookiecutter.project_slug}}/.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ SECRET_KEY=secret
22
DEBUG=True
33
MODEL_PATH={{cookiecutter.machine_learn_model_path}}
44
MODEL_NAME={{cookiecutter.machine_learn_model_name}}
5-
MEMOIZATION_FLAG=False
5+
MEMOIZATION_FLAG=True
6+
DATABASE_URL=postgresql://postgres:postgres@db:5432/app

{{cookiecutter.project_slug}}/app/api/routes/predictor.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from core.config import INPUT_EXAMPLE
66
from fastapi import APIRouter, HTTPException
77
from fastapi.concurrency import run_in_threadpool
8+
from loguru import logger
9+
from db import SessionLocal
10+
from models.log import RequestLog
811
from models.prediction import (
912
HealthResponse,
1013
MachineLearningDataInput,
@@ -44,10 +47,24 @@ async def predict(data_input: MachineLearningDataInput):
4447
except Exception as err:
4548
raise HTTPException(status_code=500, detail=f"Exception: {err}") from err
4649

47-
return MachineLearningResponse(
50+
response = MachineLearningResponse(
4851
prediction=prediction, prediction_label=prediction_label
4952
)
5053

54+
try:
55+
with SessionLocal() as db:
56+
db.add(
57+
RequestLog(
58+
request=json.dumps(data_input.model_dump()),
59+
response=json.dumps(response.model_dump()),
60+
)
61+
)
62+
db.commit()
63+
except Exception:
64+
logger.exception("failed to log request")
65+
66+
return response
67+
5168

5269
@router.get(
5370
"/health",

{{cookiecutter.project_slug}}/app/core/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
MIN_CONNECTIONS_COUNT: int = config("MIN_CONNECTIONS_COUNT", cast=int, default=10)
1616
SECRET_KEY: Secret = config("SECRET_KEY", cast=Secret, default="")
1717
MEMOIZATION_FLAG: bool = config("MEMOIZATION_FLAG", cast=bool, default=True)
18+
DATABASE_URL: str = config("DATABASE_URL", default="sqlite:///./app.db")
1819

1920
PROJECT_NAME: str = config("PROJECT_NAME", default="{{cookiecutter.project_name}}")
2021

{{cookiecutter.project_slug}}/app/core/events.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
import joblib
44
from fastapi import FastAPI
5+
from loguru import logger
6+
from sqlalchemy.exc import OperationalError
7+
8+
from core.config import MEMOIZATION_FLAG
9+
from db import Base, engine
510

611

712
def preload_model():
@@ -15,6 +20,11 @@ def preload_model():
1520

1621
def create_start_app_handler(app: FastAPI) -> Callable:
1722
def start_app() -> None:
18-
preload_model()
23+
if MEMOIZATION_FLAG:
24+
preload_model()
25+
try:
26+
Base.metadata.create_all(bind=engine)
27+
except OperationalError:
28+
logger.exception("failed to initialize database")
1929

2030
return start_app
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from sqlalchemy import create_engine
2+
from sqlalchemy.orm import sessionmaker, declarative_base
3+
4+
from core.config import DATABASE_URL
5+
6+
engine = create_engine(DATABASE_URL)
7+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
8+
Base = declarative_base()

{{cookiecutter.project_slug}}/app/main.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
def get_application() -> FastAPI:
88
application = FastAPI(title=PROJECT_NAME, debug=DEBUG, version=VERSION)
99
application.include_router(api_router, prefix=API_PREFIX)
10-
if MEMOIZATION_FLAG:
11-
application.add_event_handler("startup", create_start_app_handler(application))
10+
application.add_event_handler("startup", create_start_app_handler(application))
1211
return application
1312

1413

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from sqlalchemy import Column, Integer, Text
2+
3+
from db import Base
4+
5+
6+
class RequestLog(Base):
7+
__tablename__ = "request_logs"
8+
9+
id = Column(Integer, primary_key=True, index=True)
10+
request = Column(Text, nullable=False)
11+
response = Column(Text, nullable=False)

{{cookiecutter.project_slug}}/docker-compose.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,19 @@ services:
1313
command: uvicorn main:app --reload --host 0.0.0.0 --port 8080
1414
volumes:
1515
- ./app:/app/
16-
- ./ml/model/:/app/ml/model/
16+
- ./ml/model/:/app/ml/model/
17+
depends_on:
18+
- db
19+
db:
20+
image: postgres:16
21+
environment:
22+
POSTGRES_USER: postgres
23+
POSTGRES_PASSWORD: postgres
24+
POSTGRES_DB: app
25+
ports:
26+
- "5432:5432"
27+
volumes:
28+
- postgres_data:/var/lib/postgresql/data
29+
30+
volumes:
31+
postgres_data:

{{cookiecutter.project_slug}}/pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ dependencies = [
1515
"joblib>=1.2.0",
1616
"scikit-learn>=1.1.3",
1717
"pandas>=2.2.3",
18-
"httpx>=0.27.0"
18+
"httpx>=0.27.0",
19+
"sqlalchemy>=2.0.0",
20+
"psycopg2-binary>=2.9.0"
1921
]
2022

2123
[project.optional-dependencies]

0 commit comments

Comments
 (0)