You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在不定义自定义日志级别时,使logging.exception仅输出至日志文件,stdout显示短错误信息

解决方案

核心思路

logging.exception()本质是ERROR级别日志+自动填充exc_info=True,我们可以利用这个特性,在标准输出的日志处理器中,针对带有exc_info的日志记录只输出简短错误消息;文件处理器则保留完整的异常栈追踪信息,全程无需自定义日志级别,完全保留用户的自定义空间。

修改步骤及代码实现

1. 调整标准输出的Formatter类

修改Formatter类的format方法,当检测到日志记录包含exc_info时,临时屏蔽异常栈追踪的渲染,只输出基础错误信息:

class Formatter(logging.Formatter):
    def format(self, record):
        color = {
            logging.CRITICAL: 31,
            logging.ERROR: 31,
            logging.FATAL: 31,
            logging.WARNING: 33,
            logging.DEBUG: 36,
            logging.INFO: 32
        }.get(record.levelno, 0)
        self._style._fmt = f"\033[37m[%(asctime)s]\033[0m \033[{color}m%(levelname)-8s\033[0m %(message)s"
        
        # 处理exception生成的日志:临时清空exc_info,避免输出栈追踪
        if record.exc_info:
            original_exc_info = record.exc_info
            record.exc_info = None
            formatted_msg = super().format(record)
            record.exc_info = original_exc_info
            return formatted_msg
        
        return super().format(record)

2. 保留文件处理器的完整异常输出逻辑

文件处理器的file_formatter使用原生格式逻辑即可,它会自动渲染exc_info对应的栈追踪,原代码中的设置已经满足需求:

file_formatter = logging.Formatter(f"[%(asctime)s] %(levelname)-8s [%(filename)s/%(funcName)s:%(lineno)s]  %(message)s")

3. 完整修改后的代码

import logging
import sys
import os
from pathlib import Path
from dataclasses import fields
from API.global_constants.dataclasses import LoggerTemplate
from API.global_constants.vars import LOGGER_NAME


def set_logger(**kwargs):
    logger_template = LoggerTemplate()
    for key, value in kwargs.items():
        if key in [field.name for field in fields(logger_template)]:
            setattr(logger_template, key, value)

    if not isinstance(logger_template.logger, logging.Logger):
        logger_template.logger = logging.getLogger(LOGGER_NAME)
    else:  # 用户自定义了logger,直接返回
        return logger_template.logger

    logger = logger_template.logger

    if not logger_template.enable_logger:
        logger.setLevel(logging.CRITICAL + 1)
    else:
        logger.setLevel(logging.DEBUG)

        std_formatter = Formatter()
        std_handler = logging.StreamHandler(sys.stdout)
        std_handler.setLevel(logger_template.log_std_level)
        std_handler.setFormatter(std_formatter)
        logger.addHandler(std_handler)

        if not logger_template.enable_logfile:
            logger.info(f'Logging started without file.')
        else:
            file_formatter = logging.Formatter(f"[%(asctime)s] %(levelname)-8s [%(filename)s/%(funcName)s:%(lineno)s]  %(message)s")
            Path.mkdir(Path(logger_template.logfile_path), parents=True, exist_ok=True)
            log_file_path = Path(logger_template.logfile_path) / Path(str(logger_template.logfile_name))
            file_handler = logging.FileHandler(log_file_path)
            file_handler.setLevel(logger_template.logfile_level)
            file_handler.setFormatter(file_formatter)
            logger.addHandler(file_handler)
            logger.info(f'Logging started in the [{log_file_path}].')

    return logger


class Formatter(logging.Formatter):
    def format(self, record):
        color = {
            logging.CRITICAL: 31,
            logging.ERROR: 31,
            logging.FATAL: 31,
            logging.WARNING: 33,
            logging.DEBUG: 36,
            logging.INFO: 32
        }.get(record.levelno, 0)
        self._style._fmt = f"\033[37m[%(asctime)s]\033[0m \033[{color}m%(levelname)-8s\033[0m %(message)s"
        
        # 处理exception日志:临时清空exc_info,避免输出栈追踪
        if record.exc_info:
            original_exc_info = record.exc_info
            record.exc_info = None
            result = super().format(record)
            record.exc_info = original_exc_info
            return result
        
        return super().format(record)


if __name__ == '__main__':
    # 测试时开启日志文件
    logger1 = set_logger(enable_logfile=True, logfile_name='test_log.log')
    logger1.debug('test message')
    logger1.info('test message')
    logger1.warning('test message')
    logger1.error('test message')
    logger1.critical('test message')
    try:
        1 / 0
    except ZeroDivisionError:
        logger1.exception('Division by zero occurred')

效果验证

  • 标准输出(stdout):仅显示带彩色格式的简短错误消息,无栈追踪:
    [2024-05-20 12:34:56,789] ERROR    Division by zero occurred
    
  • 日志文件:显示完整的ERROR级别日志,包含完整栈追踪:
    [2024-05-20 12:34:56,789] ERROR    [test.py/<module>:87]  Division by zero occurred
    Traceback (most recent call last):
      File "test.py", line 85, in <module>
        1 / 0
    ZeroDivisionError: division by zero
    

注意事项

  • 方案完全基于Python logging原生机制,未新增自定义日志级别,保留了用户的自定义空间(比如用户可通过设置log_std_level=logging.CRITICAL关闭标准输出的ERROR级日志)
  • 临时修改record.exc_info后会立即恢复,不会影响其他日志处理器的正常工作

内容的提问来源于stack exchange,提问作者Mika

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 14:40:53