Skip to content

Commit

Permalink
Merge pull request #202 from aiokitchen/featuire/rework-logging
Browse files Browse the repository at this point in the history
Rework logging
  • Loading branch information
mosquito committed Mar 14, 2024
2 parents 5d20793 + 08a5691 commit 60bff21
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 190 deletions.
18 changes: 10 additions & 8 deletions aiomisc/log.py
Expand Up @@ -7,7 +7,9 @@
from contextlib import suppress
from functools import partial
from socket import socket
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
from typing import (
Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, Union,
)
from weakref import finalize

import aiomisc_log
Expand Down Expand Up @@ -75,9 +77,7 @@ def wrap_logging_handler(
return buffered_handler


class UnhandledLoopHook(aiomisc_log.UnhandledHookBase):
LOGGER_NAME = "asyncio.unhandled"

class UnhandledLoopHook(aiomisc_log.UnhandledHook):
@staticmethod
def _fill_transport_extra(
transport: Optional[asyncio.Transport],
Expand Down Expand Up @@ -109,8 +109,8 @@ def __call__(
protocol: Optional[asyncio.Protocol] = context.pop("protocol", None)
transport: Optional[asyncio.Transport] = context.pop("transport", None)
sock: Optional[socket] = context.pop("socket", None)
source_traceback: List[traceback.FrameSummary] = context.pop(
"source_traceback", None,
source_traceback: List[traceback.FrameSummary] = (
context.pop("source_traceback", None) or []
)

if exception is None:
Expand Down Expand Up @@ -141,15 +141,16 @@ def basic_config(
buffered: bool = True, buffer_size: int = 1024,
flush_interval: Union[int, float] = 0.2,
loop: Optional[asyncio.AbstractEventLoop] = None,
handlers: Iterable[logging.Handler] = (),
**kwargs: Any,
) -> None:
loop = loop or asyncio.get_event_loop()
unhandled_hook = UnhandledLoopHook()
unhandled_hook = UnhandledLoopHook(logger_name="asyncio.unhandled")

def wrap_handler(handler: logging.Handler) -> logging.Handler:
nonlocal buffer_size, buffered, loop, unhandled_hook

unhandled_hook.set_handler(handler)
unhandled_hook.add_handler(handler)

if buffered:
return wrap_logging_handler(
Expand All @@ -164,6 +165,7 @@ def wrap_handler(handler: logging.Handler) -> logging.Handler:
level=level,
log_format=log_format,
handler_wrapper=wrap_handler,
handlers=handlers,
**kwargs,
)

Expand Down
89 changes: 50 additions & 39 deletions aiomisc_log/__init__.py
Expand Up @@ -2,26 +2,22 @@
import logging.handlers
import os
import sys
from contextvars import ContextVar
from dataclasses import dataclass
from itertools import chain
from types import TracebackType
from typing import Any, Callable, Optional, Type, Union
from typing import Any, Callable, Iterable, Optional, Type, Union

from .enum import LogFormat, LogLevel
from .formatter import (
color_formatter, journald_formatter, json_handler, rich_formatter,
)


LOG_LEVEL: Optional[Any] = None
LOG_FORMAT: Optional[Any] = None

try:
import contextvars
LOG_LEVEL = contextvars.ContextVar("LOG_LEVEL", default=logging.INFO)
LOG_FORMAT = contextvars.ContextVar(
"LOG_FORMAT", default=LogFormat.default(),
)
except ImportError:
pass
LOG_LEVEL: ContextVar = ContextVar("LOG_LEVEL", default=logging.INFO)
LOG_FORMAT: ContextVar = ContextVar(
"LOG_FORMAT", default=LogFormat.default(),
)


DEFAULT_FORMAT = "%(levelname)s:%(name)s:%(message)s"
Expand All @@ -30,12 +26,13 @@
def create_logging_handler(
log_format: LogFormat = LogFormat.color,
date_format: Optional[str] = None, **kwargs: Any,
) -> logging.Handler:
) -> Optional[logging.Handler]:

if LOG_FORMAT is not None:
LOG_FORMAT.set(log_format)
LOG_FORMAT.set(log_format)

if log_format == LogFormat.stream:
if log_format == LogFormat.disabled:
return None
elif log_format == LogFormat.stream:
handler: logging.Handler = logging.StreamHandler()
if date_format and date_format is not Ellipsis:
formatter = logging.Formatter(
Expand Down Expand Up @@ -90,74 +87,88 @@ def pass_wrapper(handler: logging.Handler) -> logging.Handler:
return handler


@dataclass
class UnhandledHookBase:
__slots__ = "logger",

LOGGER_NAME: str = "unhandled"
logger: logging.Logger
logger_name: str = "unhandled"
logger_default_message: str = "Unhandled exception"

def __init__(self) -> None:
self.logger = logging.getLogger().getChild(self.LOGGER_NAME)
self.logger.propagate = False

def set_handler(self, handler: logging.Handler) -> None:
self.logger.handlers.clear()
self.logger.handlers.append(handler)
class UnhandledHook(UnhandledHookBase):
def __init__(self, **kwargs: Any) -> None:
logger = logging.getLogger().getChild(self.logger_name)
logger.propagate = False
logger.handlers.clear()
super().__init__(logger=logger, **kwargs)

def add_handler(self, handler: logging.Handler) -> None:
self.logger.handlers.append(handler)

class UnhandledHook(UnhandledHookBase):
MESSAGE: str = "Unhandled exception"

class UnhandledPythonHook(UnhandledHook):
def __call__(
self,
exc_type: Type[BaseException],
exc_value: BaseException,
exc_traceback: TracebackType,
) -> None:
self.logger.exception(
self.MESSAGE, exc_info=(exc_type, exc_value, exc_traceback),
self.logger_default_message,
exc_info=(exc_type, exc_value, exc_traceback),
)


def basic_config(
level: Union[int, str] = logging.INFO,
level: Union[int, str] = LogLevel.info,
log_format: Union[str, LogFormat] = LogFormat.color,
handler_wrapper: HandlerWrapperType = pass_wrapper,
handlers: Iterable[logging.Handler] = (),
**kwargs: Any,
) -> None:

if isinstance(level, str):
level = LogLevel[level]

logging.basicConfig()
logger = logging.getLogger()
logger.handlers.clear()
logging.basicConfig(handlers=[], level=logging.NOTSET)
root_logger = logging.getLogger()
root_logger.handlers.clear()

if isinstance(log_format, str):
log_format = LogFormat[log_format]

raw_handler = create_logging_handler(log_format, **kwargs)
unhandled_hook = UnhandledHook()
unhandled_hook = UnhandledPythonHook()
sys.excepthook = unhandled_hook # type: ignore

handler = handler_wrapper(raw_handler)
logging_handlers = list(
map(
handler_wrapper,
filter(
None,
chain(
[create_logging_handler(log_format, **kwargs)],
handlers,
),
),
),
)

if LOG_LEVEL is not None:
LOG_LEVEL.set(level)
LOG_LEVEL.set(level)

# noinspection PyArgumentList
logging.basicConfig(
level=int(level),
handlers=[handler],
handlers=logging_handlers,
)

unhandled_hook.set_handler(raw_handler)
for handler in logging_handlers:
unhandled_hook.add_handler(handler)


__all__ = (
"LogFormat",
"LogLevel",
"basic_config",
"create_logging_handler",
"LOG_FORMAT",
"LOG_LEVEL",
)
6 changes: 3 additions & 3 deletions aiomisc_log/enum.py
Expand Up @@ -12,15 +12,15 @@ def check_journal_stream() -> bool:
return False

try:
import rich

RICH_INSTALLED = bool(rich)
import rich as _ # noqa
RICH_INSTALLED = True
except ImportError:
RICH_INSTALLED = False


@unique
class LogFormat(IntEnum):
disabled = -1
stream = 0
color = 1
json = 2
Expand Down
6 changes: 1 addition & 5 deletions aiomisc_log/formatter/color.py
Expand Up @@ -11,11 +11,7 @@ def color_formatter(
stream: Optional[IO[str]] = None,
date_format: Optional[str] = None, **_: Any,
) -> logging.Handler:

date_format = (
date_format if date_format is not None else DateFormat.color.value
)

date_format = date_format or DateFormat.color.value
stream = stream or sys.stderr
handler = logging.StreamHandler(stream)

Expand Down
22 changes: 14 additions & 8 deletions aiomisc_log/formatter/rich.py
@@ -1,29 +1,35 @@
import logging
import sys
from typing import IO, Any, Optional

from aiomisc_log.enum import DateFormat


try:
from rich.console import Console
from rich import reconfigure
from rich.logging import RichHandler

def rich_formatter(
date_format: Optional[str] = None, stream: Optional[IO[str]] = None,
rich_tracebacks: bool = False, **_: Any,
**kwargs: Any,
) -> logging.Handler:
handler = RichHandler(
console=Console(file=stream or sys.stderr),
log_time_format=date_format or DateFormat.rich.value,
rich_tracebacks=rich_tracebacks,
kwargs.setdefault("rich_tracebacks", False)
kwargs.setdefault(
"log_time_format", date_format or DateFormat.rich.value,
)

if "console" not in kwargs:
if stream is None:
reconfigure(stderr=True)
else:
reconfigure(file=stream)

handler = RichHandler(**kwargs)
formatter = logging.Formatter("%(message)s")
handler.setFormatter(formatter)
return handler
except ImportError:
def rich_formatter(
date_format: Optional[str] = None, stream: Optional[IO[str]] = None,
rich_tracebacks: bool = False, **_: Any,
**kwargs: Any,
) -> logging.Handler:
raise ImportError("You must install \"rich\" library for use it")
26 changes: 25 additions & 1 deletion docs/source/logging.rst
Expand Up @@ -107,11 +107,35 @@ But it isn't dependency and you have to install `Rich`_ manually.
.. _Rich: https://pypi.org/project/rich/

Disabled
++++++++

Disable to configure logging handler. Useful when you want to configure your own logging handlers using
`handlers=` argument.

.. code-block:: python
:name: test_log_disabled
import logging
from aiomisc.log import basic_config
# Configure rich log handler
basic_config(
level=logging.INFO,
log_format='disabled',
handlers=[logging.StreamHandler()],
buffered=False,
)
logging.info("Use default python logger for example")
Buffered log handler
++++++++++++++++++++

Parameter `buffered=True` enables a memory buffer that flushes logs in a thread.
Parameter `buffered=True` enables a memory buffer that flushes logs in a thread. In case the `handlers=`
each will be buffered.

.. code-block:: python
:name: test_logging_buffered
Expand Down

0 comments on commit 60bff21

Please sign in to comment.