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 tests/ tests/
COPY app/ app/ COPY app/ app/
COPY alembic/ alembic/ COPY alembic/ alembic/
COPY .env alembic.ini ./ COPY .env alembic.ini config.ini ./
# create a non-root user and switch to it, for security. # create a non-root user and switch to it, for security.
RUN addgroup --system --gid 1001 "app-user" RUN addgroup --system --gid 1001 "app-user"

View File

@ -43,7 +43,6 @@ lint: ## Lint project code.
.PHONY: format .PHONY: format
format: ## Format project code. format: ## Format project code.
isort app tests
black app tests --line-length=120 black app tests --line-length=120
.PHONY: slim-build .PHONY: slim-build

View File

@ -58,6 +58,7 @@ Hope you enjoy it.
- 3 OCT 2022 poetry added to project - 3 OCT 2022 poetry added to project
- 12 NOV 2022 ruff implemented to project as linting tool - 12 NOV 2022 ruff implemented to project as linting tool
- 14 FEB 2023 bump project to Python 3.11 - 14 FEB 2023 bump project to Python 3.11
- 10 APR 2023 implement logging with rich
### Local development with poetry ### 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) @router.post("/", status_code=status.HTTP_201_CREATED, response_model=NonsenseResponse)
async def create_nonsense( async def create_nonsense(payload: NonsenseSchema, db_session: AsyncSession = Depends(get_db)):
payload: NonsenseSchema, db_session: AsyncSession = Depends(get_db)
):
nonsense = Nonsense(**payload.dict()) nonsense = Nonsense(**payload.dict())
await nonsense.save(db_session) await nonsense.save(db_session)
return nonsense return nonsense
@ -40,3 +38,13 @@ async def update_nonsense(
nonsense = await Nonsense.find(db_session, name) nonsense = await Nonsense.find(db_session, name)
await nonsense.update(db_session, **payload.dict()) await nonsense.update(db_session, **payload.dict())
return nonsense 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.database import get_db
from app.models.stuff import Stuff from app.models.stuff import Stuff
from app.schemas.stuff import StuffResponse, StuffSchema 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") router = APIRouter(prefix="/v1/stuff")
logger = get_logger(__name__)
@router.post("/add_many", status_code=status.HTTP_201_CREATED) @router.post("/add_many", status_code=status.HTTP_201_CREATED)
async def create_multi_stuff( async def create_multi_stuff(payload: list[StuffSchema], db_session: AsyncSession = Depends(get_db)):
payload: list[StuffSchema], db_session: AsyncSession = Depends(get_db)
):
try: try:
stuff_instances = [ stuff_instances = [Stuff(name=stuf.name, description=stuf.description) for stuf in payload]
Stuff(name=stuf.name, description=stuf.description) for stuf in payload
]
db_session.add_all(stuff_instances) db_session.add_all(stuff_instances)
await db_session.commit() await db_session.commit()
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
# logger.exception(ex) # logger.exception(ex)
raise HTTPException( raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex
else: else:
logger.info( logger.info(f"{len(stuff_instances)} instances of Stuff inserted into database.")
f"{len(stuff_instances)} instances of Stuff inserted into database."
)
return True return True
@router.post("", status_code=status.HTTP_201_CREATED, response_model=StuffResponse) @router.post("", status_code=status.HTTP_201_CREATED, response_model=StuffResponse)
async def create_stuff( async def create_stuff(payload: StuffSchema, db_session: AsyncSession = Depends(get_db)):
payload: StuffSchema, db_session: AsyncSession = Depends(get_db)
):
stuff = Stuff(name=payload.name, description=payload.description) stuff = Stuff(name=payload.name, description=payload.description)
await stuff.save(db_session) await stuff.save(db_session)
return stuff return stuff

View File

@ -19,4 +19,5 @@ class Settings(BaseSettings):
def get_settings(): def get_settings():
return Settings() return Settings()
settings = get_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 sqlalchemy.orm import sessionmaker
from app import config from app import config
from app.utils import get_logger from app.logging import AppLogger
logger = get_logger(__name__)
global_settings = config.get_settings() global_settings = config.get_settings()
logger = AppLogger.__call__().get_logger()
engine = create_async_engine( engine = create_async_engine(
global_settings.asyncpg_url, global_settings.asyncpg_url,
@ -18,13 +17,11 @@ engine = create_async_engine(
# expire_on_commit=False will prevent attributes from being expired # expire_on_commit=False will prevent attributes from being expired
# after commit. # after commit.
AsyncSessionFactory = sessionmaker( AsyncSessionFactory = sessionmaker(engine, autoflush=False, expire_on_commit=False, class_=AsyncSession)
engine, autoflush=False, expire_on_commit=False, class_=AsyncSession
)
# Dependency # Dependency
async def get_db() -> AsyncGenerator: async def get_db() -> AsyncGenerator:
async with AsyncSessionFactory() as session: async with AsyncSessionFactory() as session:
logger.debug(f"ASYNC Pool: {engine.pool.status()}") # logger.debug(f"ASYNC Pool: {engine.pool.status()}")
yield session 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.nonsense import router as nonsense_router
from app.api.shakespeare import router as shakespeare_router from app.api.shakespeare import router as shakespeare_router
from app.api.stuff import router as stuff_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") app = FastAPI(title="Stuff And Nonsense API", version="0.5")

View File

@ -38,9 +38,7 @@ class Base:
db_session.add(self) db_session.add(self)
return await db_session.commit() return await db_session.commit()
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
raise HTTPException( raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex
async def delete(self, db_session: AsyncSession): async def delete(self, db_session: AsyncSession):
""" """
@ -53,9 +51,7 @@ class Base:
await db_session.commit() await db_session.commit()
return True return True
except SQLAlchemyError as ex: except SQLAlchemyError as ex:
raise HTTPException( raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
) from ex
async def update(self, db_session: AsyncSession, **kwargs): async def update(self, db_session: AsyncSession, **kwargs):
""" """

View File

@ -29,9 +29,7 @@ class Nonsense(Base):
if instance is None: if instance is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail={ detail={"Record not found": f"There is no record for requested name value : {name}"},
"Record not found": f"There is no record for requested name value : {name}"
},
) )
else: else:
return instance return instance

View File

@ -31,9 +31,7 @@ class Character(Base):
abbrev = Column(String(32)) abbrev = Column(String(32))
description = Column(String(2056)) description = Column(String(2056))
work = relationship( work = relationship("Work", secondary="shakespeare.character_work", back_populates="character")
"Work", secondary="shakespeare.character_work", back_populates="character"
)
paragraph = relationship("Paragraph", back_populates="character") paragraph = relationship("Paragraph", back_populates="character")
@ -68,9 +66,7 @@ class Work(Base):
total_paragraphs = Column(Integer, nullable=False) total_paragraphs = Column(Integer, nullable=False)
notes = Column(Text) notes = Column(Text)
character = relationship( character = relationship("Character", secondary="shakespeare.character_work", back_populates="work")
"Character", secondary="shakespeare.character_work", back_populates="work"
)
chapter = relationship("Chapter", back_populates="work") chapter = relationship("Chapter", back_populates="work")
paragraph = relationship("Paragraph", back_populates="work") paragraph = relationship("Paragraph", back_populates="work")
@ -78,9 +74,7 @@ class Work(Base):
class Chapter(Base): class Chapter(Base):
__tablename__ = "chapter" __tablename__ = "chapter"
__table_args__ = ( __table_args__ = (
ForeignKeyConstraint( ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="chapter_work_id_fkey"),
["work_id"], ["shakespeare.work.id"], name="chapter_work_id_fkey"
),
PrimaryKeyConstraint("id", name="chapter_pkey"), PrimaryKeyConstraint("id", name="chapter_pkey"),
UniqueConstraint( UniqueConstraint(
"work_id", "work_id",
@ -111,9 +105,7 @@ t_character_work = Table(
["shakespeare.character.id"], ["shakespeare.character.id"],
name="character_work_character_id_fkey", name="character_work_character_id_fkey",
), ),
ForeignKeyConstraint( ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="character_work_work_id_fkey"),
["work_id"], ["shakespeare.work.id"], name="character_work_work_id_fkey"
),
PrimaryKeyConstraint("character_id", "work_id", name="character_work_pkey"), PrimaryKeyConstraint("character_id", "work_id", name="character_work_pkey"),
schema="shakespeare", schema="shakespeare",
) )
@ -136,9 +128,7 @@ class Paragraph(Base):
], ],
name="paragraph_chapter_fkey", name="paragraph_chapter_fkey",
), ),
ForeignKeyConstraint( ForeignKeyConstraint(["work_id"], ["shakespeare.work.id"], name="paragraph_work_id_fkey"),
["work_id"], ["shakespeare.work.id"], name="paragraph_work_id_fkey"
),
PrimaryKeyConstraint("id", name="paragraph_pkey"), PrimaryKeyConstraint("id", name="paragraph_pkey"),
{"schema": "shakespeare"}, {"schema": "shakespeare"},
) )
@ -162,13 +152,7 @@ class Paragraph(Base):
@classmethod @classmethod
async def find(cls, db_session: AsyncSession, character: str): async def find(cls, db_session: AsyncSession, character: str):
stmt = ( stmt = select(cls).join(Character).join(Chapter).join(Work).where(Character.name == character)
select(cls)
.join(Character)
.join(Chapter)
.join(Work)
.where(Character.name == character)
)
result = await db_session.execute(stmt) result = await db_session.execute(stmt)
instance = result.scalars().all() instance = result.scalars().all()
return instance return instance

View File

@ -1,21 +1,18 @@
import logging from threading import Lock
from functools import lru_cache
from rich.console import Console
from rich.logging import RichHandler
console = Console(color_system="256", width=200, style="blue")
@lru_cache class SingletonMeta(type):
def get_logger(module_name): """
logger = logging.getLogger(module_name) This is a thread-safe implementation of Singleton.
handler = RichHandler( """
rich_tracebacks=True, console=console, tracebacks_show_locals=True
) _instances = {}
handler.setFormatter(
logging.Formatter("[ %(threadName)s:%(funcName)s:%(lineno)d ] - %(message)s") _lock: Lock = Lock()
)
logger.addHandler(handler) def __call__(cls, *args, **kwargs):
logger.setLevel(logging.DEBUG) with cls._lock:
return logger 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 - .env
- .secrets - .secrets
command: bash -c " 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 --host 0.0.0.0 --port 8080
--lifespan=on --use-colors --loop uvloop --http httptools --lifespan=on --use-colors --loop uvloop --http httptools
--reload --log-level debug --reload --log-level debug