StarRailCopilot/module/logger/logger.py
2023-09-10 01:53:34 +08:00

361 lines
11 KiB
Python

import datetime
import logging
import os
import sys
from typing import Callable, List
from rich.console import Console, ConsoleOptions, ConsoleRenderable, NewLine
from rich.highlighter import NullHighlighter, RegexHighlighter
from rich.logging import RichHandler
from rich.rule import Rule
from rich.style import Style
from rich.theme import Theme
from rich.traceback import Traceback
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
def empty_function(*args, **kwargs):
pass
# cnocr will set root logger in cnocr.utils
# Delete logging.basicConfig to avoid logging the same message twice.
logging.basicConfig = empty_function
logging.raiseExceptions = True # Set True if wanna see encode errors on console
# Remove HTTP keywords (GET, POST etc.)
RichHandler.KEYWORDS = []
class RichFileHandler(RichHandler):
# Rename
pass
class RichRenderableHandler(RichHandler):
"""
Pass renderable into a function
"""
def __init__(self, *args, func: Callable[[ConsoleRenderable], None] = None, **kwargs):
super().__init__(*args, **kwargs)
self._func = func
def emit(self, record: logging.LogRecord) -> None:
message = self.format(record)
traceback = None
if (
self.rich_tracebacks
and record.exc_info
and record.exc_info != (None, None, None)
):
exc_type, exc_value, exc_traceback = record.exc_info
assert exc_type is not None
assert exc_value is not None
traceback = Traceback.from_exception(
exc_type,
exc_value,
exc_traceback,
width=self.tracebacks_width,
extra_lines=self.tracebacks_extra_lines,
theme=self.tracebacks_theme,
word_wrap=self.tracebacks_word_wrap,
show_locals=self.tracebacks_show_locals,
locals_max_length=self.locals_max_length,
locals_max_string=self.locals_max_string,
)
message = record.getMessage()
if self.formatter:
record.message = record.getMessage()
formatter = self.formatter
if hasattr(formatter, "usesTime") and formatter.usesTime():
record.asctime = formatter.formatTime(
record, formatter.datefmt)
message = formatter.formatMessage(record)
message_renderable = self.render_message(record, message)
log_renderable = self.render(
record=record, traceback=traceback, message_renderable=message_renderable
)
# Directly put renderable into function
self._func(log_renderable)
def handle(self, record: logging.LogRecord) -> bool:
if not self._func:
return True
super().handle(record)
class HTMLConsole(Console):
"""
Force full feature console
but not working lol :(
"""
@property
def options(self) -> ConsoleOptions:
return ConsoleOptions(
max_height=self.size.height,
size=self.size,
legacy_windows=False,
min_width=1,
max_width=self.width,
encoding='utf-8',
is_terminal=False,
)
class Highlighter(RegexHighlighter):
base_style = 'web.'
highlights = [
# (r'(?P<datetime>(\d{2}|\d{4})(?:\-)?([0]{1}\d{1}|[1]{1}[0-2]{1})'
# r'(?:\-)?([0-2]{1}\d{1}|[3]{1}[0-1]{1})(?:\s)?([0-1]{1}\d{1}|'
# r'[2]{1}[0-3]{1})(?::)?([0-5]{1}\d{1})(?::)?([0-5]{1}\d{1}).\d+\b)'),
(r'(?P<time>([0-1]{1}\d{1}|[2]{1}[0-3]{1})(?::)?'
r'([0-5]{1}\d{1})(?::)?([0-5]{1}\d{1})(.\d+\b))'),
r"(?P<brace>[\{\[\(\)\]\}])",
r"\b(?P<bool_true>True)\b|\b(?P<bool_false>False)\b|\b(?P<none>None)\b",
r"(?P<path>(([A-Za-z]\:)|.)?\B([\/\\][\w\.\-\_\+]+)*[\/\\])(?P<filename>[\w\.\-\_\+]*)?",
# r"(?<![\\\w])(?P<str>b?\'\'\'.*?(?<!\\)\'\'\'|b?\'.*?(?<!\\)\'|b?\"\"\".*?(?<!\\)\"\"\"|b?\".*?(?<!\\)\")",
]
WEB_THEME = Theme({
"web.brace": Style(bold=True),
"web.bool_true": Style(color="bright_green", italic=True),
"web.bool_false": Style(color="bright_red", italic=True),
"web.none": Style(color="magenta", italic=True),
"web.path": Style(color="magenta"),
"web.filename": Style(color="bright_magenta"),
"web.str": Style(color="green", italic=False, bold=False),
"web.time": Style(color="cyan"),
"rule.text": Style(bold=True),
})
# Logger init
logger_debug = False
logger = logging.getLogger('alas')
logger.setLevel(logging.DEBUG if logger_debug else logging.INFO)
file_formatter = logging.Formatter(
fmt='%(asctime)s.%(msecs)03d | %(levelname)s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
console_formatter = logging.Formatter(
fmt='%(asctime)s.%(msecs)03d%(message)s', datefmt='%Y-%m-%d %H:%M:%S')
web_formatter = logging.Formatter(
fmt='%(asctime)s.%(msecs)03d%(message)s', datefmt='%H:%M:%S')
# Add console logger
# console = logging.StreamHandler(stream=sys.stdout)
# console.setFormatter(formatter)
# console.flush = sys.stdout.flush
# logger.addHandler(console)
# Add rich console logger
stdout_console = console = Console()
console_hdlr = RichHandler(
show_path=False,
show_time=False,
rich_tracebacks=True,
tracebacks_show_locals=True,
tracebacks_extra_lines=3,
)
console_hdlr.setFormatter(console_formatter)
logger.addHandler(console_hdlr)
# Ensure running in Alas root folder
os.chdir(os.path.join(os.path.dirname(__file__), '../../'))
# Add file logger
pyw_name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
def _set_file_logger(name=pyw_name):
if '_' in name:
name = name.split('_', 1)[0]
log_file = f'./log/{datetime.date.today()}_{name}.txt'
try:
file = logging.FileHandler(log_file, encoding='utf-8')
except FileNotFoundError:
os.mkdir('./log')
file = logging.FileHandler(log_file, encoding='utf-8')
file.setFormatter(file_formatter)
logger.handlers = [h for h in logger.handlers if not isinstance(
h, (logging.FileHandler, RichFileHandler))]
logger.addHandler(file)
logger.log_file = log_file
def set_file_logger(name=pyw_name):
if '_' in name:
name = name.split('_', 1)[0]
log_file = f'./log/{datetime.date.today()}_{name}.txt'
try:
file = open(log_file, mode='a', encoding='utf-8')
except FileNotFoundError:
os.mkdir('./log')
file = open(log_file, mode='a', encoding='utf-8')
file_console = Console(
file=file,
no_color=True,
highlight=False,
width=119,
)
hdlr = RichFileHandler(
console=file_console,
show_path=False,
show_time=False,
show_level=False,
rich_tracebacks=True,
tracebacks_show_locals=True,
tracebacks_extra_lines=3,
highlighter=NullHighlighter(),
)
hdlr.setFormatter(file_formatter)
logger.handlers = [h for h in logger.handlers if not isinstance(
h, (logging.FileHandler, RichFileHandler))]
logger.addHandler(hdlr)
logger.log_file = log_file
def set_func_logger(func):
console = HTMLConsole(
force_terminal=False,
force_interactive=False,
width=80,
color_system='truecolor',
markup=False,
safe_box=False,
highlighter=Highlighter(),
theme=WEB_THEME
)
hdlr = RichRenderableHandler(
func=func,
console=console,
show_path=False,
show_time=False,
show_level=True,
rich_tracebacks=True,
tracebacks_show_locals=True,
tracebacks_extra_lines=2,
highlighter=Highlighter(),
)
hdlr.setFormatter(web_formatter)
logger.handlers = [h for h in logger.handlers if not isinstance(
h, RichRenderableHandler)]
logger.addHandler(hdlr)
def _get_renderables(
self: Console, *objects, sep=" ", end="\n", justify=None, emoji=None, markup=None, highlight=None,
) -> List[ConsoleRenderable]:
"""
Refer to rich.console.Console.print()
"""
if not objects:
objects = (NewLine(),)
render_hooks = self._render_hooks[:]
with self:
renderables = self._collect_renderables(
objects,
sep,
end,
justify=justify,
emoji=emoji,
markup=markup,
highlight=highlight,
)
for hook in render_hooks:
renderables = hook.process_renderables(renderables)
return renderables
def print(*objects: ConsoleRenderable, **kwargs):
for hdlr in logger.handlers:
if isinstance(hdlr, RichRenderableHandler):
for renderable in _get_renderables(hdlr.console, *objects, **kwargs):
hdlr._func(renderable)
elif isinstance(hdlr, RichHandler):
hdlr.console.print(*objects)
def rule(title="", *, characters="", style="rule.line", end="\n", align="center"):
rule = Rule(title=title, characters=characters,
style=style, end=end, align=align)
print(rule)
def hr(title, level=3):
title = str(title).upper()
if level == 1:
logger.rule(title, characters='')
logger.info(title)
if level == 2:
logger.rule(title, characters='')
logger.info(title)
if level == 3:
logger.info(f"[bold]<<< {title} >>>[/bold]", extra={"markup": True})
if level == 0:
logger.rule(characters='')
logger.rule(title, characters=' ')
logger.rule(characters='')
def attr(name, text):
logger.info('[%s] %s' % (str(name), str(text)))
def attr_align(name, text, front='', align=22):
name = str(name).rjust(align)
if front:
name = front + name[len(front):]
logger.info('%s: %s' % (name, str(text)))
def show():
logger.info('INFO')
logger.warning('WARNING')
logger.debug('DEBUG')
logger.error('ERROR')
logger.critical('CRITICAL')
logger.hr('hr0', 0)
logger.hr('hr1', 1)
logger.hr('hr2', 2)
logger.hr('hr3', 3)
logger.info(r'Brace { [ ( ) ] }')
logger.info(r'True, False, None')
logger.info(r'E:/path\\to/alas/alas.exe, /root/alas/, ./relative/path/log.txt')
local_var1 = 'This is local variable'
# Line before exception
raise Exception("Exception")
# Line below exception
def error_convert(func):
def error_wrapper(msg, *args, **kwargs):
if isinstance(msg, Exception):
msg = f'{type(msg).__name__}: {msg}'
return func(msg, *args, **kwargs)
return error_wrapper
logger.error = error_convert(logger.error)
logger.hr = hr
logger.attr = attr
logger.attr_align = attr_align
logger.set_file_logger = set_file_logger
logger.set_func_logger = set_func_logger
logger.rule = rule
logger.print = print
logger.log_file: str
logger.set_file_logger()
logger.hr('Start', level=0)