在多个模块中使用日志记录

414

我有一个小的Python项目,它具有以下结构 -

Project 
 -- pkg01
   -- test01.py
 -- pkg02
   -- test02.py
 -- logging.conf

我计划使用默认的日志记录模块将消息打印到标准输出和日志文件中。 要使用日志记录模块,需要进行一些初始化 -

import logging.config

logging.config.fileConfig('logging.conf')
logger = logging.getLogger('pyApp')

logger.info('testing')

目前,我在每个模块开始记录消息之前都执行此初始化。是否可能只在一个地方执行此初始化,以便所有项目中的日志记录都重用相同的设置?


5
关于你对我回答的评论:你不必在每个进行日志记录的模块中都调用 fileConfig,除非所有模块中都有 if __name__ == '__main__' 的逻辑。prost的回答对于库而言并不是最佳实践,尽管它可能适用于你——在库包中不应该配置日志记录,除非添加一个 NullHandler - Vinay Sajip
1
prost 暗示我们需要在每个模块中调用 import 和 logger 语句,并且只在主模块中调用 fileconfig 语句。这不是跟你说的很像吗? - Quest Monger
8
Prost 表示你应该将日志配置代码放在 package/__init__.py 文件中。这通常不是你放置 if __name__ == '__main__' 代码的位置。此外,Prost 的示例似乎会在导入时无条件地调用配置代码,这对我来说看起来不太对。通常,日志配置代码应该在一个地方完成,并且不应该作为导入的副作用发生,除非你正在导入 __main__ - Vinay Sajip
使用内置函数怎么样?https://stackoverflow.com/a/60232385/3404763? - fatih_dur
12个回答

452

最佳实践是,在每个模块中定义一个类似这样的记录器:

import logging
logger = logging.getLogger(__name__)

在模块的顶部附近,然后在模块中的其他代码中执行例如。

logger.debug('My message with %s', 'variable data')

如果您需要在模块内细分日志记录活动,请使用例如:

loggerA = logging.getLogger(__name__ + '.A')
loggerB = logging.getLogger(__name__ + '.B')

适当时记录到loggerAloggerB

在您的主程序中,示例如下:

def main():
    "your program code"

if __name__ == '__main__':
    import logging.config
    logging.config.fileConfig('/path/to/logging.conf')
    main()
或者
def main():
    import logging.config
    logging.config.fileConfig('/path/to/logging.conf')
    # your program code

if __name__ == '__main__':
    main()

请参考这里进行多模块的日志记录,以及这里进行代码库模块的日志配置。

更新:在调用fileConfig()时,如果您使用的是Python 2.6或更高版本,请指定disable_existing_loggers=False(详见文档)。默认值为True以保持向后兼容性,这会导致fileConfig()禁用除显式命名配置中列出的所有现有记录器及其祖先之外的所有现有记录器。将该值设置为False后,现有记录器将保持不变。如果使用Python 2.7 / Python 3.2或更高版本,则可以考虑使用比fileConfig()更好的dictConfig() API,因为它可以更好地控制配置。


49
如果你看一下我的例子,我已经在做你上面建议的事情。我的问题是如何将这个日志初始化集中处理,以便我不必重复那三个语句。 此外,在你的例子中,你漏掉了'logging.config.fileConfig('logging.conf')'这条语句。这条语句实际上是我担心的根本原因。你知道,如果我在每个模块中初始化记录器,我就必须在每个模块中输入这个语句。这意味着需要在每个模块中跟踪conf文件的路径,这对我来说并不是最佳实践(想象一下当更改模块/包位置时会发生什么混乱)。 - Quest Monger
9
如果你在创建日志记录器之后调用fileConfig函数,无论是在同一模块中还是在另一个模块中(例如,在文件顶部创建记录器时),都不起作用。日志配置仅适用于创建之后的记录器。因此,这种方法不起作用或不适用于多个模块。您可以始终创建另一个文件来保存配置文件的位置... ;) - Vincent Ketelaars
2
@Oxidator:不一定-请参见disable_existing_loggers标志,默认情况下为True,但可以设置为False - Vinay Sajip
2
@Vinay Sajip,谢谢您。您有没有推荐一些可以在模块内和类外工作的记录器?由于导入是在调用主函数之前完成的,因此这些日志已经被记录了。我猜在主模块中在所有导入之前设置记录器可能是唯一的方法?如果您愿意,这个记录器可以在主函数中被覆盖。 - Vincent Ketelaars
1
如果我想让所有特定于模块的记录器具有与默认警告不同的日志记录级别,那么我是否需要在每个模块上进行设置?比如说,我想让所有模块以INFO级别进行记录。 - Raj
显示剩余10条评论

