mirror of
https://github.com/grillazz/fastapi-sqlalchemy-asyncpg.git
synced 2025-08-26 16:40:40 +03:00
Merge pull request #86 from grillazz/75-revisit-logging
75 revisit logging
This commit is contained in:
commit
3cea4f784d
@ -34,7 +34,7 @@ ENV PYTHONPATH=/home/code/ PYTHONHASHSEED=0
|
||||
COPY tests/ tests/
|
||||
COPY app/ app/
|
||||
COPY alembic/ alembic/
|
||||
COPY .env alembic.ini ./
|
||||
COPY .env alembic.ini config.ini ./
|
||||
|
||||
# create a non-root user and switch to it, for security.
|
||||
RUN addgroup --system --gid 1001 "app-user"
|
||||
|
1
Makefile
1
Makefile
@ -43,7 +43,6 @@ lint: ## Lint project code.
|
||||
|
||||
.PHONY: format
|
||||
format: ## Format project code.
|
||||
isort app tests
|
||||
black app tests --line-length=120
|
||||
|
||||
.PHONY: slim-build
|
||||
|
@ -58,6 +58,7 @@ Hope you enjoy it.
|
||||
- 3 OCT 2022 poetry added to project
|
||||
- 12 NOV 2022 ruff implemented to project as linting tool
|
||||
- 14 FEB 2023 bump project to Python 3.11
|
||||
- 10 APR 2023 implement logging with rich
|
||||
|
||||
### Local development with poetry
|
||||
|
||||
|
@ -9,9 +9,7 @@ router = APIRouter(prefix="/v1/nonsense")
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=NonsenseResponse)
|
||||
async def create_nonsense(
|
||||
payload: NonsenseSchema, db_session: AsyncSession = Depends(get_db)
|
||||
):
|
||||
async def create_nonsense(payload: NonsenseSchema, db_session: AsyncSession = Depends(get_db)):
|
||||
nonsense = Nonsense(**payload.dict())
|
||||
await nonsense.save(db_session)
|
||||
return nonsense
|
||||
@ -40,3 +38,13 @@ async def update_nonsense(
|
||||
nonsense = await Nonsense.find(db_session, name)
|
||||
await nonsense.update(db_session, **payload.dict())
|
||||
return nonsense
|
||||
|
||||
|
||||
@router.post("/", response_model=NonsenseResponse)
|
||||
async def merge_nonsense(
|
||||
payload: NonsenseSchema,
|
||||
db_session: AsyncSession = Depends(get_db),
|
||||
):
|
||||
nonsense = Nonsense(**payload.dict())
|
||||
await nonsense.save_or_update(db_session)
|
||||
return nonsense
|
||||
|
@ -5,39 +5,29 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.database import get_db
|
||||
from app.models.stuff import Stuff
|
||||
from app.schemas.stuff import StuffResponse, StuffSchema
|
||||
from app.utils import get_logger
|
||||
from app.logging import AppLogger
|
||||
|
||||
logger = AppLogger.__call__().get_logger()
|
||||
|
||||
router = APIRouter(prefix="/v1/stuff")
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.post("/add_many", status_code=status.HTTP_201_CREATED)
|
||||
async def create_multi_stuff(
|
||||
payload: list[StuffSchema], db_session: AsyncSession = Depends(get_db)
|
||||
):
|
||||
async def create_multi_stuff(payload: list[StuffSchema], db_session: AsyncSession = Depends(get_db)):
|
||||
try:
|
||||
stuff_instances = [
|
||||
Stuff(name=stuf.name, description=stuf.description) for stuf in payload
|
||||
]
|
||||
stuff_instances = [Stuff(name=stuf.name, description=stuf.description) for stuf in payload]
|
||||
db_session.add_all(stuff_instances)
|
||||
await db_session.commit()
|
||||
except SQLAlchemyError as ex:
|
||||
# logger.exception(ex)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
|
||||
) from ex
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex
|
||||
else:
|
||||
logger.info(
|
||||
f"{len(stuff_instances)} instances of Stuff inserted into database."
|
||||
)
|
||||
logger.info(f"{len(stuff_instances)} instances of Stuff inserted into database.")
|
||||
return True
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED, response_model=StuffResponse)
|
||||
async def create_stuff(
|
||||
payload: StuffSchema, db_session: AsyncSession = Depends(get_db)
|
||||
):
|
||||
async def create_stuff(payload: StuffSchema, db_session: AsyncSession = Depends(get_db)):
|
||||
stuff = Stuff(name=payload.name, description=payload.description)
|
||||
await stuff.save(db_session)
|
||||
return stuff
|
||||
|
@ -19,4 +19,5 @@ class Settings(BaseSettings):
|
||||
def get_settings():
|
||||
return Settings()
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
|
@ -4,11 +4,10 @@ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app import config
|
||||
from app.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
from app.logging import AppLogger
|
||||
|
||||
global_settings = config.get_settings()
|
||||
logger = AppLogger.__call__().get_logger()
|
||||
|
||||
engine = create_async_engine(
|
||||
global_settings.asyncpg_url,
|
||||
@ -18,13 +17,11 @@ engine = create_async_engine(
|
||||
|
||||
# expire_on_commit=False will prevent attributes from being expired
|
||||
# after commit.
|
||||
AsyncSessionFactory = sessionmaker(
|
||||
engine, autoflush=False, expire_on_commit=False, class_=AsyncSession
|
||||
)
|
||||
AsyncSessionFactory = sessionmaker(engine, autoflush=False, expire_on_commit=False, class_=AsyncSession)
|
||||
|
||||
|
||||
# Dependency
|
||||
async def get_db() -> AsyncGenerator:
|
||||
async with AsyncSessionFactory() as session:
|
||||
logger.debug(f"ASYNC Pool: {engine.pool.status()}")
|
||||
# logger.debug(f"ASYNC Pool: {engine.pool.status()}")
|
||||
yield session
|
||||
|
22
app/logging.py
Normal file
22
app/logging.py
Normal file
@ -0,0 +1,22 @@
|
||||
import logging
|
||||
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
|
||||
|
||||
from app.utils import SingletonMeta
|
||||
|
||||
|
||||
class AppLogger(metaclass=SingletonMeta):
|
||||
_logger = None
|
||||
|
||||
def __init__(self):
|
||||
self._logger = logging.getLogger(__name__)
|
||||
|
||||
def get_logger(self):
|
||||
return self._logger
|
||||
|
||||
|
||||
class RichConsoleHandler(RichHandler):
|
||||
def __init__(self, width=200, style=None, **kwargs):
|
||||
super().__init__(console=Console(color_system="256", width=width, style=style), **kwargs)
|
@ -3,9 +3,9 @@ from fastapi import FastAPI
|
||||
from app.api.nonsense import router as nonsense_router
|
||||
from app.api.shakespeare import router as shakespeare_router
|
||||
from app.api.stuff import router as stuff_router
|
||||
from app.utils import get_logger
|
||||
from app.logging import AppLogger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
logger = AppLogger.__call__().get_logger()
|
||||
|
||||
app = FastAPI(title="Stuff And Nonsense API", version="0.5")
|
||||
|
||||
|
@ -38,9 +38,7 @@ class Base:
|
||||
db_session.add(self)
|
||||
return await db_session.commit()
|
||||
except SQLAlchemyError as ex:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
|
||||
) from ex
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex
|
||||
|
||||
async def delete(self, db_session: AsyncSession):
|
||||
"""
|
||||
@ -53,9 +51,7 @@ class Base:
|
||||
await db_session.commit()
|
||||
return True
|
||||
except SQLAlchemyError as ex:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
|
||||
) from ex
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex
|
||||
|
||||
async def update(self, db_session: AsyncSession, **kwargs):
|
||||
"""
|
||||
|
@ -29,9 +29,7 @@ class Nonsense(Base):
|
||||
if instance is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail={
|
||||
"Record not found": f"There is no record for requested name value : {name}"
|
||||
},
|
||||
detail={"Record not found": f"There is no record for requested name value : {name}"},
|
||||
)
|
||||
else:
|
||||
return instance
|
||||
|
@ -31,9 +31,7 @@ class Character(Base):
|
||||
abbrev = Column(String(32))
|
||||
description = Column(String(2056))
|
||||
|
||||
work = relationship(
|
||||
"Work", secondary="shakespeare.character_work", back_populates="character"
|
||||
)
|
||||
work = relationship("Work", secondary="shakespeare.character_work", back_populates="character")
|
||||
paragraph = relationship("Paragraph", back_populates="character")
|
||||
|
||||
|
||||
@ -68,9 +66,7 @@ class Work(Base):
|
||||
total_paragraphs = Column(Integer, nullable=False)
|
||||
notes = Column(Text)
|
||||
|
||||
character = relationship(
|
||||
"Character", secondary="shakespeare.character_work", back_populates="work"
|
||||
)
|
||||
character = relationship("Character", secondary="shakespeare.character_work", back_populates="work")
|
||||
chapter = relationship("Chapter", back_populates="work")
|
||||
paragraph = relationship("Paragraph", back_populates="work")
|
||||
|
||||
@ -78,9 +74,7 @@ class Work(Base):
|
||||
class Chapter(Base):
|
||||
__tablename__ = "chapter"
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(
|
||||
["work_id"], ["shakespeare.work.id"], name="chapter_work_id_fkey"
|
||||
),
|
||||
ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="chapter_work_id_fkey"),
|
||||
PrimaryKeyConstraint("id", name="chapter_pkey"),
|
||||
UniqueConstraint(
|
||||
"work_id",
|
||||
@ -111,9 +105,7 @@ t_character_work = Table(
|
||||
["shakespeare.character.id"],
|
||||
name="character_work_character_id_fkey",
|
||||
),
|
||||
ForeignKeyConstraint(
|
||||
["work_id"], ["shakespeare.work.id"], name="character_work_work_id_fkey"
|
||||
),
|
||||
ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="character_work_work_id_fkey"),
|
||||
PrimaryKeyConstraint("character_id", "work_id", name="character_work_pkey"),
|
||||
schema="shakespeare",
|
||||
)
|
||||
@ -136,9 +128,7 @@ class Paragraph(Base):
|
||||
],
|
||||
name="paragraph_chapter_fkey",
|
||||
),
|
||||
ForeignKeyConstraint(
|
||||
["work_id"], ["shakespeare.work.id"], name="paragraph_work_id_fkey"
|
||||
),
|
||||
ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="paragraph_work_id_fkey"),
|
||||
PrimaryKeyConstraint("id", name="paragraph_pkey"),
|
||||
{"schema": "shakespeare"},
|
||||
)
|
||||
@ -162,13 +152,7 @@ class Paragraph(Base):
|
||||
|
||||
@classmethod
|
||||
async def find(cls, db_session: AsyncSession, character: str):
|
||||
stmt = (
|
||||
select(cls)
|
||||
.join(Character)
|
||||
.join(Chapter)
|
||||
.join(Work)
|
||||
.where(Character.name == character)
|
||||
)
|
||||
stmt = select(cls).join(Character).join(Chapter).join(Work).where(Character.name == character)
|
||||
result = await db_session.execute(stmt)
|
||||
instance = result.scalars().all()
|
||||
return instance
|
||||
|
35
app/utils.py
35
app/utils.py
@ -1,21 +1,18 @@
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
|
||||
console = Console(color_system="256", width=200, style="blue")
|
||||
from threading import Lock
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_logger(module_name):
|
||||
logger = logging.getLogger(module_name)
|
||||
handler = RichHandler(
|
||||
rich_tracebacks=True, console=console, tracebacks_show_locals=True
|
||||
)
|
||||
handler.setFormatter(
|
||||
logging.Formatter("[ %(threadName)s:%(funcName)s:%(lineno)d ] - %(message)s")
|
||||
)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
return logger
|
||||
class SingletonMeta(type):
|
||||
"""
|
||||
This is a thread-safe implementation of Singleton.
|
||||
"""
|
||||
|
||||
_instances = {}
|
||||
|
||||
_lock: Lock = Lock()
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
with cls._lock:
|
||||
if cls not in cls._instances:
|
||||
instance = super().__call__(*args, **kwargs)
|
||||
cls._instances[cls] = instance
|
||||
return cls._instances[cls]
|
||||
|
55
config.ini
Normal file
55
config.ini
Normal file
@ -0,0 +1,55 @@
|
||||
[loggers]
|
||||
keys = root, sqlalchemy
|
||||
|
||||
[handlers]
|
||||
keys = console, console_rich, error_file, access_file
|
||||
|
||||
[formatters]
|
||||
keys = generic, generic_rich, access
|
||||
|
||||
[logger_root]
|
||||
; Logging level for all loggers
|
||||
level = NOTSET
|
||||
handlers = console_rich, error_file
|
||||
|
||||
[logger_sqlalchemy]
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[handler_console]
|
||||
class = logging.StreamHandler
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
stram = ext://sys.stdout
|
||||
|
||||
[handler_error_file]
|
||||
class = logging.FileHandler
|
||||
formatter = generic
|
||||
level = WARNING
|
||||
args = ('/tmp/error.log','w')
|
||||
|
||||
[handler_access_file]
|
||||
class = logging.FileHandler
|
||||
formatter = access
|
||||
args = ('/tmp/access.log',)
|
||||
|
||||
[formatter_generic]
|
||||
format = [%(process)d|%(name)-12s|%(filename)s:%(lineno)d] %(levelname)-7s %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
class = logging.Formatter
|
||||
|
||||
[formatter_access]
|
||||
format = %(message)s
|
||||
class = logging.Formatter
|
||||
|
||||
|
||||
[formatter_generic_rich]
|
||||
format = [%(process)d %(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
class = logging.Formatter
|
||||
|
||||
[handler_console_rich]
|
||||
class = app.logging.RichConsoleHandler
|
||||
args = (100, "blue")
|
||||
kwargs = {"omit_repeated_times":False, "show_time": False, "enable_link_path": True, "tracebacks_show_locals": True}
|
||||
level = NOTSET
|
@ -8,7 +8,8 @@ services:
|
||||
- .env
|
||||
- .secrets
|
||||
command: bash -c "
|
||||
alembic upgrade head && uvicorn app.main:app
|
||||
uvicorn app.main:app
|
||||
--log-config ./config.ini
|
||||
--host 0.0.0.0 --port 8080
|
||||
--lifespan=on --use-colors --loop uvloop --http httptools
|
||||
--reload --log-level debug
|
||||
|
Loading…
x
Reference in New Issue
Block a user