Last active
August 21, 2023 15:09
-
-
Save LifeMoroz/52be5a112052e2dd4735e42da8804244 to your computer and use it in GitHub Desktop.
logger decorator for async app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Adds async context to a Python logger. | |
""" | |
import asyncio | |
import functools | |
import inspect | |
import logging | |
from contextvars import ContextVar | |
from typing import Any, Callable | |
_EXTRA_TYPE = dict[str, Any] | |
_EXTRA_VAR = ContextVar("extra", default=dict()) | |
class Logger: | |
""" | |
Wrapper for a python :py:class:`logging.Logger`. | |
This wrapper reads the current local context and emits | |
them for each log message. | |
""" | |
_original_get = None | |
def __init__(self, name: str | None = None): | |
self.base_logger = self.__class__._original_get(name) | |
@classmethod | |
def setup(cls): | |
cls._original_get = logging.getLogger | |
logging.getLogger = Logger | |
def _msg(self, func: Callable, msg, *args, **kwargs): | |
"""Log with our extra values,""" | |
extra = _EXTRA_VAR.get() | kwargs.pop("extra", {}) | |
# Because we wrap a logging.Logger instance through 2 layers | |
# of redirection, we need to add 2 to the logger's stacklevel | |
# so we correctly log the logging statement's line number and function name | |
original_stacklevel = kwargs.pop("stacklevel", 1) | |
stacklevel = original_stacklevel + 2 | |
return func(msg, *args, extra=extra, stacklevel=stacklevel, **kwargs) | |
def __getattr__(self, item): | |
attr = getattr(self.base_logger, item) | |
if attr in ("debug", "info", "warning", "error", "exception", "critical"): | |
def _log(*args, **kwargs): | |
self._msg(attr, *args, **kwargs) | |
return _log | |
return attr | |
class log_extra: | |
def __init__(self, extra=None, *_, **kwargs): | |
self.old_extra = dict() | |
self.new_extra = (extra or {}) | kwargs | |
def __enter__(self): | |
self.old_extra = _EXTRA_VAR.get() | |
_EXTRA_VAR.set(self.old_extra | self.new_extra) | |
def __exit__(self, *args, **kwds): | |
_EXTRA_VAR.set(self.old_extra) | |
def __call__(self, f: Callable | type): | |
if inspect.isclass(f): | |
for attr in f.__dict__: # there's propably a better way to do this | |
if callable(getattr(f, attr)): | |
setattr(f, attr, self(getattr(f, attr))) | |
return f | |
elif inspect.iscoroutinefunction(f): | |
@functools.wraps(f) | |
async def actual_decorator(*args, **kwargs): | |
with self: | |
return await f(*args, **kwargs) | |
else: | |
@functools.wraps(f) | |
def actual_decorator(*args, **kwargs): | |
with self: | |
return f(*args, **kwargs) | |
return actual_decorator |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import asyncio | |
import logging | |
from .log_extra import log_extra, _EXTRA_VAR | |
async def handler(): | |
print(_EXTRA_VAR.get()) | |
async def handler2(): | |
with log_extra(id="2"): | |
await handler() | |
@log_extra(id="3") | |
async def handler3(): | |
await handler() | |
@log_extra(class_name="X") | |
class X: | |
def x(self): | |
print(_EXTRA_VAR.get()) | |
async def y(self): | |
print(_EXTRA_VAR.get()) | |
async def task(): | |
Logger(__name__).error("My message") | |
@log_extra(id=1) | |
async def main(): | |
await handler() | |
await handler2() | |
await handler3() | |
await handler() | |
X().x() | |
await X().y() | |
_task = asyncio.create_task(task()) | |
while not _task.done(): | |
await asyncio.sleep(1) | |
Logger.setup() | |
logger = logging.getLogger(__name__) | |
logger.error("My message 2") | |
asyncio.run(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment