Python 使用 dictConfig 进行日志记录,使用两个流处理程序以不同的消息级别发布到 stdout 和 stderr

Python logging with dictConfig using two streamhandlers posting to stdout and stderr at different message levels

我正在努力实现这些帖子中所做的事情 Python logging split between stdout and stderr

Python logging split between stdout and stderr

但是用dictConfig,到目前为止还没有成功。这是我的代码和配置:

这是我用来生成标准输出日志的配置

# logging.json
{
    "version": 1,
    "disable_existing_loggers": false,
    "formatters": {
        "console": {
            "format": "%(asctime)s: %(levelname)s: %(pathname)s: \n%(message)s\n"
        },
        "file": {
            "format": "%(asctime)s: %(levelname)s: %(pathname)s: %(lineno)d: \n%(message)s\n"
        }
    },
    "handlers": {
            "console": {
                "level": "INFO",
                "formatter": "console",
                "class": "logging.StreamHandler",
                "stream": "ext://sys.stdout"
            },
            "file": {
                "level": "DEBUG",
                "formatter": "file",
                "class": "logging.FileHandler",
                "encoding": "utf-8",
                "filename": "app.log"
            }
    },
    "loggers": {
        "": {
            "handlers": ["console", "file"],
            "level": "INFO",
            "propagate": false
        },
        "default": {
            "handlers": ["console", "file"],
            "level": "DEBUG",
            "propagate": false
        }

    }
}

而这个是用于 stderr 日志的

# logging_stderr.json

{
    "version": 1,
    "disable_existing_loggers": false,
    "formatters": {
        "console": {
            "format": "%(asctime)s: %(levelname)s: %(pathname)s: \n%(message)s\n"
        },
        "file": {
            "format": "%(asctime)s: %(levelname)s: %(pathname)s: %(lineno)d: \n%(message)s\n"
        }
    },
    "handlers": {
            "console": {
                "level": "WARN",
                "formatter": "console",
                "class": "logging.StreamHandler",
                "stream": "ext://sys.stderr"
            },
            "file": {
                "level": "DEBUG",
                "formatter": "file",
                "class": "logging.FileHandler",
                "encoding": "utf-8",
                "filename": "wusync.log"
            }
    },
    "loggers": {
        "": {
            "handlers": ["console", "file"],
            "level": "INFO",
            "propagate": false
        },
        "default": {
            "handlers": ["console", "file"],
            "level": "DEBUG",
            "propagate": false
        }

    }
}

然后在我的代码中,我有一个辅助函数。

import logging
import logging.config
import os
from os.path import abspath, basename, dirname, exists, isfile, isdir, join, split, splitext
import sys

_script_dir = abspath(dirname(__file__))


def build_default_logger(logdir, name=None, cfgfile=None):
    """
    Create per-file logger and output to shared log file.
    - If found config file under script folder, use it;
    - Otherwise use default config: save to /project_root/project_name.log.
    - 'filename' in config is a filename; must prepend folder path to it.
    :logdir: directory the log file is saved into.
    :name: basename of the log file,
    :cfgfile: config file in the format of dictConfig.
    :return: logger object.
    """
    try:
        os.makedirs(logdir)
    except:
        pass

    cfg_file = cfgfile or join(_script_dir, 'logging.json')
    logging_config = None
    try:
        if sys.version_info.major > 2:
            with open(cfg_file, 'r', encoding=TXT_CODEC, errors='backslashreplace', newline=None) as f:
                text = f.read()
        else:
            with open(cfg_file, 'rU') as f:
                text = f.read()
        # Add object_pairs_hook=collections.OrderedDict hook for py3.5 and lower.
        logging_config = json.loads(text, object_pairs_hook=collections.OrderedDict)
        logging_config['handlers']['file']['filename'] = join(logdir, logging_config['handlers']['file']['filename'])
    except Exception:
        filename = name or basename(basename(logdir.strip('\/')))
        log_path = join(logdir, '{}.log'.format(filename))
        logging_config = {
            "version": 1,
            "disable_existing_loggers": False,
            "formatters": {
                "console": {
                    "format": "%(asctime)s: %(levelname)s: %(pathname)s: \n%(message)s\n"
                },
                "file": {
                    "format": "%(asctime)s: %(levelname)s: %(pathname)s: %(lineno)d: \n%(message)s\n"
                }
            },
            "handlers": {
                    "console": {
                        "level": "INFO",
                        "formatter": "console",
                        "class": "logging.StreamHandler",
                        "stream": "ext://sys.stdout"
                    },
                    "file": {
                        "level": "DEBUG",
                        "formatter": "file",
                        "class": "logging.FileHandler",
                        "encoding": "utf-8",
                        "filename": log_path
                    }
            },
            "loggers": {
                "": {
                    "handlers": ["console", "file"],
                    "level": "INFO",
                    "propagate": True
                },
                "default": {
                    "handlers": ["console", "file"],
                    "level": "WARN",
                    "propagate": True
                }
            }
        }
    if name:
        logging_config['loggers'][name] = logging_config['loggers']['default']
    logging.config.dictConfig(logging_config)
    return logging.getLogger(name or 'default')

