Python日志输出中添加上下文信息

除了传递给日志记录函数的参数(如msg)外,有时候我们还想在日志输出中包含一些额外的上下文信息。比如,在一个网络应用中,可能希望在日志中记录客户端的特定信息,如:远程客户端的IP地址和用户名。这里我们来介绍以下几种实现方式:

  • 通过向日志记录函数传递一个extra参数引入上下文信息;
  • 使用LoggerAdapter引入上下文信息;
  • 使用Filters引入上下文信息;

一、通过向日志记录函数传递extra参数引入上下文信息

前面我们提到过,可以通过向日志记录函数传递一个extra参数来实现向日志输出中添加额外的上下文信息,如:

import logging
import sys fmt = logging.Formatter("%(asctime)s - %(name)s - %(ip)s - %(username)s - %(message)s")
h_console = logging.StreamHandler(sys.stdout)
h_console.setFormatter(fmt) logger = logging.getLogger("MyPro")
logger.setLevel(logging.DEBUG)
logger.addHandler(h_console) extra_dict = {"ip": "95.27.26.126", "username": "Petter"}
logger.debug("User Login!", extra=extra_dict) extra_dict = {"ip": "95.27.26.127", "username": "Jerry"}
logger.info("User Access!", extra=extra_dict)

运行输出结果为:

2018-05-06 16:02:45,424 - MyPro - 95.27.26.126 - Petter - User Login!
2018-05-06 16:02:45,424 - MyPro - 95.27.26.127 - Jerry - User Access!

但是用这种方式来传递信息并不是那么方便,因为每次调用日志记录方法都要传递一个extra关键字参数。即便没有需要插入的上下文信息也是如此,因为该logger设置的formatter格式中指定的字段必须要要存在。所以,我们推荐使用下面两种方式来实现上下文信息的引入。

也许可以尝试创建不同的Logger实例来解决上面的问题,但是这显然不是一个好的解决方案,因为这些Logger实例并不会进行垃圾回收,尽管这在实践中并不是个问题,但是当Logger数量变得不可控将会非常难以管理。

二、使用LoggerAdapter引入上下文信息

使用LoggerAdapter类来传递上下文信息到日志事件中是一个非常简单的方式,可以把它看做第一种实现方式的优化版---因为它为extra提供了默认值。这个类设计的类似于Logger,因此我们可以像使用Logger类的实例那样来调用debug()、info()、warning()、error()、exception()、critical()和log()方法。当创建一个LoggerAdapter的实例时,我们需要传递一个Logger实例和一个包含上下文信息的类字典对象给该类的实例构建方法。当调用LoggerAdapter实例的一个日志记录方法时,该方法会在对日志消息和字典对象进行处理后,调用构建该实例时传递给该实例的logger对象的同名的日志记录方法,下面是LoggerAdapter类中几个方法的定义:

class LoggerAdapter(object):
"""
An adapter for loggers which makes it easier to specify contextual
information in logging output.
""" def __init__(self, logger, extra):
"""
Initialize the adapter with a logger and a dict-like object which
provides contextual information. This constructor signature allows
easy stacking of LoggerAdapters, if so desired. You can effectively pass keyword arguments as shown in the
following example: adapter = LoggerAdapter(someLogger, dict(p1=v1, p2="v2"))
"""
self.logger = logger
self.extra = extra def process(self, msg, kwargs):
"""
Process the logging message and keyword arguments passed in to
a logging call to insert contextual information. You can either
manipulate the message itself, the keyword args or both. Return
the message and kwargs modified (or not) to suit your needs. Normally, you'll only need to override this one method in a
LoggerAdapter subclass for your specific needs.
"""
kwargs["extra"] = self.extra
return msg, kwargs #
# Boilerplate convenience methods
#
def debug(self, msg, *args, **kwargs):
"""
Delegate a debug call to the underlying logger.
"""
self.log(DEBUG, msg, *args, **kwargs) def info(self, msg, *args, **kwargs):
"""
Delegate an info call to the underlying logger.
"""
self.log(INFO, msg, *args, **kwargs) def warning(self, msg, *args, **kwargs):
"""
Delegate a warning call to the underlying logger.
"""
self.log(WARNING, msg, *args, **kwargs) def warn(self, msg, *args, **kwargs):
warnings.warn("The 'warn' method is deprecated, "
"use 'warning' instead", DeprecationWarning, 2)
self.warning(msg, *args, **kwargs) def error(self, msg, *args, **kwargs):
"""
Delegate an error call to the underlying logger.
"""
self.log(ERROR, msg, *args, **kwargs) def exception(self, msg, *args, exc_info=True, **kwargs):
"""
Delegate an exception call to the underlying logger.
"""
self.log(ERROR, msg, *args, exc_info=exc_info, **kwargs) def critical(self, msg, *args, **kwargs):
"""
Delegate a critical call to the underlying logger.
"""
self.log(CRITICAL, msg, *args, **kwargs) def log(self, level, msg, *args, **kwargs):
"""
Delegate a log call to the underlying logger, after adding
contextual information from this adapter instance.
"""
if self.isEnabledFor(level):
msg, kwargs = self.process(msg, kwargs)
self.logger.log(level, msg, *args, **kwargs)
......省略其余代码......

