我在python应用程序中有一个日志设置,它登录到文件和MongoDB。设置如下:
[logger_root]
handlers=myHandler,mongoHandler
level=DEBUG
qualname=myapp
[handler_myHandler]
class=handlers.RotatingFileHandler
level=DEBUG
formatter=myFormatter
args=('myapp.log', 'a',20000000,10)
[handler_mongoHandler]
class=myapp.MongoLogger.MongoLogger
level=INFO
args=('log',)
[formatter_myFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
MongoLogger的emit()函数如下:
def emit(self, record):
logdata = record.__dict__
try:
if(self.data == None):
self.initDb()
self.logtable.insert(logdata)
except:
self.handleError(record)
日志记录就这样完成了:
logger.info("Processing account %s..." % account)
它工作得相当好,但现在我还有一个额外的要求。我希望它有一些上下文 - 即,能够定义自定义值 - 比如帐户名称 - 因此在帐户处理中完成的每个日志都会将帐户名称作为传递给{{1的record
的一部分上面也可用于emit
字符串中的格式化程序。
是否可以使用日志记录模块执行此操作?有没有其他更好的方法可以做同样的事情?
答案 0 :(得分:11)
您可以在获得帐户名称后在帐户处理代码中定义一个功能,如下所示:
# account_name should already be defined
log = lambda msg: logger.info(msg, extra={'account': account_name})
###
log('Processing account...')
请注意extra
关键字参数。它用于向日志记录添加其他上下文 - 在本例中为帐户名称。
您可以使用格式化程序中通过extra
传递的上下文:
format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s - %(account)s'
请注意,如果您设置格式化程序并忘记传递account
,则会收到字符串格式异常。
答案 1 :(得分:4)
Tony的答案只是提供一个函数,但是使用LoggerAdapter
我们可以得到一个上下文记录器。
class ILoggerAdapter(LoggerAdapter):
def __init__(self, logger, extra):
super(ILoggerAdapter, self).__init__(logger, extra)
self.env = extra
def process(self, msg, kwargs):
msg, kwargs = super(ILoggerAdapter, self).process(msg, kwargs)
result = copy.deepcopy(kwargs)
default_kwargs_key = ['exc_info', 'stack_info', 'extra']
custome_key = [k for k in result.keys() if k not in default_kwargs_key]
result['extra'].update({k: result.pop(k) for k in custome_key})
return msg, result
然后将您的记录器包装为
new_logger = ILoggerAdapter(old_logger, extra={'name': 'name'})
# new_logger is just same as old_logger
# but can be pass to record
new_logger.info(msg='haha', id=100)
def emit(self, record):
print(record.name)
print(record.id)
答案 2 :(得分:1)
Python官方文档(logging coockbook)建议了两种将上下文信息添加到日志的方法:
filter
方法返回bool,索引是否发出记录)。但是,它还允许您处理记录-并根据需要添加属性。例如,您可以基于全局threading.local变量来设置属性。下面是一个Filter
示例,它从全局threading.local变量附加属性:
log_utils.py
import logging
import threading
log_context_data = threading.local()
class ThreadingLocalContextFilter(logging.Filter):
"""
This is a filter which injects contextual information from `threading.local` (log_context_data) into the log.
"""
def __init__(self, attributes: List[str]):
super().__init__()
self.attributes = attributes
def filter(self, record):
for a in self.attributes:
setattr(record, a, getattr(log_context_data, a, 'default_value'))
return True
log_context_data
可以在开始处理帐户时设置,完成后重置。但是,我建议使用上下文管理器进行设置:
也在 log_utils.py 中:
class SessionContext(object):
def __init__(self, logger, context: dict = None):
self.logger = logger
self.context: dict = context
def __enter__(self):
for key, val in self.context.items():
setattr(log_context_data, key, val)
return self
def __exit__(self, et, ev, tb):
for key in self.context.keys():
delattr(log_context_data, key)
还有一个用法示例 my_script.py :
root_logger = logging.getLogger()
handler = ...
handler.setFormatter(
logging.Formatter('{name}: {levelname} {account} - {message}', style='{'))
handler.addFilter(ThreadingLocalContextFilter(['account']))
root_logger.addHandler(handler)
...
...
using SessionContext(logger=root_logger, context={'account': account}):
...
...
注意:
Filter
仅适用于与其连接的logger
。因此,如果我们将其附加到logging.getLogger('foo')
,则不会影响logging.getLogger('foo.bar')
。解决方案是将Filter
附加到Handler
而不是logger
上。ThreadingLocalContextFilter
不包含必需的属性,则log_context_data
可以拒绝记录。这取决于您的需求。何时使用什么?
答案 3 :(得分:1)
原谅我的自我推销,不过我写了一个名为log-with-context
的Python包来解决这个问题。您可以在 PyPI 和 GitHub 上找到它。以下是如何使用 log-with-context
使用任意数量的线程本地上下文值记录消息。
log-with-context
和日志格式化程序。您必须将 log-with-context
与显示 extra
字段的不同记录器配对。我最喜欢的日志格式化程序是 JSON-log-formatter。
您可以使用 pip 安装这两个软件包:
pip3 install log-with-context JSON-log-formatter
这里是如何配置 JSON-log-formatter
以打印 INFO
的 JSON 日志,更糟糕的是标准错误。
import logging.config
logging.config.dictConfig({
"version": 1,
"disable_existing_loggers": True,
"formatters": {
"json": {"()": "json_log_formatter.JSONFormatter"},
},
"handlers": {
"console": {
"formatter": "json",
"class": "logging.StreamHandler",
}
},
"loggers": {
"": {"handlers": ["console"], "level": "INFO"},
},
})
log-with-context
创建一个新的记录器。然后,这里是如何创建您的记录器:
from log_with_context import add_logging_context, Logger
LOGGER = Logger(__name__)
add_logging_context
上下文管理器记录消息。您可以使用 add_logging_context
上下文管理器推送和弹出上下文键和值。以下是一些示例代码,用于展示如何使用上下文管理器:
with add_logging_context(current_request="hi"):
LOGGER.info("Level 1")
with add_logging_context(more_info="this"):
LOGGER.warning("Level 2")
LOGGER.info("Back to level 1")
LOGGER.error("No context at all...")
这是上面的日志消息打印为 JSON 时的样子:
{"current_request": "hi", "message": "Level 1", "time": "2021-08-03T21:29:45.987392"}
{"current_request": "hi", "more_info": "this", "message": "Level 2", "time": "2021-08-03T21:29:45.988786"}
{"current_request": "hi", "message": "Back to level 1", "time": "2021-08-03T21:29:45.989178"}
{"message": "No context at all...", "time": "2021-08-03T21:29:45.989600"}
答案 4 :(得分:0)
您可以扩展Handling类并为其提供一些包含帐户名称的参数,但我不确定这对日志记录设置文件有何影响。
或者,之前我这样做的方法是添加包装器方法来调用记录器方法。
所以我要定义:
class MyClass:
account_name = "bob"
def info(self, message):
logging.info("%s|%s" % (self.account_name, message))
更改emit函数以处理解包,然后调用self.info("Info message")
代替logger.info("Info message")
。