271

实际上,每个记录器都是父级包记录器的子记录器 (即 package.subpackage.module 继承自 package.subpackage),因此您只需配置根记录器即可。这可以通过 logging.config.fileConfig(为记录器设置自己的配置)或 logging.basicConfig(设置根记录器)来实现。在入口模块 (__main__.py 或您想要运行的任何其他脚本,例如 main_script.py。也可以使用 __init__.py) 中设置日志。

使用 basicConfig:

# package/__main__.py
import logging
import sys

logging.basicConfig(stream=sys.stdout, level=logging.INFO)

使用 fileConfig:

# package/__main__.py
import logging
import logging.config

logging.config.fileConfig('logging.conf')

然后使用以下方式创建每个记录器:

# package/submodule.py
# or
# package/subpackage/submodule.py
import logging
log = logging.getLogger(__name__)

log.info("Hello logging!")

更多信息请参见高级日志记录教程


44
这绝对是解决问题最简单的方法,更不用说它暴露和利用了模块之间的父子关系,这是我作为新手所不知道的。谢谢。 - Quest Monger
3
由于问题涉及到独立模块,实际上更相关的答案应该是更加清晰明确的。 - Jan Sila
2
也许是个愚蠢的问题:如果__main__.py中没有日志记录器(例如,我想在没有记录器的脚本中使用该模块),logging.getLogger(__name__)是否仍会对该模块进行某种形式的记录,还是会引发异常? - Bill
2
@Bill,我不确定我是否理解了你的问题。你是指你没有logging.basicConfig或logging.config.fileConfig吗?你肯定可以使用logging.getLogger并进行一些日志记录,只是它不会在任何地方打印任何东西。许多库都会记录日志,但它们会将日志设置(例如日志消息的位置)留给其用户。 - Stan Prokop
2
最终,我有了一个可用的记录器,但在Windows上使用joblib进行并行运行时失败了。我猜这是对系统的手动调整——Parallel还有其他问题。但是,它肯定有效!谢谢。 - B Furtado
显示剩余11条评论

63

对我来说,使用一份日志库实例在多个模块中的简单方法如下:

base_logger.py

import logging

logger = logging
logger.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)

其他文件

from base_logger import logger

if __name__ == '__main__':
    logger.info("This is an info message")

6
对于我的小项目来说,这是最佳解决方案。值得注意的是,根记录器是一个单例对象,这很方便,也增加了这个简单解决方案的实用性。 - Rafs
6
这个回答被低估了。如果你的项目只需要一个日志记录器,那么就没有必要用 getLogger(__name__) 创建多个日志记录器了。使用这个回答,你只需要一行来导入/配置日志记录器。我还更喜欢在代码中使用 basicConfig 而不是 fileConfig(logging.conf),因为你可以进行动态配置。另一种变化是,你可以删除 logger = logging 的别名,直接使用 logging.info()。或者你可以创建一个更短的别名比如 log=logging,来使用 log.info() - wisbucky
2
谢谢-简单而美妙。你能否让这个代码通过主函数来实现对日志文件进行命名的功能? - MattiH

16

我通常按以下方式执行。

使用一个名为 'log_conf.py' 的 Python 文件来配置我的日志为单例模式。

