first commit
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
import asyncio
|
||||
import io
|
||||
import sys
|
||||
import traceback
|
||||
from asyncio import AbstractEventLoop, Task
|
||||
from typing import Iterable, Optional, Callable, Awaitable, List, NamedTuple
|
||||
|
||||
from aiologger.filters import StdoutFilter, Filterer
|
||||
from aiologger.formatters.base import Formatter
|
||||
from aiologger.handlers.base import Handler
|
||||
from aiologger.handlers.streams import AsyncStreamHandler
|
||||
from aiologger.levels import LogLevel, check_level
|
||||
from aiologger.records import LogRecord
|
||||
from aiologger.utils import (
|
||||
get_current_frame,
|
||||
create_task,
|
||||
loop_compat,
|
||||
bind_loop,
|
||||
)
|
||||
|
||||
_HandlerFactory = Callable[[], Awaitable[Iterable[Handler]]]
|
||||
|
||||
|
||||
class _Caller(NamedTuple):
|
||||
filename: str
|
||||
line_number: int
|
||||
function_name: str
|
||||
stack: Optional[str]
|
||||
|
||||
|
||||
def o_o():
|
||||
"""
|
||||
Ordinarily we would use __file__ for this, but frozen modules don't always
|
||||
have __file__ set, for some reason (see Issue logging#21736). Thus, we get
|
||||
the filename from a handy code object from a function defined in this
|
||||
module.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"I shouldn't be called. My only purpose is to provide "
|
||||
"the filename from a handy code object."
|
||||
)
|
||||
|
||||
|
||||
# _srcfile is used when walking the stack to check when we've got the first
|
||||
# caller stack frame, by skipping frames whose filename is that of this
|
||||
# module's source. It therefore should contain the filename of this module's
|
||||
# source file.
|
||||
_srcfile = o_o.__code__.co_filename
|
||||
|
||||
|
||||
@loop_compat
|
||||
class Logger(Filterer):
|
||||
def __init__(self, *, name="aiologger", level=LogLevel.NOTSET) -> None:
|
||||
super(Logger, self).__init__()
|
||||
self.name = name
|
||||
self.level = check_level(level)
|
||||
self.parent = None
|
||||
self.propagate = True
|
||||
self.handlers: List[Handler] = []
|
||||
self.disabled = False
|
||||
self._was_shutdown = False
|
||||
|
||||
self._dummy_task: Optional[Task] = None
|
||||
|
||||
@classmethod
|
||||
def with_default_handlers(
|
||||
cls,
|
||||
*,
|
||||
name="aiologger",
|
||||
level=LogLevel.NOTSET,
|
||||
formatter: Optional[Formatter] = None,
|
||||
**kwargs,
|
||||
):
|
||||
self = cls(name=name, level=level, **kwargs) # type: ignore
|
||||
|
||||
_AsyncStreamHandler = bind_loop(AsyncStreamHandler, kwargs)
|
||||
self.add_handler(
|
||||
_AsyncStreamHandler(
|
||||
stream=sys.stdout,
|
||||
level=LogLevel.DEBUG,
|
||||
formatter=formatter,
|
||||
filter=StdoutFilter(),
|
||||
)
|
||||
)
|
||||
self.add_handler(
|
||||
_AsyncStreamHandler(
|
||||
stream=sys.stderr, level=LogLevel.WARNING, formatter=formatter
|
||||
)
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def find_caller(self, stack_info=False) -> _Caller:
|
||||
"""
|
||||
Find the stack frame of the caller so that we can note the source
|
||||
file name, line number and function name.
|
||||
"""
|
||||
frame = get_current_frame()
|
||||
# On some versions of IronPython, currentframe() returns None if
|
||||
# IronPython isn't run with -X:Frames.
|
||||
if frame is not None:
|
||||
frame = frame.f_back
|
||||
while hasattr(frame, "f_code"):
|
||||
code = frame.f_code
|
||||
filename = code.co_filename
|
||||
if filename == _srcfile:
|
||||
frame = frame.f_back
|
||||
continue
|
||||
sinfo = None
|
||||
if stack_info:
|
||||
sio = io.StringIO()
|
||||
sio.write("Stack (most recent call last):\n")
|
||||
traceback.print_stack(frame, file=sio)
|
||||
sinfo = sio.getvalue()
|
||||
if sinfo[-1] == "\n":
|
||||
sinfo = sinfo[:-1]
|
||||
sio.close()
|
||||
return _Caller(
|
||||
filename=code.co_filename or "(unknown file)",
|
||||
line_number=frame.f_lineno,
|
||||
function_name=code.co_name,
|
||||
stack=sinfo,
|
||||
)
|
||||
return _Caller(
|
||||
filename="(unknown file)",
|
||||
line_number=0,
|
||||
function_name="(unknown function)",
|
||||
stack=None,
|
||||
)
|
||||
|
||||
async def call_handlers(self, record):
|
||||
"""
|
||||
Pass a record to all relevant handlers.
|
||||
|
||||
Loop through all handlers for this logger and its parents in the
|
||||
logger hierarchy. If no handler was found, raises an error. Stop
|
||||
searching up the hierarchy whenever a logger with the "propagate"
|
||||
attribute set to zero is found - that will be the last logger
|
||||
whose handlers are called.
|
||||
"""
|
||||
c = self
|
||||
found = 0
|
||||
while c:
|
||||
for handler in c.handlers:
|
||||
found = found + 1
|
||||
if record.levelno >= handler.level:
|
||||
await handler.handle(record)
|
||||
if not c.propagate:
|
||||
c = None # break out
|
||||
else:
|
||||
c = c.parent
|
||||
if found == 0:
|
||||
raise Exception("No handlers could be found for logger")
|
||||
|
||||
def add_handler(self, handler: Handler) -> None:
|
||||
"""
|
||||
Add the specified handler to this logger.
|
||||
"""
|
||||
if not (handler in self.handlers):
|
||||
self.handlers.append(handler)
|
||||
|
||||
def remove_handler(self, handler: Handler) -> None:
|
||||
"""
|
||||
Remove the specified handler from this logger.
|
||||
"""
|
||||
if handler in self.handlers:
|
||||
self.handlers.remove(handler)
|
||||
|
||||
async def handle(self, record):
|
||||
"""
|
||||
Call the handlers for the specified record.
|
||||
|
||||
This method is used for unpickled records received from a socket, as
|
||||
well as those created locally. Logger-level filtering is applied.
|
||||
"""
|
||||
if (not self.disabled) and self.filter(record):
|
||||
await self.call_handlers(record)
|
||||
|
||||
def _log(
|
||||
self,
|
||||
level,
|
||||
msg,
|
||||
args,
|
||||
exc_info=None,
|
||||
extra=None,
|
||||
stack_info=False,
|
||||
caller: _Caller = None,
|
||||
) -> Task:
|
||||
|
||||
sinfo = None
|
||||
if _srcfile and caller is None: # type: ignore
|
||||
# IronPython doesn't track Python frames, so find_caller raises an
|
||||
# exception on some versions of IronPython. We trap it here so that
|
||||
# IronPython can use logging.
|
||||
try:
|
||||
fn, lno, func, sinfo = self.find_caller(stack_info)
|
||||
except ValueError: # pragma: no cover
|
||||
fn, lno, func = "(unknown file)", 0, "(unknown function)"
|
||||
elif caller:
|
||||
fn, lno, func, sinfo = caller
|
||||
else: # pragma: no cover
|
||||
fn, lno, func = "(unknown file)", 0, "(unknown function)"
|
||||
if exc_info and isinstance(exc_info, BaseException):
|
||||
exc_info = (type(exc_info), exc_info, exc_info.__traceback__)
|
||||
|
||||
record = LogRecord( # type: ignore
|
||||
name=self.name,
|
||||
level=level,
|
||||
pathname=fn,
|
||||
lineno=lno,
|
||||
msg=msg,
|
||||
args=args,
|
||||
exc_info=exc_info,
|
||||
func=func,
|
||||
sinfo=sinfo,
|
||||
extra=extra,
|
||||
)
|
||||
return create_task(self.handle(record))
|
||||
|
||||
def __make_dummy_task(self) -> Task:
|
||||
async def _dummy(*args, **kwargs):
|
||||
return
|
||||
|
||||
return create_task(_dummy())
|
||||
|
||||
def is_enabled_for(self, level) -> bool:
|
||||
return level >= self.level
|
||||
|
||||
def _make_log_task(self, level, msg, *args, **kwargs) -> Task:
|
||||
"""
|
||||
Creates an asyncio.Task for a msg if logging is enabled for level.
|
||||
Returns a dummy task otherwise.
|
||||
"""
|
||||
if not self.is_enabled_for(level):
|
||||
if self._dummy_task is None:
|
||||
self._dummy_task = self.__make_dummy_task()
|
||||
return self._dummy_task
|
||||
|
||||
if kwargs.get("exc_info", False):
|
||||
if not isinstance(kwargs["exc_info"], BaseException):
|
||||
kwargs["exc_info"] = sys.exc_info()
|
||||
|
||||
return self._log( # type: ignore
|
||||
level, msg, *args, caller=self.find_caller(False), **kwargs
|
||||
)
|
||||
|
||||
def debug(self, msg, *args, **kwargs) -> Task:
|
||||
"""
|
||||
Log msg with severity 'DEBUG'.
|
||||
|
||||
To pass exception information, use the keyword argument exc_info with
|
||||
a true value, e.g.
|
||||
|
||||
await logger.debug("Houston, we have a %s", "thorny problem", exc_info=1)
|
||||
"""
|
||||
return self._make_log_task(LogLevel.DEBUG, msg, args, **kwargs)
|
||||
|
||||
def info(self, msg, *args, **kwargs) -> Task:
|
||||
"""
|
||||
Log msg with severity 'INFO'.
|
||||
|
||||
To pass exception information, use the keyword argument exc_info with
|
||||
a true value, e.g.
|
||||
|
||||
await logger.info("Houston, we have an interesting problem", exc_info=1)
|
||||
"""
|
||||
return self._make_log_task(LogLevel.INFO, msg, args, **kwargs)
|
||||
|
||||
def warning(self, msg, *args, **kwargs) -> Task:
|
||||
"""
|
||||
Log msg with severity 'WARNING'.
|
||||
|
||||
To pass exception information, use the keyword argument exc_info with
|
||||
a true value, e.g.
|
||||
|
||||
await logger.warning("Houston, we have a bit of a problem", exc_info=1)
|
||||
"""
|
||||
return self._make_log_task(LogLevel.WARNING, msg, args, **kwargs)
|
||||
|
||||
warn = warning
|
||||
|
||||
def error(self, msg, *args, **kwargs) -> Task:
|
||||
"""
|
||||
Log msg with severity 'ERROR'.
|
||||
|
||||
To pass exception information, use the keyword argument exc_info with
|
||||
a true value, e.g.
|
||||
|
||||
await logger.error("Houston, we have a major problem", exc_info=1)
|
||||
"""
|
||||
return self._make_log_task(LogLevel.ERROR, msg, args, **kwargs)
|
||||
|
||||
def critical(self, msg, *args, **kwargs) -> Task:
|
||||
"""
|
||||
Log msg with severity 'CRITICAL'.
|
||||
|
||||
To pass exception information, use the keyword argument exc_info with
|
||||
a true value, e.g.
|
||||
|
||||
await logger.critical("Houston, we have a major disaster", exc_info=1)
|
||||
"""
|
||||
return self._make_log_task(LogLevel.CRITICAL, msg, args, **kwargs)
|
||||
|
||||
fatal = critical
|
||||
|
||||
def exception(self, msg, *args, exc_info=True, **kwargs) -> Task:
|
||||
"""
|
||||
Convenience method for logging an ERROR with exception information.
|
||||
"""
|
||||
return self.error(msg, *args, exc_info=exc_info, **kwargs)
|
||||
|
||||
async def shutdown(self):
|
||||
"""
|
||||
Perform any cleanup actions in the logging system (e.g. flushing
|
||||
buffers).
|
||||
|
||||
Should be called at application exit.
|
||||
"""
|
||||
if self._was_shutdown:
|
||||
return
|
||||
self._was_shutdown = True
|
||||
await self._do_shutdown()
|
||||
|
||||
async def _do_shutdown(self):
|
||||
"""
|
||||
Does actual shutdown
|
||||
"""
|
||||
for handler in reversed(self.handlers):
|
||||
if not handler:
|
||||
continue
|
||||
try:
|
||||
if handler.initialized:
|
||||
await handler.flush()
|
||||
await handler.close()
|
||||
|
||||
except Exception:
|
||||
"""
|
||||
Ignore errors which might be caused
|
||||
because handlers have been closed but
|
||||
references to them are still around at
|
||||
application exit. Basically ignore everything,
|
||||
as we're shutting down
|
||||
"""
|
||||
pass
|
||||
Reference in New Issue
Block a user