在多个模块中使用日志记录
Posted
技术标签:
【中文标题】在多个模块中使用日志记录【英文标题】:Using logging in multiple modules 【发布时间】:2013-03-21 13:57:30 【问题描述】:我有一个具有以下结构的小型 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')
目前,我在开始记录消息之前在每个模块中执行此初始化。是否可以在一个地方只执行一次此初始化,以便通过在整个项目中记录来重用相同的设置?
【问题讨论】:
回应您对我的回答的评论:您不必在每个记录日志的模块中调用fileConfig
,除非您在所有模块中都有if __name__ == '__main__'
逻辑。如果包是一个库,prost 的回答不是一个好习惯,尽管它可能对你有用 - 除了添加 NullHandler
之外,不应配置库包中的日志记录。
prost 暗示我们需要在每个模块中调用 import 和 logger stmt,并且只在主模块中调用 fileconfig stmt。是不是和你说的很像?
prost 说你应该把日志配置代码放在package/__init__.py
中。这通常不是您放置if __name__ == '__main__'
代码的地方。此外,prost 的示例看起来会在导入时无条件地调用配置代码,这对我来说看起来不正确。通常,日志配置代码应该在一个地方完成,并且不应该作为导入的副作用发生,除非您正在导入 __main__。
使用内置函数怎么样,***.com/a/60232385/3404763?
【参考方案1】:
我总是这样做。
使用单个 python 文件将我的日志配置为名为“log_conf.py
”的单例模式
#-*-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')
在另一个模块中,只需导入配置即可。
from log_conf import Logger
Logger.logr.info("Hello World")
这是一种简单高效的单例模式。
【讨论】:
感谢您详细介绍单例模式。我正计划实施这个,但是@prost 解决方案要简单得多,完全适合我的需求。但是,我确实看到您的解决方案很有用,即具有多个入口点的大型项目(除了主要的)。 danke。 这没用。根记录器已经是一个单例。只需使用 logging.info 而不是 Logger.logr.info。 它没用吗?在复杂的项目中,当您有多个组件(模块集)并且您希望每个组件都有自己的记录器并且该组件的所有模块共享同一个记录器时,我认为这会有所帮助【参考方案2】:实际上每一个logger都是父包logger的子logger(即package.subpackage.module
继承package.subpackage)
的配置,所以你只需要配置根logger即可。这可以通过logging.config.fileConfig
来实现(你的记录器自己的配置)或logging.basicConfig
(设置根记录器)。在您的入口模块中设置日志记录(__main__.py
或您想要运行的任何内容,例如main_script.py
。__init__.py
也可以)
使用基本配置:
# package/__main__.py
import logging
import sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
使用文件配置:
# 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!")
欲了解更多信息,请参阅Advanced Logging Tutorial。
【讨论】:
这是迄今为止最简单的问题解决方案,更不用说它暴露和利用模块之间的父子关系,这是我作为菜鸟所不知道的。 danke。 实际上更相关的答案,因为问题与单独的模块有关。 也许是愚蠢的问题:如果__main__.py
中没有记录器(例如,如果我想在没有记录器的脚本中使用模块)logging.getLogger(__name__)
仍然会在模块还是会引发异常?
@Bill 我不确定我是否理解你的问题。你的意思是你没有logging.basicConfig或logging.config.fileConfig?您绝对可以使用 logging.getLogger 并进行一些日志记录,它不会在任何地方打印任何内容。许多库都进行日志记录,但它们将日志记录设置(例如日志消息的去向)留给了用户。
最后。我有一个工作记录器,但在 Windows 中使用 joblib 并行运行时失败了。我想这是对系统的手动调整——Parallel 有其他问题。但是,它肯定有效!谢谢【参考方案3】:
最佳做法是,在每个模块中,都有一个这样定义的记录器:
import logging
logger = logging.getLogger(__name__)
在模块顶部附近,然后在模块的其他代码中执行例如
logger.debug('My message with %s', 'variable data')
如果您需要在模块内细分日志记录活动,请使用例如
loggerA = logging.getLogger(__name__ + '.A')
loggerB = logging.getLogger(__name__ + '.B')
并酌情登录loggerA
和loggerB
。
在你的主程序中,例如:
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()
请参阅here 了解来自多个模块的日志记录,以及here 了解将被其他代码用作库模块的代码的记录配置。
更新:调用fileConfig()
时,如果您使用的是Python 2.6 或更高版本,您可能需要指定disable_existing_loggers=False
(有关详细信息,请参阅the docs)。默认值为True
以实现向后兼容性,这会导致所有现有记录器被fileConfig()
禁用,除非它们或其祖先在配置中明确命名。将值设置为 False
时,现有记录器将被单独保留。如果使用 Python 2.7/Python 3.2 或更高版本,您可能希望考虑使用比 fileConfig()
更好的 dictConfig()
API,因为它可以更好地控制配置。
【讨论】:
如果你看看我的例子,我已经在按照你上面的建议做。我的问题是我如何集中这个日志初始化,这样我就不必重复这 3 个语句。另外,在您的示例中,您错过了 'logging.config.fileConfig('logging.conf')' stmt。这个stmt实际上是我担心的根本原因。你看,如果我在每个模块中都启动了记录器,我将不得不在每个模块中输入这个 stmt。这意味着在每个模块中跟踪 conf 文件的路径,这对我来说似乎不是最佳实践(想象一下更改模块/包位置时的破坏)。 如果您在创建记录器后调用 fileConfig,无论是在同一个模块中还是在另一个模块中(例如,当您在文件顶部创建记录器时)都不起作用。日志记录配置仅适用于之后创建的记录器。所以这种方法对多个模块不起作用或不是一个可行的选择。 @Quest Monger:您始终可以创建另一个包含配置文件位置的文件..;) @Oxidator:不一定 - 查看disable_existing_loggers
标志,默认为True
,但可以设置为False
。
@Vinay Sajip,谢谢。您对在模块中工作但也在类之外工作的记录器有建议吗?由于导入是在调用 main 函数之前完成的,因此这些日志已经被记录下来。我想在主模块中的所有导入之前设置您的记录器是唯一的方法吗?如果您愿意,可以在 main 中覆盖此记录器。
如果我想让我的所有模块特定记录器的记录级别与默认警告不同,我是否必须在每个模块上进行该设置?比如说,我想让我的所有模块都记录在 INFO 中。【参考方案4】:
你也可以想出这样的东西!
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 可以成为整个应用程序的可插拔。 希望它有意义且有价值。
【讨论】:
日志模块已经处理了单例。 logging.getLogger("Hello") 将在所有模块中获取相同的记录器。【参考方案6】:引入另一种解决方案。
在我的模块的 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__)
当日志丢失时,您可以通过它们来自的模块来区分它们的来源。
【讨论】:
“我的模块的主初始化”是什么意思?并且“然后在每个班级我都需要一个记录器,我需要:”?您能否提供一个示例 called_module.py,以及将其用作从模块 caller_module.py 导入的示例? See this answer 获取我所询问的格式的灵感。不是试图高傲。我正在努力理解你的答案,我知道如果你这样写我会的。 @lucid_dreamer 我澄清了。 谢谢 - 这帮助我终于让它工作了。你怎么让它更像我想要的。我有一个主文件(称为 main.py),我在其中调用不同的模块。我希望这个 main.py 设置日志文件的名称。使用您的解决方案是不可能的。 我想通了。在 main.py 我使用 logger = get_module_logger('filename') 并且这是在导入任何模块之前完成的【参考方案7】:这些答案中的一些建议您在模块的顶部进行操作
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')
另外,如果您在 main 中使用 fileconfig,请设置 disable_existing_loggers=False,以防您的库设计者使用模块级记录器实例。
【讨论】:
你不能在import my_module
之前运行logging.config.fileConfig('logging.ini')
吗?正如建议in this answer.
不确定 - 但以这种方式混合导入和可执行代码肯定也被认为是不好的做法。您也不希望您的客户在导入之前检查他们是否需要配置日志记录,尤其是当有一个简单的替代方案时!想象一下,如果像 requests 这样广泛使用的库已经做到了……!
“不确定 - 但以这种方式混合导入和可执行代码肯定也被认为是不好的做法。” - 为什么?
在最后一个“良好实践”示例中,您实际上并不需要第 4 行 (logger = logging.getLogger(__name__)
)。
您似乎与official docs 相矛盾:'命名记录器时使用的一个好的约定是在每个使用日志记录的模块中使用模块级记录器,命名如下:logger = logging.getLogger(__name__)
' 【参考方案8】:
有几个答案。我最终得到了一个对我有意义的类似但不同的解决方案,也许它对你也有意义。 我的主要目标是能够按级别将日志传递给处理程序(调试级别日志到控制台,警告及以上到文件):
from flask import Flask
import logging
from logging.handlers import RotatingFileHandler
app = Flask(__name__)
# make default logger output everything to the console
logging.basicConfig(level=logging.DEBUG)
rotating_file_handler = RotatingFileHandler(filename="logs.log")
rotating_file_handler.setLevel(logging.INFO)
app.logger.addHandler(rotating_file_handler)
创建了一个不错的 util 文件,名为 logger.py:
import logging
def get_logger(name):
return logging.getLogger("flask.app." + name)
flask.app 是烧瓶中的硬编码值。应用程序记录器始终以 flask.app 作为其模块名称。
现在,在每个模块中,我都可以在以下模式下使用它:
from logger import get_logger
logger = get_logger(__name__)
logger.info("new log")
这将以最少的努力为“app.flask.MODULE_NAME”创建一个新日志。
【讨论】:
【参考方案9】:最好的做法是单独创建一个模块,它只有一个方法,我们的任务是为调用方法提供一个记录器处理程序。将此文件另存为 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')
【讨论】:
如果您没有任何附加参数,这很好。但是,假设您在应用程序中有--debug
选项,并希望根据此参数在应用程序中的 所有 记录器中设置日志记录级别...
@TheGodfather 是的,这很难通过这种方法实现。在这种情况下,我们可以做的是创建一个类,该类在创建对象时将格式化程序作为参数,并具有类似的功能来返回记录器处理程序。您对此有何看法?
是的,我做了类似的事情,让get_logger(level=logging.INFO)
返回某种单例,所以当它第一次从主应用程序调用时,它会用适当的级别初始化记录器和处理程序,然后返回对所有其他方法使用相同的 logger
对象。【参考方案10】:
python 新手,所以我不知道这是否可取,但它非常适合不重写样板。
您的项目必须有一个 init.py 以便它可以作为模块加载
# Put this in your module's __init__.py
import logging.config
import sys
# I used this dictionary test, you would put:
# logging.config.fileConfig('logging.conf')
# The "" entry in loggers is the root logger, tutorials always
# use "root" but I can't get that to work
logging.config.dictConfig(
"version": 1,
"formatters":
"default":
"format": "%(asctime)s %(levelname)s %(name)s %(message)s"
,
,
"handlers":
"console":
"level": 'DEBUG',
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout"
,
"loggers":
"":
"level": "DEBUG",
"handlers": ["console"]
)
def logger():
# Get the name from the caller of this function
return logging.getLogger(sys._getframe(1).f_globals['__name__'])
sys._getframe(1)
建议来自here
然后在任何其他文件中使用您的记录器:
from [your module name here] import logger
logger().debug("FOOOOOOOOO!!!")
注意事项:
-
您必须将文件作为模块运行,否则
import [your module]
将不起作用:
python -m [your module name].[your filename without .py]
程序入口点的记录器名称为__main__
,但任何使用__name__
的解决方案都会出现此问题。
【讨论】:
【参考方案11】:对我来说,在多个模块中使用一个日志库实例的简单方法是以下解决方案:
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")
【讨论】:
对于我的小项目,这是解决方案。请注意,根记录器是一个单例,很方便,它增加了这个简单解决方案的纹理。 这是一个被低估的答案。如果您的项目只需要一个记录器,则无需使用getLogger(__name__)
创建多个记录器。有了这个答案,您只需要一行来导入/配置记录器。我也更喜欢在代码中使用basicConfig
而不是fileConfig(logging.conf)
,因为您可以进行动态配置。另一个变体是您可以删除 logger = logging
别名并直接使用 logging.info()
。或者您创建一个较短的别名,例如 log=logging
以使用 log.info()
。
谢谢 - 简单而美妙。您能否以某种方式编写此代码以启用日志文件的命名 - 由主函数驱动?【参考方案12】:
我想添加我的解决方案(它基于 logging cookbook 和该线程中的其他文章和建议。但是我花了很长时间才弄清楚为什么它没有立即按我的预期工作。所以我创建了一个小测试项目来了解日志记录的工作原理。
既然我想通了,我想分享我的解决方案,也许它可以对某人有所帮助。
我知道我的一些代码可能不是最佳实践,但我仍在学习。当我使用它们时,我将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.py
和 mod2.py
此时我不想使用basicConfig()
或FileConfig()
,而是像日志记录手册 中那样使用它
所以基本上,这意味着,我需要在 daemon.py
(总是)和模块 mod1.py
和 mod2.py
(仅在直接调用它们时)初始化 root 记录器。
为了使几个模块中的这个初始化更容易,我创建了my_logger.py
,它确实如此,这在食谱中有所描述。
我的错误
之前,我在那个模块中的错误是使用logger = logging.getLogger(__name__)
(模块记录器)而不是使用logger = logging.getLogger()
(获取root记录器)来初始化记录器。
第一个问题是,当从daemon.py
调用时,记录器的命名空间被设置为my_log_test.common.my_logger
。 mod1.py
中具有“不匹配”命名空间 my_log_test.pkg1.mod1
的模块记录器因此无法附加到另一个记录器,我将看不到 mod1 的日志输出。
第二个“问题”是,我的主程序在daemon.py
而不是__main__.py
。但毕竟对我来说不是一个真正的问题,但它增加了命名空间的混乱。
工作解决方案
这来自食谱,但在一个单独的模块中。我还添加了一个可以从守护进程调用的 logger_cleanup
函数,以删除超过 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"pathlogfilename", 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()
如果有帮助,我很高兴。也很高兴收到反馈!
【讨论】:
谢谢你提到的,当我使用根记录器时它工作了以上是关于在多个模块中使用日志记录的主要内容,如果未能解决你的问题,请参考以下文章