#-*-coding:utf-8-*-

import logging.config

def singleton(cls):
    instances = {}
    def get_instance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return get_instance()

@singleton
class Logger():
    def __init__(self):
        logging.config.fileConfig('logging.conf')
        self.logr = logging.getLogger('root')

在另一个模块中,只需导入 config。

from log_conf import Logger

Logger.logr.info("Hello World")

这是一个单例模式用于记录日志,简单高效。


1
感谢您详细介绍单例模式。我原本计划实现它,但是@prost的解决方案更简单,完全符合我的需求。不过,我认为您的解决方案对于具有多个入口点(而非主要入口点)的大型项目非常有用。谢谢。 - Quest Monger
85
这没用。根记录器已经是单例模式了。直接使用logging.info而不是Logger.logr.info。 - Pod
虽然这样做有用吗?在复杂的项目中,当您拥有多个组件(模块集)并且您希望每个组件都有自己的记录器,并且该组件的所有模块共享相同的记录器时,我认为这将会有所帮助。 - Hamed

14

提供另一种解决方案。

在我的模块的init.py文件中,我有类似下面的代码:

# mymodule/__init__.py
import logging

def get_module_logger(mod_name):
  logger = logging.getLogger(mod_name)
  handler = logging.StreamHandler()
  formatter = logging.Formatter(
        '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
  handler.setFormatter(formatter)
  logger.addHandler(handler)
  logger.setLevel(logging.DEBUG)
  return logger

然后在每个模块中,我需要一个记录器,我这样做:

# mymodule/foo.py
from [modname] import get_module_logger
logger = get_module_logger(__name__)

当日志丢失时,您可以通过它们来自的模块区分它们的来源。


“my module's main init” 是什么意思?还有,“然后在每个类中我需要一个记录器,我这样做:”?你能提供一个名为 called_module.py 的示例以及从模块 caller_module.py 中导入它的用法示例吗?可以参考此答案(https://dev59.com/lWUo5IYBdhLWcg3w2CWJ#36083311)来了解我所要求的格式。我并不是想表现高人一等,我只是想理解你的答案,如果你用这种方式写的话,我就能明白了。 - lucid_dreamer
1
@lucid_dreamer 我澄清了。 - Tommy
谢谢 - 这帮了我最终让它工作了。你怎么让它更像我想要的呢?我有一个主文件(称之为main.py),在其中调用不同的模块。我希望这个main.py设置日志文件的名称。使用您的解决方案是不可能的。 - MattiH
我已经想通了。在 main.py 中,我使用 logger = get_module_logger('filename'),并且这是在导入任何模块之前完成的。 - MattiH

13
几个答案建议在模块顶部执行以下操作:

import logging
logger = logging.getLogger(__name__)

我理解这被认为是非常糟糕的做法。原因是文件配置将默认禁用所有现有的记录器。例如:
#my_module
import logging

logger = logging.getLogger(__name__)

def foo():
    logger.info('Hi, foo')

class Bar(object):
    def bar(self):
        logger.info('Hi, bar')

在您的主模块中:

#main
import logging

# load my module - this now configures the logger
import my_module

# This will now disable the logger in my module by default, [see the docs][1] 
logging.config.fileConfig('logging.ini')

my_module.foo()
bar = my_module.Bar()
bar.bar()

现在,在logging.ini中指定的日志将为空,因为fileconfig调用禁用了现有的记录器。
虽然可以通过disable_existing_Loggers=False来解决这个问题,但实际上,你库的许多客户可能不知道这种行为,并且无法接收你的日志。通过始终在本地调用logging.getLogger来为客户提供便利。感谢:我从Victor Lin's Website中了解到了这种行为。
因此,良好的做法是始终在本地调用logging.getLogger。例如:
#my_module
import logging

logger = logging.getLogger(__name__)

def foo():
    logging.getLogger(__name__).info('Hi, foo')

class Bar(object):
    def bar(self):
        logging.getLogger(__name__).info('Hi, bar')    

此外,如果您在主要程序中使用fileconfig,请将disable_existing_loggers设置为False,以防库设计者使用模块级别的记录器实例。

1
你能不能在 import my_module 之前不运行 logging.config.fileConfig('logging.ini')?正如这个答案所建议的。 - lucid_dreamer
1
不确定,但将导入和可执行代码混合使用肯定也被认为是不良实践。您也不希望客户在导入之前检查是否需要配置日志记录,特别是当有一个微不足道的替代方案时!想象一下,如果像requests这样广泛使用的库也这样做了....! - phil_20686
1
我肯定会称你的“好习惯”为坏习惯。你试图从用户那里夺取控制权,但在Python中,我们期望用户是一个合理的人。如果我现在进行配置并使用disable_existing_loggers=True,我会非常惊讶地发现你忽略了这一点并仍然记录日志。 - DerWeh
7
您似乎与官方文档相矛盾:'在每个使用日志记录的模块中,使用模块级别的记录器,并按以下方式命名:logger = logging.getLogger(__name__)'。 - iron9
1
假设我们在my_module中有很多日志记录语句,每次都写logging.getLogger(__name__).info("some message")非常繁琐。我认为这不是一个好的实践 - jdhao
显示剩余9条评论

13

我想分享我的解决方案(它基于日志菜谱和该主题的其他文章和建议)。然而,我花了很长时间才弄清楚为什么它没有立即按照我的期望工作。所以我创建了一个小测试项目来学习日志记录是如何工作的。

自从我搞清楚了之后,我想分享我的解决方案,也许它可以帮助某些人。

我知道我的一些代码可能不是最佳实践,但我还在学习。我把print()函数留在了里面,因为在日志记录未按预期工作时,我使用了它们。这些已在我的其他应用程序中删除。同时,我欢迎对代码或结构的任何部分提出反馈。

my_log_test 项目结构(从我正在工作的另一个项目中克隆/简化)

my_log_test
├── __init__.py
├── __main__.py
├── daemon.py
├── common
│   ├── my_logger.py
├── pkg1
│   ├── __init__.py
│   └── mod1.py
└── pkg2
    ├── __init__.py
    └── mod2.py

需求

以下是我使用的组合中特别之处或我尚未明确提及的一些事项:

  • 主模块为daemon.py,由__main__.py调用。
  • 在开发/测试期间,希望能够单独调用模块mod1.pymod2.py
  • 目前不想使用basicConfig()FileConfig(),而是像logging cookbook中那样保持不变。

因此,这意味着我需要在daemon.py(始终)以及在模块mod1.pymod2.py(仅在直接调用它们时)中初始化root日志记录器。

为了使多个模块中的初始化更加容易,我创建了my_logger.py,其中描述了logging cookbook中所述的内容。

我的错误

在该模块之前,我的错误在于使用logging.getLogger(__name__)(模块记录器)来初始化记录器,而不是使用logging.getLogger()(获取root记录器)。

第一个问题是,当从daemon.py调用时,记录器的命名空间设置为my_log_test.common.my_logger。因此,在mod1.py中具有“不匹配”命名空间my_log_test.pkg1.mod1的模块记录器无法连接到其他记录器,因此我将看不到来自mod1的日志输出。

第二个“问题”是,我的主程序在daemon.py中而不是在__main__.py中。但终究对我来说不是真正的问题,只是增加了命名空间的混乱程度。

可行解决方案

这是logging cookbook中的内容,但在一个单独的模块中。我还添加了一个logger_cleanup函数,我可以从daemon中调用该函数以删除x天之前的日志。

## my_logger.py
from datetime import datetime
import time
import os

## Init logging start 
import logging
import logging.handlers

def logger_init():
    print("print in my_logger.logger_init()")
    print("print my_logger.py __name__: " +__name__)
    path = "log/"
    filename = "my_log_test.log"

    ## get logger
    #logger = logging.getLogger(__name__) ## this was my mistake, to init a module logger here
    logger = logging.getLogger() ## root logger
    logger.setLevel(logging.INFO)

    # File handler
    logfilename = datetime.now().strftime("%Y%m%d_%H%M%S") + f"_{filename}"
    file = logging.handlers.TimedRotatingFileHandler(f"{path}{logfilename}", when="midnight", interval=1)
    #fileformat = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
    fileformat = logging.Formatter("%(asctime)s [%(levelname)s]: %(name)s: %(message)s")
    file.setLevel(logging.INFO)
    file.setFormatter(fileformat)

    # Stream handler
    stream = logging.StreamHandler()
    #streamformat = logging.Formatter("%(asctime)s [%(levelname)s:%(module)s] %(message)s")
    streamformat = logging.Formatter("%(asctime)s [%(levelname)s]: %(name)s: %(message)s")
    stream.setLevel(logging.INFO)
    stream.setFormatter(streamformat)

    # Adding all handlers to the logs
    logger.addHandler(file)
    logger.addHandler(stream)


def logger_cleanup(path, days_to_keep):
    lclogger = logging.getLogger(__name__)
    logpath = f"{path}"
    now = time.time()
    for filename in os.listdir(logpath):
        filestamp = os.stat(os.path.join(logpath, filename)).st_mtime
        filecompare = now - days_to_keep * 86400
        if  filestamp < filecompare:
            lclogger.info("Delete old log " + filename)
            try:
                os.remove(os.path.join(logpath, filename))
            except Exception as e:
                lclogger.exception(e)
                continue

要运行 deamon.py(通过__main__.py),请使用python3 -m my_log_test

## __main__.py
from  my_log_test import daemon

if __name__ == '__main__':
    print("print in __main__.py")
    daemon.run()

要直接运行deamon.py,请使用python3 -m my_log_test.daemon

## daemon.py
from datetime import datetime
import time
import logging
import my_log_test.pkg1.mod1 as mod1
import my_log_test.pkg2.mod2 as mod2

## init ROOT logger from my_logger.logger_init()
from my_log_test.common.my_logger import logger_init
logger_init() ## init root logger
logger = logging.getLogger(__name__) ## module logger

def run():
    print("print in daemon.run()")
    print("print daemon.py __name__: " +__name__)
    logger.info("Start daemon")
    loop_count = 1
    while True:
        logger.info(f"loop_count: {loop_count}")
        logger.info("do stuff from pkg1")
        mod1.do1()
        logger.info("finished stuff from pkg1")

        logger.info("do stuff from pkg2")
        mod2.do2()
        logger.info("finished stuff from pkg2")

        logger.info("Waiting a bit...")
        time.sleep(30)


if __name__ == '__main__':
    try:
        print("print in daemon.py if __name__ == '__main__'")
        logger.info("running daemon.py as main")
        run()
    except KeyboardInterrupt as e:
        logger.info("Program aborted by user")
    except Exception as e:
        logger.info(e)

要直接运行mod1.py,请使用python3 -m my_log_test.pkg1.mod1

## mod1.py
import logging
# mod1_logger = logging.getLogger(__name__)
mod1_logger = logging.getLogger("my_log_test.daemon.pkg1.mod1") ## for testing, namespace set manually

def do1():
    print("print in mod1.do1()")
    print("print mod1.py __name__: " +__name__)
    mod1_logger.info("Doing someting in pkg1.do1()")

if __name__ == '__main__':
    ## Also enable this pkg to be run directly while in development with
    ## python3 -m my_log_test.pkg1.mod1

    ## init root logger
    from my_log_test.common.my_logger import logger_init
    logger_init() ## init root logger

    print("print in mod1.py if __name__ == '__main__'")
    mod1_logger.info("Running mod1.py as main")
    do1()

运行mod2.py(直接)使用python3 -m my_log_test.pkg2.mod2

## mod2.py
import logging
logger = logging.getLogger(__name__)

def do2():
    print("print in pkg2.do2()")
    print("print mod2.py __name__: " +__name__) # setting namespace through __name__
    logger.info("Doing someting in pkg2.do2()")

if __name__ == '__main__':
    ## Also enable this pkg to be run directly while in development with
    ## python3 -m my_log_test.pkg2.mod2

    ## init root logger
    from my_log_test.common.my_logger import logger_init
    logger_init() ## init root logger

    print("print in mod2.py if __name__ == '__main__'")
    logger.info("Running mod2.py as main")
    do2()

如果有所帮助,我很开心。也非常欢迎收到反馈!


1
谢谢你提到的,当我使用根记录器时它起作用了。 - Mohamad Al Mdfaa

5
你可以像这样想出一些东西!
def get_logger(name=None):
    default = "__app__"
    formatter = logging.Formatter('%(levelname)s: %(asctime)s %(funcName)s(%(lineno)d) -- %(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S')
    log_map = {"__app__": "app.log", "__basic_log__": "file1.log", "__advance_log__": "file2.log"}
    if name:
        logger = logging.getLogger(name)
    else:
        logger = logging.getLogger(default)
    fh = logging.FileHandler(log_map[name])
    fh.setFormatter(formatter)
    logger.addHandler(fh)
    logger.setLevel(logging.DEBUG)
    return logger

现在,如果将上述内容定义在一个单独的模块中并在需要记录日志的其他模块中导入,则可以在同一模块和整个项目中使用多个记录器。
a=get_logger("__app___")
b=get_logger("__basic_log__")
a.info("Starting logging!")
b.debug("Debug Mode")

5

@Yarkee的解决方案看起来更好。我想再补充一些内容——

class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances.keys():
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]


