Python自带日志库实现springboot彩色效果

整体目标

在这里插入图片描述

涉及的库均为Python3自带库实现

  • logging
  • sys
  • enum

终端显示彩色基本原理参考👉Terminal里的颜色的那些事

Python打印日志可以直接借用logging自带的库实现,但是默认的打印实在太丑了,长下面这样

在这里插入图片描述

这只是一条日志看着还好比较清爽,如果多了之后可想而知,大片红色看着由多么糟糕,借鉴SpringBoot的处理怎么做到SpringBoot那样的彩色日志输出呢?

在这里插入图片描述

这里突然想起来,Java自带的日志框架(JUL)打印也是一片红色,可能原生的东西都比较呆正。

日志框架的组成

日志框架无论哪种语言基本都是遵循一样的做法,包括以下组件:

  • Logger:日志记录器,用于记录打印日志,我们操作的就是这个对象
  • Handler:处理器,有些也叫Appender,就是实际处理日志记录的东西,比如说我们生成一条日志字符串,这个字符串是输出到控制台,还是输出到文件呢?或者说两者都输出呢?这就是处理器做的事情,如果存在多个处理器还可以重复输出。
  • Formatter:格式化器,有些框架也叫Layout,就是格式化信息的,比如我们看到上面工工整整的时间,日志级别,就是我们定义好的格式,交给他处理。

正常打印一条日志,都是由日志记录器开始,日志记录器存在属性Handler处理器,处理器Handler存在属性Formatter。因此我们需要操作的对象就是Formatter

import logging  
  
if __name__ == '__main__':  
    # Logger Handler Formatter 三者之间的关系,相互链接建立联系  
    formatter = logging.Formatter(logging.BASIC_FORMAT)  
    handler = logging.StreamHandler()  
      
    handler.setFormatter(formatter)  
    logging.getLogger().addHandler(handler)

因此我们如果要自定义处理器或者自定义格式化器,都是从上面的setFormatter和addHandler入手的。

如何自定义这些组件? 答案是继承,Python中我们无法实现接口,但是可以继承基类,只要是儿子,自然也就是处理器,或者格式化器。

class CustomFormatter(logging.Formatter):  
	# 重写父类的方法,再调用父类,相当于增强方法
    def format(self, record):   
	    # 这里就可以处理记录record的了
        s = super().format(record)  
        return s

我们的基本思路就是拿到需要格式化的数据,%(asctime)s时间字符串,%(levelname)s等级字符串这些,拿到这些数据那就可以进行彩色处理了。

终端彩色显示

ANSI转义序列(ANSI escape sequences)是一种带内信号的转义序列标准,用于控制视频文本终端上的光标位置、颜色和其他选项。在文本中嵌入确定的字节序列,大部分以ESC转义字符和"["字符开始,终端会把这些字节序列解释为相应的指令,而不是普通的字符编码。

大体原理就是类似\r代表回到开头,\n代表换行这种,具有一定的转义含义的东西,在终端不同的转义含义就可以控制输出的字体颜色,背景颜色,字体加粗,划线等等。

基本格式为:

