diff --git a/README.md b/README.md index dfa364c..e4338de 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,27 @@ Data set is coming form https://github.com/catherinedevlin/opensourceshakespeare Next models were generated with https://github.com/agronholm/sqlacodegen And after some tweaking I got desired result +### Rainbow logs with rich :rainbow: + +To deliver better user(developer) experience when watching logs with tons of information +from few emitters (which are really needy on development stage) project is using [rich](https://github.com/Textualize/rich) library. +Event with [rich](https://github.com/Textualize/rich) superpowers reading logs is not easy. +Found [rich](https://github.com/Textualize/rich) really nice - +but it took time to learn how to integrate it as logger object properly and keep it as singleton. + +To address below needs: +- it is hard to find what I am looking for even with glasses on. +- don’t want to hire ELK to be able to use logs. +- want to move fast enough with debugging. + +Below steps were done to integrate [rich](https://github.com/Textualize/rich) into project. +1. Configure emitters with [config.ini](https://github.com/grillazz/fastapi-sqlalchemy-asyncpg/blob/main/config.ini) +2. Eliminate duplicates i.e. sqlalchemy echo by separate handlers +3. Keep logger as singleton pattern to avoid multiple instances +4. add uvicorn parameter --log-config config.ini + +![sample-logs-with-rich](/static/logz.png) + Hope you enjoy it. ### Change Log @@ -59,6 +80,7 @@ Hope you enjoy it. - 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 +- 28 APR 2023 Rainbow logs with rich :rainbow: ### Local development with poetry diff --git a/app/models/base.py b/app/models/base.py index 028e0fe..133e117 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -4,7 +4,7 @@ from asyncpg import UniqueViolationError from fastapi import HTTPException, status from sqlalchemy.exc import SQLAlchemyError, IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.ext.declarative import as_declarative, declared_attr +from sqlalchemy.orm import as_declarative, declared_attr @as_declarative() @@ -53,16 +53,19 @@ class Base: except SQLAlchemyError as ex: raise HTTPException(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: AsyncSession, **kwargs): """ - :param db_session: - :param kwargs: + :param db: + :param kwargs :return: """ - for k, v in kwargs.items(): - setattr(self, k, v) - await self.save(db_session) + try: + for k, v in kwargs.items(): + setattr(self, k, v) + return await db.commit() + except SQLAlchemyError as ex: + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)) from ex async def save_or_update(self, db: AsyncSession): try: diff --git a/config.ini b/config.ini index cae41bc..103ebaa 100644 --- a/config.ini +++ b/config.ini @@ -1,55 +1,48 @@ [loggers] -keys = root, sqlalchemy +keys = root, sqlalchemy.engine.Engine, uvicorn.access [handlers] -keys = console, console_rich, error_file, access_file +keys = stream, sqlalchemy, uvicorn [formatters] -keys = generic, generic_rich, access +keys = default [logger_root] -; Logging level for all loggers -level = NOTSET -handlers = console_rich, error_file +level = INFO +propagate = 0 +handlers = stream -[logger_sqlalchemy] -handlers = -qualname = sqlalchemy.engine +[logger_sqlalchemy.engine.Engine] +level = INFO +propagate = 0 +handlers = sqlalchemy +qualname = sqlalchemy.engine.Engine -[handler_console] -class = logging.StreamHandler -level = NOTSET -formatter = generic -stram = ext://sys.stdout +[logger_uvicorn.access] +level = INFO +propagate = 0 +handlers = uvicorn +qualname = uvicorn.access -[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] +[handler_stream] 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 +kwargs = {"omit_repeated_times":True, "show_time": False, "enable_link_path": False, "tracebacks_show_locals": True} +args = (100, "white") +formatter = default +stream = ext://sys.stdout + +[handler_sqlalchemy] +class = app.logging.RichConsoleHandler +kwargs = {"omit_repeated_times":True, "show_time": False, "enable_link_path": False, "tracebacks_show_locals": True} +args = (100, "magenta") +formatter = default + +[handler_uvicorn] +class = app.logging.RichConsoleHandler +kwargs = {"omit_repeated_times":True, "show_time": False, "enable_link_path": False, "tracebacks_show_locals": True} +args = (100, "yellow") +formatter = default + +[formatter_default] +format = [%(process)d|%(name)-12s] %(message)s +class = logging.Formatter \ No newline at end of file diff --git a/static/logz.png b/static/logz.png new file mode 100644 index 0000000..8af8129 Binary files /dev/null and b/static/logz.png differ