Skip to content

Commit 6c54aee

Browse files
authored
Merge pull request #208 from grillazz/198-add-simple-caching
198 add structlog
2 parents 9cdc85a + 8e7692b commit 6c54aee

File tree

14 files changed

+158
-92
lines changed

14 files changed

+158
-92
lines changed

app/api/health.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
from starlette.concurrency import run_in_threadpool
77

88
from app.services.smtp import SMTPEmailService
9-
from app.utils.logging import AppLogger
9+
from app.utils.logging import AppStructLogger
1010

11-
logger = AppLogger().get_logger()
11+
logger = AppStructLogger().get_logger()
1212

1313
router = APIRouter()
1414

app/api/ml.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from fastapi.responses import StreamingResponse
55

66
from app.services.llm import get_llm_service
7-
from app.utils.logging import AppLogger
7+
from app.utils.logging import AppStructLogger
88

9-
logger = AppLogger().get_logger()
9+
logger = AppStructLogger().get_logger()
1010

1111
router = APIRouter()
1212

app/api/stuff.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from app.database import get_db
66
from app.models.stuff import Stuff
77
from app.schemas.stuff import StuffResponse, StuffSchema
8-
from app.utils.logging import AppLogger
8+
from app.utils.logging import AppStructLogger
99

10-
logger = AppLogger().get_logger()
10+
logger = AppStructLogger().get_logger()
1111

1212
router = APIRouter(prefix="/v1/stuff")
1313

app/api/user.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
from app.models.user import User
88
from app.schemas.user import TokenResponse, UserLogin, UserResponse, UserSchema
99
from app.services.auth import create_access_token
10-
from app.utils.logging import AppLogger
10+
from app.utils.logging import AppStructLogger
1111

12-
logger = AppLogger().get_logger()
12+
logger = AppStructLogger().get_logger()
1313

1414
router = APIRouter(prefix="/v1/user")
1515

app/database.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
44

55
from app.config import settings as global_settings
6-
from app.utils.logging import AppLogger
6+
from app.utils.logging import AppStructLogger
77

8-
logger = AppLogger().get_logger()
8+
logger = AppStructLogger().get_logger()
99

1010
engine = create_async_engine(
1111
global_settings.asyncpg_url.unicode_string(),

app/main.py

Lines changed: 47 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
from pathlib import Path
33

44
import asyncpg
5-
6-
# from apscheduler import AsyncScheduler
7-
# from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
8-
# from apscheduler.eventbrokers.redis import RedisEventBroker
95
from fastapi import Depends, FastAPI, Request
106
from fastapi.responses import HTMLResponse
117
from fastapi.templating import Jinja2Templates
@@ -17,75 +13,66 @@
1713
from app.api.stuff import router as stuff_router
1814
from app.api.user import router as user_router
1915
from app.config import settings as global_settings
20-
21-
# from app.database import engine
2216
from app.redis import get_redis
2317
from app.services.auth import AuthBearer
18+
from app.utils.logging import AppStructLogger
2419

25-
# from app.services.scheduler import SchedulerMiddleware
26-
from app.utils.logging import AppLogger
27-
28-
logger = AppLogger().get_logger()
29-
20+
logger = AppStructLogger().get_logger()
3021
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "templates")
3122