通过分析上面的代码可以得出以下结论:

  • 上下文信息是在LoggerAdapter类的process()方法中被添加到日志记录的输出消息中的,如果要实现自定义需求只需要实现LoggerAdapter的子类并重写process()方法即可;
  • process()方法的默认实现中,没有修改msg的值,只是为关键字参数插入了一个名为extra的key,这个extra的值为传递给LoggerAdapter类构造方法的参数值;
  • LoggerAdapter类构造方法所接收的extra参数,实际上就是为了满足logger的formatter格式要求所提供的默认上下文信息;

关于上面的3个结论,我们来看下面的例子:

import logging
import sys # 初始化一个要传递给LoggerAdapter构造方法的logger实例
fmt = logging.Formatter("%(asctime)s - %(name)s - %(ip)s - %(username)s - %(message)s")
h_console = logging.StreamHandler(sys.stdout)
h_console.setFormatter(fmt)
init_logger = logging.getLogger("MyPro")
init_logger.setLevel(logging.DEBUG)
init_logger.addHandler(h_console) # 初始化一个要传递给LoggerAdapter构造方法的上下文字典对象
extra_dict = {"ip": "IP", "username": "USERNAME"} # 获取一个LoggerAdapter类的实例
logger = logging.LoggerAdapter(init_logger, extra_dict) # 应用中的日志记录方法调用
logger.info("User Login!")
logger.info("User Login!", extra={"ip": "95.27.26.126", "username": "Petter"})
logger.extra = {"ip": "95.27.26.126", "username": "Petter"}
logger.info("User Login!")
logger.info("User Login!")

运行输出结果为:

# 使用extra默认值:{"ip": "IP", "username": "USERNAME"}
2018-05-06 16:27:52,321 - MyPro - IP - USERNAME - User Login! # info(msg, extra)方法中传递的extra方法没有覆盖默认值
2018-05-06 16:27:52,322 - MyPro - IP - USERNAME - User Login! # extra默认值被修改了
2018-05-06 16:27:52,322 - MyPro - 95.27.26.126 - Petter - User Login!
2018-05-06 16:27:52,322 - MyPro - 95.27.26.126 - Petter - User Login!

根据上面的程序输出结果,我们会发现一个问题:传递给LoggerAdapter类构造方法的extra参数值不能被LoggerAdapter实例的日志记录函数(如上面调用的info()方法)中的extra参数覆盖,只能通过修改LoggerAdapter实例的extra属性来修改默认值(如上面使用的logger.extra=xxx),但是这也就意味着默认值被修改了。

解决这个问题的思路应该是:实现一个LoggerAdapter的子类,重写process()方法。其中对于kwargs参数的操作应该是先判断其本身是否包含extra关键字,如果包含则不使用默认值进行替换;如果kwargs参数中不包含extra关键字则取默认值,来看具体实现:

import logging
import sys class MyLoggerAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
if "extra" not in kwargs:
kwargs["extra"] = self.extra
return msg, kwargs if __name__ == "__main__":
# 初始化一个要传递给LoggerAdapter构造方法的logger实例
fmt = logging.Formatter("%(asctime)s - %(name)s - %(ip)s - %(username)s - %(message)s")
h_console = logging.StreamHandler(sys.stdout)
h_console.setFormatter(fmt)
init_logger = logging.getLogger("MyPro")
init_logger.setLevel(logging.DEBUG)
init_logger.addHandler(h_console) # 初始化一个要传递给LoggerAdapter构造方法的上下文字典对象
extra_dict = {"ip": "IP", "username": "USERNAME"} # 获取一个LoggerAdapter类的实例
logger = MyLoggerAdapter(init_logger, extra_dict) # 应用中的日志记录方法调用
logger.info("User Login!")
logger.info("User Login!", extra={"ip": "95.27.26.126", "username": "Petter"})
logger.info("User Login!")
logger.info("User Login!")

