Merge pull request #86 from grillazz/75-revisit-logging

75 revisit logging
This commit is contained in:
Jakub Miazek 2023-04-10 21:45:15 +02:00 committed by GitHub
commit 3cea4f784d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 132 additions and 83 deletions

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -19,4 +19,5 @@ class Settings(BaseSettings):
def get_settings():
return Settings()
settings = get_settings()

View File

@ -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
View 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)

View File

@ -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")

View File

@ -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):
"""

View File

@ -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

View File

@ -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

View File

@ -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
View 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

View File

@ -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