32-
3323
@asynccontextmanager
34-
async def lifespan(_app: FastAPI):
35-
# Load the redis connection
36-
_app.redis = await get_redis()
37-
38-
_postgres_dsn = global_settings.postgres_url.unicode_string()
39-
24+
async def lifespan(app: FastAPI):
25+
app.redis = await get_redis()
26+
postgres_dsn = global_settings.postgres_url.unicode_string()
4027
try:
41-
# TODO: cache with the redis connection
42-
# Initialize the postgres connection pool
43-
_app.postgres_pool = await asyncpg.create_pool(
44-
dsn=_postgres_dsn,
28+
app.postgres_pool = await asyncpg.create_pool(
29+
dsn=postgres_dsn,
4530
min_size=5,
4631
max_size=20,
4732
)
48-
logger.info(f"Postgres pool created: {_app.postgres_pool.get_idle_size()=}")
33+
logger.info("Postgres pool created", idle_size=app.postgres_pool.get_idle_size())
4934
yield
5035
finally:
51-
# close redis connection and release the resources
52-
await _app.redis.close()
53-
# close postgres connection pool and release the resources
54-
await _app.postgres_pool.close()
55-
56-
57-
app = FastAPI(title="Stuff And Nonsense API", version="0.19.0", lifespan=lifespan)
58-
59-
app.include_router(stuff_router)
60-
app.include_router(nonsense_router)
61-
app.include_router(shakespeare_router)
62-
app.include_router(user_router)
63-
app.include_router(ml_router, prefix="/v1/ml", tags=["ML"])
64-
65-
66-
app.include_router(health_router, prefix="/v1/public/health", tags=["Health, Public"])
67-
app.include_router(
68-
health_router,
69-
prefix="/v1/health",
70-
tags=["Health, Bearer"],
71-
dependencies=[Depends(AuthBearer())],
72-
)
73-
74-
75-
@app.get("/index", response_class=HTMLResponse)
76-
def get_index(request: Request):
77-
return templates.TemplateResponse("index.html", {"request": request})
78-
79-
36+
await app.redis.close()
37+
await app.postgres_pool.close()
38+
39+
def create_app() -> FastAPI:
40+
app = FastAPI(
41+
title="Stuff And Nonsense API",
42+
version="0.19.0",
43+
lifespan=lifespan,
44+
)
45+
app.include_router(stuff_router)
46+
app.include_router(nonsense_router)
47+
app.include_router(shakespeare_router)
48+
app.include_router(user_router)
49+
app.include_router(ml_router, prefix="/v1/ml", tags=["ML"])
50+
app.include_router(health_router, prefix="/v1/public/health", tags=["Health, Public"])
51+
app.include_router(
52+
health_router,
53+
prefix="/v1/health",
54+
tags=["Health, Bearer"],
55+
dependencies=[Depends(AuthBearer())],
56+
)
57+
58+
@app.get("/index", response_class=HTMLResponse)
59+
def get_index(request: Request):
60+
return templates.TemplateResponse("index.html", {"request": request})
61+
62+
return app
63+
64+
app = create_app()
65+
66+
# --- Unused/experimental code and TODOs ---
67+
# from apscheduler import AsyncScheduler
68+
# from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
69+
# from apscheduler.eventbrokers.redis import RedisEventBroker
70+
# from app.database import engine
71+
# from app.services.scheduler import SchedulerMiddleware
8072
# _scheduler_data_store = SQLAlchemyDataStore(engine, schema="scheduler")
81-
# _scheduler_event_broker = RedisEventBroker(
82-
# client_or_url=global_settings.redis_url.unicode_string()
83-
# )
73+
# _scheduler_event_broker = RedisEventBroker(client_or_url=global_settings.redis_url.unicode_string())
8474
# _scheduler_himself = AsyncScheduler(_scheduler_data_store, _scheduler_event_broker)
85-
#
8675
# app.add_middleware(SchedulerMiddleware, scheduler=_scheduler_himself)
87-
88-
89-
# TODO: every not GET meth should reset cache
90-
# TODO: every scheduler task which needs to act on database should have access to connection pool via request - maybe ?
76+
# TODO: every non-GET method should reset cache
77+
# TODO: scheduler tasks needing DB should access connection pool via request
9178
# TODO: https://stackoverflow.com/questions/16053364/make-sure-only-one-worker-launches-the-apscheduler-event-in-a-pyramid-web-app-ru

app/models/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
from sqlalchemy.ext.asyncio import AsyncSession
77
from sqlalchemy.orm import DeclarativeBase, declared_attr
88

9-
from app.utils.logging import AppLogger
9+
from app.utils.logging import AppStructLogger
1010

11-
logger = AppLogger().get_logger()
11+
logger = AppStructLogger().get_logger()
1212

1313

1414
class Base(DeclarativeBase):

app/services/auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
from app.config import settings as global_settings
88
from app.models.user import User
9-
from app.utils.logging import AppLogger
9+
from app.utils.logging import AppStructLogger
1010

11-
logger = AppLogger().get_logger()
11+
logger = AppStructLogger().get_logger()
1212

1313

1414
async def get_from_redis(request: Request, key: str):

app/services/smtp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
from pydantic import EmailStr
88

99
from app.config import settings as global_settings
10-
from app.utils.logging import AppLogger
10+
from app.utils.logging import AppStructLogger
1111
from app.utils.singleton import SingletonMetaNoArgs
1212

13-
logger = AppLogger().get_logger()
13+
logger = AppStructLogger().get_logger()
1414

1515

1616
@define

app/utils/logging.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,65 @@
11
import logging
2+
import os
3+
from logging.handlers import RotatingFileHandler
4+
from pathlib import Path
25

3-
from rich.console import Console
4-
from rich.logging import RichHandler
6+
import orjson
7+
import structlog
8+
from attrs import define, field
9+
from whenever._whenever import Instant
510

6-
from app.utils.singleton import SingletonMeta
11+
from app.utils.singleton import SingletonMetaNoArgs
712

813

9-
class AppLogger(metaclass=SingletonMeta):
10-
_logger = None
14+
# TODO: merge this wrapper with the one in structlog under one hood of AppLogger
15+
class BytesToTextIOWrapper:
16+
def __init__(self, handler, encoding="utf-8"):
17+
self.handler = handler
18+
self.encoding = encoding
1119

12-
def __init__(self):
13-
self._logger = logging.getLogger(__name__)
20+
def write(self, b):
21+
if isinstance(b, bytes):
22+
self.handler.stream.write(b.decode(self.encoding))
23+
else:
24+
self.handler.stream.write(b)
25+
self.handler.flush()
26+
27+
def flush(self):
28+
self.handler.flush()
29+
30+
def close(self):
31+
self.handler.close()
1432

15-
def get_logger(self):
16-
return self._logger
1733

34+
@define(slots=True)
35+
class AppStructLogger(metaclass=SingletonMetaNoArgs):
36+
_logger: structlog.BoundLogger = field(init=False)
1837

19-
class RichConsoleHandler(RichHandler):
20-
def __init__(self, width=200, style=None, **kwargs):
21-
super().__init__(
22-
console=Console(color_system="256", width=width, style=style, stderr=True),
23-
**kwargs,
38+
def __attrs_post_init__(self):
39+
_log_date = Instant.now().py_datetime().strftime("%Y%m%d")
40+
_log_path = Path(f"{_log_date}_{os.getpid()}.log")
41+
_handler = RotatingFileHandler(
42+
filename=_log_path,
43+
mode="a",
44+
maxBytes=10 * 1024 * 1024,
45+
backupCount=5,
46+
encoding="utf-8"
2447
)
48+
structlog.configure(
49+
cache_logger_on_first_use=True,
50+
wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
51+
processors=[
52+
structlog.contextvars.merge_contextvars,
53+
structlog.processors.add_log_level,
54+
structlog.processors.format_exc_info,
55+
structlog.processors.TimeStamper(fmt="iso", utc=True),
56+
structlog.processors.JSONRenderer(serializer=orjson.dumps),
57+
],
58+
logger_factory=structlog.BytesLoggerFactory(
59+
file=BytesToTextIOWrapper(_handler)
60+
)
61+
)
62+
self._logger = structlog.get_logger()
63+
64+
def get_logger(self) -> structlog.BoundLogger:
65+
return self._logger

0 commit comments

Comments
 (0)