运行输出结果为:

# 使用extra默认值:{"ip": "IP", "username": "USERNAME"}
2018-05-06 16:38:43,337 - MyPro - IP - USERNAME - User Login! # info(msg, extra)方法中传递的extra方法已覆盖默认值
2018-05-06 16:38:43,337 - MyPro - 95.27.26.126 - Petter - User Login! # extra默认值保持不变
2018-05-06 16:38:43,337 - MyPro - IP - USERNAME - User Login!
2018-05-06 16:38:43,337 - MyPro - IP - USERNAME - User Login!

perfect!问题解决了。其实,如果我们想不受formatter的限制,在日志输出中实现自由的字段插入,可以通过在自定义LoggerAdapter的子类的process()方法中将字典参数中的关键字信息拼接到日志事件的消息中。很明显,这些上下文中的字段信息在日志输出中的位置是有限的。而使用"extra"的优势在于,这个类字典对象的值将被合并到这个LogRecord实例的__dict__中,这样就允许我们通过Formatter实例自定义日志输出的格式字符串。这虽然使得上下文信息中的字段信息在日志输出中的位置变得与内置字段一样灵活,但是前提是传递给构造器方法的这个类字典对象中的key必须是确定且明了的。

三、使用Filters引入上下文信息

另外,我们还可以使用自定义的Filter.Filter实例的方式,在filter(record)方法中修改传递过来的LogRecord实例,把要加入的上下文信息作为新的属性赋值给该实例,这样就可以通过指定formatter的字符串格式来输出这些上下文信息了。我们模仿上面的实现,在传递给filter(record)方法的LogRecord实例中添加两个与当前网络请求相关的信息:IP和USERNAME。

import logging
from random import choice class ContextFilter(logging.Filter):
ip = "IP"
username = "USER" def filter(self, record):
record.ip = self.ip
record.username = self.username
return True if __name__ == "__main__":
levels = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL)
users = ['Tom', 'Jerry', 'Peter']
ips = ['113.108.98.34', '219.238.78.91', '43.123.99.68'] logging.basicConfig(level=logging.DEBUG,
format='%(asctime)-15s %(name)-5s %(levelname)-8s %(ip)-15s %(username)-8s %(message)s')
logger = logging.getLogger('myLogger')
my_filter = ContextFilter()
logger.addFilter(my_filter)
logger.debug('A debug message')
logger.info('An info message with %s', 'some parameters') for x in range(5):
lvl = choice(levels)
lvlname = logging.getLevelName(lvl)
my_filter.ip = choice(ips)
my_filter.username = choice(users)
logger.log(lvl, 'A message at %s level with %d %s', lvlname, 2, 'parameters')

运行输出结果为:

2018-05-06 16:52:54,853 myLogger DEBUG    IP              USER     A debug message
2018-05-06 16:52:54,853 myLogger INFO IP USER An info message with some parameters
2018-05-06 16:52:54,853 myLogger ERROR 219.238.78.91 Tom A message at ERROR level with 2 parameters
2018-05-06 16:52:54,854 myLogger DEBUG 43.123.99.68 Peter A message at DEBUG level with 2 parameters
2018-05-06 16:52:54,854 myLogger INFO 219.238.78.91 Peter A message at INFO level with 2 parameters
2018-05-06 16:52:54,854 myLogger DEBUG 219.238.78.91 Jerry A message at DEBUG level with 2 parameters
2018-05-06 16:52:54,854 myLogger DEBUG 219.238.78.91 Tom A message at DEBUG level with 2 parameters

需要说明的是:实际的网络应用程序中,可能还要考虑多线程并发时的线程安全问题,此时可以把连接信息或自定义过滤器的实例通过threading.local保存到一个threadlocal中。

