From 218c95165f8c201362a17973fc713c7f46d45e51 Mon Sep 17 00:00:00 2001 From: KAAANG <79990647+SAKURA-CAT@users.noreply.github.com> Date: Wed, 19 Jun 2024 01:32:20 +0800 Subject: [PATCH] update logging --- .gitignore | 3 +- ...45\345\277\227\346\211\223\345\215\260.md" | 168 ++++++++++++++ ...45\345\277\227\346\224\266\351\233\206.md" | 0 swankit/log/log.py | 217 +++++------------- 4 files changed, 232 insertions(+), 156 deletions(-) create mode 100644 "docs/wiki/\347\254\2544\351\203\250\345\210\206\357\274\232\346\227\245\345\277\227\346\211\223\345\215\260.md" delete mode 100644 "docs/wiki/\347\254\2544\351\203\250\345\210\206\357\274\232\346\227\245\345\277\227\346\224\266\351\233\206.md" diff --git a/.gitignore b/.gitignore index 3eddab3..486a198 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ venv/ test/temp/ dist/ -__pycache__ +__pycache__/ .pytest_cache/ +playground/ .DS_Store \ No newline at end of file diff --git "a/docs/wiki/\347\254\2544\351\203\250\345\210\206\357\274\232\346\227\245\345\277\227\346\211\223\345\215\260.md" "b/docs/wiki/\347\254\2544\351\203\250\345\210\206\357\274\232\346\227\245\345\277\227\346\211\223\345\215\260.md" new file mode 100644 index 0000000..57f7087 --- /dev/null +++ "b/docs/wiki/\347\254\2544\351\203\250\345\210\206\357\274\232\346\227\245\345\277\227\346\211\223\345\215\260.md" @@ -0,0 +1,168 @@ +为了方便一些信息的展示,swankit提供了一个简单的日志打印类,它实现了与[logging](https://docs.python.org/zh-cn/3/library/logging.html) +类似的终端打印功能。 + +## 使用日志打印功能 + +[SwanLabSharedLog](https://github.com/SwanHubX/SwanLab-Toolkit/blob/main/swankit/log/log.py) 实现了一个简单的日志打印功能,与 +logging类似,有`debug`、`info`、`warning`、`error`、`critical`等级别的日志打印功能。 +但由于logging很多功能我们并不需要并且有一些兼容性问题,所以我们自己实现了一个类似的。 + +> SwanLabSharedLog使用了[FONT](https://github.com/SwanHubX/SwanLab-Toolkit/blob/main/swankit/log/utils.py) +> 类,并且此类也作为日志的一部分功能暴露,参考[为终端打印添加颜色](#为终端打印添加颜色)。 + +### 基本使用方式 + +通过如下方式使用日志打印功能: + +````python +from swankit.log import SwanLabSharedLog + +logger = SwanLabSharedLog("test") + +logger.info("This is a debug message.") # 输出 test: This is a debug message. +```` + +#### 实例化参数 + +与logging类似,在实例化时可以为其取一个名字,对应不同的日志打印类型,但与logging不同的是,SwanLabSharedLog的实例化并不是全局挂载, +因此即使取两个相同的名字的对象,也不会相互影响。 + +SwanLabSharedLog的实例化参数如下: + +| 参数名 | 类型 | 说明 | +|-------|---------------|--------------------------------------------------------------------------| +| name | str | 日志打印的名字,影响日志的前缀部分 | +| level | int | 日志的等级,参考[日志等级](#日志等级) | +| file | SupportsWrite | 日志输出的文件流,可以是sys.stdout、sys.stderr等,默认为标准输出流stdout,此参数与`print`的`file`参数相同 | + +#### 日志等级 + +logger支持的方法有`debug`、`info`、`warning`、`error`、`critical`,分别对应一个日志等级,等级值如下: + +| 等级名 | 等级值 | +|----------|-----| +| DEBUG | 10 | +| INFO | 20 | +| WARNING | 30 | +| ERROR | 40 | +| CRITICAL | 50 | + +这些在`SwanLabSharedLog`的同名文件也有定义,可以通过`swankit.log.DEBUG`等获取到对应的等级值。 + +### 属性 + +| Attribute | 名称 | 描述 | +|-----------------|------|----------| +| [level](#level) | 日志等级 | 当前日志输出等级 | + +##### level + +`level`属性为日志的输出等级,可以通过`logger.level`获取。 +需要注意的是,获取到的日志等级为int类型,可以通过`swankit.log.DEBUG`等获取到对应的日志等级对应的等级值。 + +**此方法为代理方法**,因此可以通过`logger.level = "debug"`的方式重新设置当前日志等级,考虑到方法为内部使用,所以如果设置的值不在 +期望的范围内,会设置为默认值`info`。 + +### 方法 + +| Method | 名称 | 描述 | +|-----------------------------|--------|-------------------------| +| [日志打印方法](#日志打印方法) | 打印日志 | 为了方便调用,使用不同的方法名对应不同的等级值 | +| [disable_log](#disable_log) | 关闭日志打印 | 关闭日志打印功能 | +| [disable_log](#disable_log) | 开启日志打印 | 开启日志打印功能 | + +#### 日志打印方法 + +SwanLabSharedLog提供了`debug`、`info`、`warning`、`error`、`critical`等方法,分别对应不同的日志等级,调用方式如下: + +````python +from swankit.log import SwanLabSharedLog + +logger = SwanLabSharedLog("test") + +logger.debug("This is a debug message.") # 不输出,因为默认等级为info +logger.info("This is a info message.") # 输出 test: This is a info message. +logger.warning("This is a warning message.") # 输出 test: This is a warning message. +logger.error("This is a error message.") # 输出 test: This is a error message. +logger.critical("This is a critical message.") # 输出 test: This is a critical message. +```` + +可以粗略地将打印日志与`print`方法相等价,区别在于打印日志会占据`file` +参数输出流(也就是实例化传入的输出流),即使设置了`file`参数, +也会在方法内被覆盖 + +#### disable_log + +无参数,直接调用即可关闭日志打印功能。 + +#### enable_log + +无参数,直接调用即可开启日志打印功能。 + +## 为终端打印添加颜色 + +SwanLabSharedLog使用了FONT来为终端打印添加颜色,这是一个简单的方法集合,可以为终端打印添加颜色, +特别的,有一个loading方法,可以实现类似转圈圈的loading效果,FONT支持的函数如下: + +| 函数名 | 描述 | +|---------------------|----------------------------------| +| [loading](#loading) | 实现类似转圈圈的终端loading效果,并且同时执行一个函数任务 | +| [swanlab](#swanlab) | 用于为某一条信息添加swanlab前缀(即将废除) | +| [brush](#brush) | 将当前终端行刷去,替换为新的字符串 | +| debug | 为字符串添加当前终端的默认颜色 | +| bold | 为字符串添加粗体效果 | +| blue | 为字符串添加蓝色效果 | +| grey | 为字符串添加灰色效果 | +| dark_grey | 为字符串添加深灰色效果 | +| green | 为字符串添加绿色效果 | +| dark_green | 为字符串添加深绿色效果 | +| yellow | 为字符串添加黄色效果 | +| red | 为字符串添加红色效果 | +| magenta | 为字符串添加洋红色效果 | +| underline | 为字符串添加下划线效果 | +| clear | 清除字符串的颜色(包括下划线)编码 | + +### 方法 + +在此列出FONT支持的一些较为复杂的方法 + +#### loading + +此函数本质上使用线程实现,一般用于发送请求时的loading效果,其参数如下: + +| 参数名 | 类型 | 说明 | +|--------------|----------|---------------------------| +| s | str | loading的提示信息,需要打印的字符串 | +| func | Callable | 需要执行的函数 | +| args | Tuple | 函数的参数 | +| interval | float | loading旋转刷新的间隔时间,默认为0.4s | +| prefix | str | loading的前缀,默认为"swanlab: " | +| brush_length | int | 刷去的长度,参考[brush](#brush) | + +#### swanlab + +此函数为字符串添加swanlab前缀,可选择color颜色(需要FONT支持的方法),即将废除,不建议使用。 + +#### brush + +此函数用于刷去当前终端行,替换为新的字符串,其参数如下: + +| 参数名 | 类型 | 说明 | +|--------|-----|---------------| +| s | str | 需要替换的字符串 | +| length | int | 需要刷去的长度,默认为20 | + +## 相关问题 + +### 为什么不使用python的[logging](https://docs.python.org/zh-cn/3/library/logging.html)库实现日志打印? + +因为似乎存在一些兼容性问题,参考此[issue](https://github.com/SwanHubX/SwanLab-Toolkit/issues/9)。 + +### 应该可以依赖于一些强大的开源库,比如[rich](https://github.com/Textualize/rich)? + +确实可以,但是目前我们并没有用到那么多的功能,所以暂时没有引入。 + +### 为什么FONT.loading方法使用线程实现而不是异步协程? + +因为存在兼容性问题(也有可能是我写的有问题,总之python协程 + 多线程似乎有一些奇怪的问题) + diff --git "a/docs/wiki/\347\254\2544\351\203\250\345\210\206\357\274\232\346\227\245\345\277\227\346\224\266\351\233\206.md" "b/docs/wiki/\347\254\2544\351\203\250\345\210\206\357\274\232\346\227\245\345\277\227\346\224\266\351\233\206.md" deleted file mode 100644 index e69de29..0000000 diff --git a/swankit/log/log.py b/swankit/log/log.py index b9d58eb..350bd50 100644 --- a/swankit/log/log.py +++ b/swankit/log/log.py @@ -7,191 +7,98 @@ @Description: 日志模块,封装logging模块,提供swanlab标准日志记录功能 """ -import logging from .utils import FONT from typing import Literal, Union import sys - -class ColoredFormatter(logging.Formatter, FONT): - def __init__(self, fmt=None, datefmt=None, style="%", handle=None): - super().__init__(fmt, datefmt, style) - self.__handle = handle - # 打印等级对应的颜色装载器 - self.__color_map = { - logging.DEBUG: self.grey, - logging.INFO: self.bold_blue, - logging.WARNING: self.yellow, - logging.ERROR: self.red, - logging.CRITICAL: self.bold_red, - } - - def bold_red(self, s: str) -> str: - """在终端中加粗的红色字符串 - - Parameters - ---------- - s : str - 需要加粗的字符串 - - Returns - ------- - str - 加粗后的字符串 - """ - # ANSI 转义码用于在终端中改变文本样式 - return self.bold(self.red(s)) - - def bold_blue(self, s: str) -> str: - """在终端中加粗的蓝色字符串 - - Parameters - ---------- - s : str - 需要加粗的字符串 - - Returns - ------- - str - 加粗后的字符串 - """ - return self.bold(self.blue(s)) - - def __get_colored_str(self, levelno, message): - """获取使用打印等级对应的颜色装载的字符串 - - Parameters - ---------- - levelno : logging.levelno - logging 等级对象 - message : string - 需要装载的颜色 - """ - - return self.__color_map.get(levelno)(message) - - def format(self, record): - """格式化打印字符串 - 1. 分割消息头和消息体 - 2. 消息头根据 logging 等级装载颜色 - 3. 使用空格填充,统一消息头长度为 20 个字符 - 4.. 拼接消息头和消息体 - - Parameters - ---------- - record : logging.record - logging 信息实例 - - Returns - ------- - string - 格式化后的字符串 - """ - log_message = super().format(record) - self.__handle(log_message + "\n") if self.__handle else None - # 分割消息,分别处理头尾 - messages: list = log_message.split(":", 1) - # 填充空格,统一消息头的长度 - message_header = messages[0] - return f"{self.__get_colored_str(record.levelno, message_header)}:{messages[1]}" - - +# ---------------------------------- 日志打印等级 ---------------------------------- Levels = Union[Literal["debug", "info", "warning", "error", "critical"], str] """ -SwanKitLog 预先定义好的日志等级 +SwanKitLog 预先定义好的日志等级字符串 """ +CRITICAL = 50 +ERROR = 40 +WARNING = 30 +INFO = 20 +DEBUG = 10 -def concat_messages(func): - """ - 装饰器,当传递打印信息有多个时,拼接为一个,并且拦截记录它们 - """ - - def wrapper(self, *args, **kwargs): - # 拼接消息,首先将所有参数转换为字符串,然后拼接 - args = [str(arg) for arg in args] - message = " ".join(args) - func(self, message, **kwargs) - - return wrapper +# ---------------------------------- 日志类 ---------------------------------- class SwanLabSharedLog: - # 日志系统支持的输出等级 - levels = { - "debug": logging.DEBUG, - "info": logging.INFO, - "warning": logging.WARNING, - "error": logging.ERROR, - "critical": logging.CRITICAL, - } - - def __init__(self, name=__name__.lower(), level: Levels = "info"): - self.prefix = name + ':' - self.__logger = logging.getLogger(name) - self.level = level - # 初始化控制台日志处理器,输出到标准输出流 - self.__handler = logging.StreamHandler(sys.stdout) - # 添加颜色格式化,并在此处设置格式化后的输出流是否可以被其他处理器处理 - colored_formatter = ColoredFormatter("%(name)s: %(message)s") - self.__handler.setFormatter(colored_formatter) - self.enable_log() - - def disable_log(self): - """ - 是否开启日志输出,实例化时默认开启 - """ - self.__logger.removeHandler(self.__handler) - def enable_log(self): - self.__logger.addHandler(self.__handler) + def __init__(self, name=__name__.lower(), level: Levels = "info", file=None): + self.__file = file if file else sys.stdout + self.__level: int = 0 + self.__prefix_dict = { + "debug": FONT.grey(name) + ":", + "info": FONT.blue(name) + ":", + "warning": FONT.yellow(name) + ":", + "error": FONT.red(name) + ":", + "critical": FONT.bold(FONT.red(name)) + ":", + } + self.__levels_dict = { + "debug": DEBUG, + "info": INFO, + "warning": WARNING, + "error": ERROR, + "critical": CRITICAL, + } + self.level = level + self.__can_log = True @property def level(self): - return self.__logger.level + return self.__level @level.setter def level(self, level: Levels): """ - Set the logging level of the logger. + 设置日志等级 + :param level: 日志等级,可选值为 debug, info, warning, error, critical,如果传入的值不在可选值中,则默认为 info + """ + self.__level = self.__levels_dict.get(level.lower(), 20) + + def disable_log(self): + """ + 关闭日志输出,实例化时默认开启 + """ + self.__can_log = False - :param level: The level to set the logger to. This should be one of the following: - - "debug" - - "info" - - "warning" - - "error" - - "critical" + def enable_log(self): + """ + 开启日志输出 + """ + self.__can_log = True - :raises: KeyError: If an invalid level is passed. + def __print(self, log_level: str, *args, **kwargs): + """ + 打印日志 """ - self.__logger.setLevel(self.levels.get(level.lower())) + if not self.__can_log: + return + level = self.__levels_dict[log_level] + if level < self.__level: + return + print(self.__prefix_dict[log_level], *args, **kwargs, file=self.__file) # 发送调试消息 - @concat_messages - def debug(self, message): - self.__logger.debug(message) - return + def debug(self, *args, **kwargs): + return self.__print("debug", *args, **kwargs) # 发送通知 - @concat_messages - def info(self, message): - self.__logger.info(message) - return + def info(self, *args, **kwargs): + return self.__print("info", *args, **kwargs) # 发生警告 - @concat_messages - def warning(self, message): - self.__logger.warning(message) - return + def warning(self, *args, **kwargs): + return self.__print("warning", *args, **kwargs) # 发生错误 - @concat_messages - def error(self, message): - self.__logger.error(message) - return + def error(self, *args, **kwargs): + return self.__print("error", *args, **kwargs) # 致命错误 - @concat_messages - def critical(self, message): - self.__logger.critical(message) - return + def critical(self, *args, **kwargs): + return self.__print("critical", *args, **kwargs)