class LoggerManager(object):
    __metaclass__ = Singleton

    _loggers = {}

    def __init__(self, *args, **kwargs):
        pass

    @staticmethod
    def getLogger(name=None):
        if not name:
            logging.basicConfig()
            return logging.getLogger()
        elif name not in LoggerManager._loggers.keys():
            logging.basicConfig()
            LoggerManager._loggers[name] = logging.getLogger(str(name))
        return LoggerManager._loggers[name]    


log=LoggerManager().getLogger("Hello")
log.setLevel(level=logging.DEBUG)

所以LoggerManager可以作为整个应用程序的可插拔模块。 希望这有意义并具有价值。

12
日志模块已经处理了单例模式。logging.getLogger("Hello") 将在所有模块中获得相同的记录器。 - Pod

2
最佳实践是单独创建一个模块,该模块只有一个方法,其任务是为调用方法提供记录器处理程序。将此文件保存为m_logger.py。
import logger, logging

def getlogger():
    # logger
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    # create console handler and set level to debug
    #ch = logging.StreamHandler()
    ch = logging.FileHandler(r'log.txt')
    ch.setLevel(logging.DEBUG)
    # create formatter
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    # add formatter to ch
    ch.setFormatter(formatter)
    # add ch to logger
    logger.addHandler(ch)
    return logger

现在每当需要日志处理程序时,请调用getlogger()方法。最初的回答。
from m_logger import getlogger
logger = getlogger()
logger.info('My mssg')

1
如果你没有任何额外的参数,这是很好的。但是如果,例如,应用程序中有 --debug 选项,并且希望基于此参数设置应用程序中 所有 记录器的记录级别... - The Godfather
@TheGodfather 是的,用这种方法实现很困难。在这种情况下,我们可以创建一个类,在对象创建时将格式化程序作为参数传递,并具有类似的函数来返回记录器处理程序。您对此有何看法? - Mousam Singh
是的,我做了类似的事情,创建了 get_logger(level=logging.INFO) 来返回某种单例,因此当它第一次从主应用程序调用时,它会使用适当的级别初始化记录器和处理程序,然后将相同的 logger 对象返回给所有其他方法。 - The Godfather

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接