终于进入我的主程序

# main.py

_script_dir = abspath(dirname(__file__))
_logger = util.build_default_logger(logdir='temp', cfgfile=abspath(join(_script_dir, 'logging_stderr.json')))
_stderr_logger = util.build_default_logger(logdir='temp', name='myerrorlog', cfgfile=abspath(join(_script_dir, 'logging_stderr.json')))


...

_logger.info('my info')
_stderr_logger.warning('my warning')

我希望信息会通过 stdout 显示,并通过 stderr 发出警告。 但结果,只有警告,信息完全消失了。

如果只使用 _logger,那么一切都会通过标准输出。

我哪里错了? 难道dictconfig只支持一个streamhandler吗?

我使用过滤器解决了我自己的问题。

这个 post 帮助了我:

install filter on logging level in python using dictConfig

我基本上想将 INFO 发送到 stdout,WARNING-to-CRITICAL 到 stderr。这意味着具有为处理程序定义两端的范围。处理程序的 level 属性仅定义低端。

现在过滤来拯救。我最终使用了这个配置:

{
    "version": 1,
    "disable_existing_loggers": false,
    "filters": {
        "infofilter": {
          "()": "util.LowPassFilter",
          "level": 20
        },
        "warnfilter": {
          "()": "util.HighPassFilter",
          "level": 30
        }
    },
    "formatters": {
        "console": {
            "format": "%(asctime)s: %(levelname)s: %(pathname)s: \n%(message)s\n"
        },
        "file": {
            "format": "%(asctime)s: %(levelname)s: %(pathname)s: %(lineno)d: \n%(message)s\n"
        }
    },
    "handlers": {
            "console": {
                "level": "INFO",
                "formatter": "console",
                "class": "logging.StreamHandler",
                "stream": "ext://sys.stdout",
                "filters": ["infofilter"]
            },
            "console_err": {
                "level": "WARN",
                "formatter": "console",
                "class": "logging.StreamHandler",
                "stream": "ext://sys.stderr",
                "filters": ["warnfilter"]
            },
            "file": {
                "level": "DEBUG",
                "formatter": "file",
                "class": "logging.FileHandler",
                "encoding": "utf-8",
                "filename": "app.log"
            }
    },
    "loggers": {
        "": {
            "handlers": ["console", "console_err", "file"],
            "level": "INFO",
            "propagate": false
        },
        "default": {
            "handlers": ["console", "console_err", "file"],
            "level": "DEBUG",
            "propagate": true
        }
    }
}

和过滤器

class LowPassFilter(object):
    """
    Logging filter: Show log messages below input level.
    - CRITICAL = 50
    - FATAL = CRITICAL
    - ERROR = 40
    - WARNING = 30
    - WARN = WARNING
    - INFO = 20
    - DEBUG = 10
    - NOTSET = 0
    """
    def __init__(self, level):
        self.__level = level

    def filter(self, log):
        return log.levelno <= self.__level


class HighPassFilter(object):
    """Logging filter: Show log messages above input level."""
    def __init__(self, level):
        self.__level = level

    def filter(self, log):
        return log.levelno >= self.__level