\033[XXXm

其中\033[以及后面的m都是固定的,只有XXX代表不同含义,分别由;分割,比如下面的例子

print("\033[31;9;3mHello Python\033[m")

其中31表示前景色(字体颜色)为红色,9表示划除线,3表示斜体

效果图
在这里插入图片描述

注意:并不是说第一个数字一定要控制颜色,也可以完全没有颜色控制的!

具体的ANSI转义表参考复杂正规版

简单的网上搜一下就好,就是说每个数字代表什么含义的码表,比如你想要粗体应该是数字1。

我们的目的是颜色,标准颜色的数字是30~37分别如下表示:

颜色前景色代码
黑色30
红色31
绿色32
黄色33
蓝色34
品红35
青色36
白色37
print("\033[31mINFO\033[m")  
print("\033[32mINFO\033[m")  
print("\033[33mINFO\033[m")  
print("\033[34mINFO\033[m")  
print("\033[35mINFO\033[m")  
print("\033[36mINFO\033[m")  
print("\033[37mINFO\033[m")

效果图
在这里插入图片描述

所以我们只需要获取到指定字符串然后包裹封装就可以了,问题是怎么获取呢?

完整实现

from enum import Enum
import logging

# 枚举类用来记录颜色
class Color(Enum):  
    BLACK = 30  
    RED = 31  
    GREEN = 32  
    YELLOW = 33  
    BLUE = 34  
    CYAN = 36 
    WHITE = 37 
    DEFAULT = 39 

# ANSI转义颜色包裹需要颜色显示的字符串
def color_text(text: str, color: Color) -> str:  
    return "\033[{}m{}\033[0m".format(str(color.value), text)


# 自定义格式化器继承Formatter,在format格式化之前拿到需要变化的字段字符串,这里主要就是日志级别和记录器名称
class CustomFormatter(logging.Formatter):  
    def format(self, record):  
        if record.levelname == "INFO":  
            level_name_color = Color.GREEN  
        elif record.levelname == "ERROR":  
            level_name_color = Color.RED  
        elif record.levelname == "WARNING":  
            level_name_color = Color.YELLOW  
        else:  
            level_name_color = Color.DEFAULT  
  
        record.levelname = color_text(record.levelname, level_name_color)  
        record.name = color_text(record.name, Color.CYAN)  
        s = super().format(record)  
        return s
    
# 设置自定义格式化器,这里封装一个方法用来获取日志记录器,正好在这里添加格式化器,控制台输出处理器Handler默认是StreamHandler
def getLogger(name: str = __name__):  
    logger = logging.getLogger(name)  
  
    handler = logging.StreamHandler()  
  
    handler.setLevel("DEBUG")  
    format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"  
    handler.setFormatter(CustomFormatter(fmt=format))  
  
    logger.addHandler(handler)  
    logger.propagate = 0  
    return logger

效果图
在这里插入图片描述

注意:这里需要配置记录器的日志级别,记录器的日志级别向下传递给处理器,所以其实处理器本质不设置日志级别也是可以的,由记录器传递。

上面还有个关键问题,为什么整体颜色还是红色?

答案是对于终端的输出分为stdout和stderr,而StreamHandler.stream输出流默认输出为stderr

在这里插入图片描述

因此我们还需要加一行关键的代码。

getLogger方法中添加

handler.stream = sys.stdout

最终效果图
在这里插入图片描述

Note:

  • 为了更接近SpringBoot效果,对格式化内容做了调整
  • 日志级别其实最高也就WARNING共7个字符,但是由于加了转义序列,实际长度并不是7,因此如果太小则会出现对不齐的情况,这里采用%(levelname)18s占位18个字符右对齐。

个人认为比较好看的格式:

"%(asctime)s %(levelname)18s %(process)d --- [%(module)10s] [ %(name)15s] %(funcName)-20s \t: %(message)s"

效果图
在这里插入图片描述

顺序分别记录下面信息:

  • 日志时间
  • 日志级别
  • 进程ID
  • 模块名,即输出日志的文件名称
  • 记录器Logger名称
  • 方法名

技巧: 对于需要改变的颜色因为包裹了转义字符,需要给占位符的字符尽量大些(实际显示并不会占位太长),而对于不需要颜色显示的字段,尽量估算最大可能显示的长度占位。

高级玩法

如何让日志在行内打印?

如果日志打印一直是重复的话,我们希望打印内容不要换行,而是行内打印,类似print下面的用法。

try:  
    last_time = time.time()  
    while True:  
        print(f"\rHello Python...{round(time.time()-last_time,2)}秒", end='')  
        time.sleep(1)  
except KeyboardInterrupt:  
    print("\n程序运行结束")  
    sys.exit(0)

效果图

这里涉及两个知识:

  • \r转义的含义是光标移动到开头
  • end=’’ 说明没行的结束没有换行符号\n

print的本质就是输出流,输出对象就是sys.out,之前提过输出流分为stdoutstderr,作为标准输出,通过end=’’ 这样在流写二进制这种字节数值给计算机终端时候,判断没有\n就不会进行换行操作,于是一直在当前行打印,由于光标移动到了开头,所以打印的实际效果是:

# 第一遍打印
Hello Python...1秒

# 第二遍打印
Hello Python...2秒

但是由于都在第一行,所以显示的效果就是在计算秒表,这是基本原理。

日志行内打印完整实现

# 这是自定义的格式化器,只要是为了ANSI转义包裹下字符串颜色显示
class CustomFormatter(logging.Formatter):  
	# 主要目的是为了记录上一次打印的数据
    last_str = None  
  
    def format(self, record):  
        if record.levelname == "INFO":  
            level_name_color = Color.GREEN  
        elif record.levelname == "ERROR":  
            level_name_color = Color.RED  
        elif record.levelname == "WARNING":  
            level_name_color = Color.YELLOW  
        else:  
            level_name_color = Color.DEFAULT  
  
        record.levelname = color_text(record.levelname, level_name_color)  
        record.funcName = color_text(record.funcName, Color.CYAN)  
        s = super().format(record)  
        self.last_str = s  
        return s

class Logger(logging.Logger):  
    def __init__(self, name):  
        super().__init__(name)  
	# 覆盖原本的info方法,就是用来增强该方法,定义一个inline可选参数,如果指定则行内打印
    def info(self, msg, inline: bool = False, *args, **kwargs):  
        if inline:  
            for handler in self.handlers:  
                if isinstance(handler, logging.StreamHandler): 
	                # 就是这里保证打印在一行的,等价print(end='')的作用 
                    old = handler.terminator  
                    handler.terminator = ''  
                    # 实际打印方法还是原来的,由于方法包裹,调用方法永远是info,因此设置stacklevel为2这样调用方法名就是上级了
                    super().info(msg, *args, stacklevel=2)  
                    if isinstance(handler.formatter, CustomFormatter):  
                        if handler.formatter.last_str is not None:  
	                        # 如果存在字符我们就回到光标处继续打印
                            handler.stream.write('\r' + handler.formatter.last_str)  
                            handler.stream.flush()  
                    # 结束重置结束符,不然一直回行内打印(单例对象) 
                    handler.terminator = old
        else:  
		    super().info(msg, *args, stacklevel=2)

效果图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值