我想分享我的解决方案(它基于日志菜谱和该主题的其他文章和建议)。然而,我花了很长时间才弄清楚为什么它没有立即按照我的期望工作。所以我创建了一个小测试项目来学习日志记录是如何工作的。
自从我搞清楚了之后,我想分享我的解决方案,也许它可以帮助某些人。
我知道我的一些代码可能不是最佳实践,但我还在学习。我把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()
,而是像logging cookbook中那样保持不变。
因此,这意味着我需要在daemon.py
(始终)以及在模块mod1.py
和mod2.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天之前的日志。
from datetime import datetime
import time
import os
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"
logger = logging.getLogger()
logger.setLevel(logging.INFO)
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]: %(name)s: %(message)s")
file.setLevel(logging.INFO)
file.setFormatter(fileformat)
stream = logging.StreamHandler()
streamformat = logging.Formatter("%(asctime)s [%(levelname)s]: %(name)s: %(message)s")
stream.setLevel(logging.INFO)
stream.setFormatter(streamformat)
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
from my_log_test import daemon
if __name__ == '__main__':
print("print in __main__.py")
daemon.run()
要直接运行deamon.py,请使用python3 -m my_log_test.daemon
from datetime import datetime
import time
import logging
import my_log_test.pkg1.mod1 as mod1
import my_log_test.pkg2.mod2 as mod2
from my_log_test.common.my_logger import logger_init
logger_init()
logger = logging.getLogger(__name__)
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
import logging
mod1_logger = logging.getLogger("my_log_test.daemon.pkg1.mod1")
def do1():
print("print in mod1.do1()")
print("print mod1.py __name__: " +__name__)
mod1_logger.info("Doing someting in pkg1.do1()")
if __name__ == '__main__':
from my_log_test.common.my_logger import logger_init
logger_init()
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
import logging
logger = logging.getLogger(__name__)
def do2():
print("print in pkg2.do2()")
print("print mod2.py __name__: " +__name__)
logger.info("Doing someting in pkg2.do2()")
if __name__ == '__main__':
from my_log_test.common.my_logger import logger_init
logger_init()
print("print in mod2.py if __name__ == '__main__'")
logger.info("Running mod2.py as main")
do2()
如果有所帮助,我很开心。也非常欢迎收到反馈!
fileConfig
,除非所有模块中都有if __name__ == '__main__'
的逻辑。prost的回答对于库而言并不是最佳实践,尽管它可能适用于你——在库包中不应该配置日志记录,除非添加一个NullHandler
。 - Vinay Sajippackage/__init__.py
文件中。这通常不是你放置if __name__ == '__main__'
代码的位置。此外,Prost 的示例似乎会在导入时无条件地调用配置代码,这对我来说看起来不太对。通常,日志配置代码应该在一个地方完成,并且不应该作为导入的副作用发生,除非你正在导入__main__
。 - Vinay Sajip