Python 日志输出中添加上下文信息的更多相关文章

  1. 【转】Python之向日志输出中添加上下文信息

    [转]Python之向日志输出中添加上下文信息 除了传递给日志记录函数的参数(如msg)外,有时候我们还想在日志输出中包含一些额外的上下文信息.比如,在一个网络应用中,可能希望在日志中记录客户端的特定 ...

  2. Python之向日志输出中添加上下文信息

    除了传递给日志记录函数的参数(如msg)外,有时候我们还想在日志输出中包含一些额外的上下文信息.比如,在一个网络应用中,可能希望在日志中记录客户端的特定信息,如:远程客户端的IP地址和用户名.这里我们 ...

  3. python logging详解及自动添加上下文信息

    之前写过一篇文章日志的艺术(The art of logging),提到了输出日志的时候记录上下文信息的重要性,我认为上下文信息包括: when:log事件发生的时间 where:log事件发生在哪个 ...

  4. Python日志输出——logging模块

    Python日志输出——logging模块 标签: loggingpythonimportmodulelog4j 2012-03-06 00:18 31605人阅读 评论(8) 收藏 举报 分类: P ...

  5. 论文系统Step1:从日志记录中提取特定信息

    论文系统Step1:从日志记录中提取特定信息 前言 论文数据需要,需要实现从服务器日志中提取出用户的特定交互行为信息.日志内容如下: 自己需要获取"请求数据包一行的信息"及&quo ...

  6. Android开发过程中在sh,py,mk文件中添加log信息的方法

    Android开发过程中在sh,py,mk文件中添加log信息的方法 在sh文件中: echo "this is a log info" + $info 在py文件中: print ...

  7. 使用 python 提取照片中的手机信息

    使用 python 提取照片中的手机信息 最近在做一个项目,有一个很重要的点是需要获取使用用户的手机信息,这里我选择从照片中获取信息.有人会问为什么不从手机里面直接获取设备信息.由于现在android ...

  8. python日志输出

    import logging logger = logging.getLogger() #生成一个日志对象,()内为日志对象的名字,可以不带,名字不给定就是root,一般给定名字,否则会把其他的日志输 ...

  9. Python 日志输出

    昨天的任务是需要记录各操作的性能数据,所以需要用这种格式来输出日志:{"adb_start_time": 1480040663, "tag_name": &qu ...

随机推荐

  1. 成都Uber优步司机奖励政策(3月24日)

    滴快车单单2.5倍,注册地址:http://www.udache.com/ 如何注册Uber司机(全国版最新最详细注册流程)/月入2万/不用抢单:http://www.cnblogs.com/mfry ...

  2. FreeRTOS的信号量和互斥量

    1. 理解如下,言简意赅的说,信号量解决同步,互斥量解决竞争. 信号量用于同步,主要任务间和中断间同步:互斥量用于互锁,用于保护同时只能有一个任务访问的资源,为资源上一把锁. 互斥量具有优先级继承,信 ...

  3. 03-JVM内存模型:堆与方法区

    一.堆(Heap) 1.1.什么是堆 堆是用于存放对象的内存区域.因此,它是垃圾收集器(GC)管理的主要目标.其具有以下特点: 堆在逻辑上划分为“新生代”和“老年代”.由于JAVA中的对象大部分是朝生 ...

  4. 程序迭代时测试操作的要点(后端&前端)

    今晚直播课内容简介,视频可点击链接免费听 <程序迭代时测试操作的要点(后端&前端)> ===== 1:迭代时后台涉及的操作有哪些?如何进行 a.更新war包:用于访问web\app ...

  5. ortp代码简析

    ortp初始化 /** *    Initialize the oRTP library. You should call this function first before using *     ...

  6. Python入门编程中的变量、字符串以及数据类型

    //2018.10.10 字符串与变量 1. 在输出语句中如果需要出现单引号或者双引号,可以使用转义符号\,它可以将其中的歧义错误解释化解,使得输出正常: 2. 对于python的任何变量都需要进行赋 ...

  7. redis 在java中的使用

    1.首先下载jar包放到你的工程中 2.练习 package com.jianyuan.redisTest; import java.util.Iterator;import java.util.Li ...

  8. Spark- 根据IP获取城市(java)

    开源 IP 地址定位库 ip2region 1.4 ip2region 是准确率 99.9% 的 IP 地址定位库,0.0x毫秒级查询,数据库文件大小只有 2.7M,提供了 Java.PHP.C.Py ...

  9. 十六:The YARN Service Registry

    yarn 服务注册功能是让长期运行的程序注册为服务一直运行. yarn中运行的程序分为两类,一类是短程序,一类一直运行的长程序.第二种也称为服务.yarn服务注册就是让应用程序能把自己注册为服务,如h ...

  10. Beta阶段项目展示博客

    Beta阶段项目展示 团队成员的简介 详细见团队简介 角色 姓名 照片 项目经理,策划 游心 策划 王子铭 策划 蔡帜 美工 赵晓宇 美工 王辰昱 开发.架构师 解小锐 开发 陈鑫 开发 李金奇 开发 ...