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

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

  • 通过向日志记录函数传递一个extra参数引入上下文信息
  • 使用LoggerAdapters引入上下文信息
  • 使用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": "113.208.78.29", "username": "Petter"}
logger.debug("User Login!", extra=extra_dict)
extra_dict = {"ip": "223.190.65.139", "username": "Jerry"}
logger.info("User Access!", extra=extra_dict)

输出:

2017-05-14 15:47:25,562 - myPro - 113.208.78.29 - Petter - User Login!
2017-05-14 15:47:25,562 - myPro - 223.190.65.139 - Jerry - User Access!

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

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

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

使用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
 def debug(self, msg, *args, **kwargs):
  """
  Delegate a debug call to the underlying logger, after adding
  contextual information from this adapter instance.
  """
  msg, kwargs = self.process(msg, kwargs)
  self.logger.debug(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": "113.208.78.29", "username": "Petter"})
logger.extra = {"ip": "113.208.78.29", "username": "Petter"}
logger.info("User Login!")
logger.info("User Login!")

输出结果:

# 使用extra默认值:{"ip": "IP", "username": "USERNAME"}
2017-05-14 17:23:15,148 - myPro - IP - USERNAME - User Login!
# info(msg, extra)方法中传递的extra方法没有覆盖默认值
2017-05-14 17:23:15,148 - myPro - IP - USERNAME - User Login!
# extra默认值被修改了
2017-05-14 17:23:15,148 - myPro - 113.208.78.29 - Petter - User Login!
2017-05-14 17:23:15,148 - myPro - 113.208.78.29 - 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": "113.208.78.29", "username": "Petter"})
 logger.info("User Login!")
 logger.info("User Login!")

输出结果:

# 使用extra默认值:{"ip": "IP", "username": "USERNAME"}
2017-05-22 17:35:38,499 - myPro - IP - USERNAME - User Login!
# info(msg, extra)方法中传递的extra方法已覆盖默认值
2017-05-22 17:35:38,499 - myPro - 113.208.78.29 - Petter - User Login!
# extra默认值保持不变
2017-05-22 17:35:38,499 - myPro - IP - USERNAME - User Login!
2017-05-22 17:35:38,499 - myPro - IP - USERNAME - User Login!

OK! 问题解决了。

其实,如果我们想不受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')
 f = ContextFilter()
 logger.addFilter(f)
 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)
  filter.ip = choice(ips)
  filter.username = choice(users)
  logger.log(lvl, 'A message at %s level with %d %s', lvlname, 2, 'parameters')

输出结果:

2017-05-15 10:21:49,401 myLogger DEBUG    IP              USER     A debug message
2017-05-15 10:21:49,401 myLogger INFO     IP              USER     An info message with some parameters
2017-05-15 10:21:49,401 myLogger INFO     219.238.78.91   Tom      A message at INFO level with 2 parameters
2017-05-15 10:21:49,401 myLogger INFO     219.238.78.91   Peter    A message at INFO level with 2 parameters
2017-05-15 10:21:49,401 myLogger DEBUG    113.108.98.34   Jerry    A message at DEBUG level with 2 parameters
2017-05-15 10:21:49,401 myLogger CRITICAL 43.123.99.68    Tom      A message at CRITICAL level with 2 parameters
2017-05-15 10:21:49,401 myLogger INFO     43.123.99.68    Jerry    A message at INFO level with 2 parameters

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

以上所述是小编给大家介绍的Python向日志输出中添加上下文信息,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • 详解Python中contextlib上下文管理模块的用法

    咱们用的os模块,读取文件的时候,其实他是含有__enter__ __exit__ .  一个是with触发的时候,一个是退出的时候. with file('nima,'r') as f: print f.readline() 那咱们自己再实现一个标准的可以with的类. 我个人写python的时候,喜欢针对一些需要有关闭逻辑的代码,构造成with的模式 . #encoding:utf-8 class echo: def __enter__(self): print 'enter' def __

  • 正确理解python中的关键字“with”与上下文管理器

    前言 如果你有阅读源码的习惯,可能会看到一些优秀的代码经常出现带有 "with" 关键字的语句,它通常用在什么场景呢?今天就来说说 with 和 上下文管理器. 对于系统资源如文件.数据库连接.socket 而言,应用程序打开这些资源并执行完业务逻辑之后,必须做的一件事就是要关闭(断开)该资源. 比如 Python 程序打开一个文件,往文件中写内容,写完之后,就要关闭该文件,否则会出现什么情况呢?极端情况下会出现 "Too many open files" 的错误,

  • Python上下文管理器和with块详解

    上下文管理器和with块,具体内容如下 上下文管理器对象存在的目的是管理 with 语句,就像迭代器的存在是为了管理 for 语句一样. with 语句的目的是简化 try/finally 模式.这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常. return 语句或 sys.exit() 调用而中止,也会执行指定的操作. finally 子句中的代码通常用于释放重要的资源,或者还原临时变更的状态. ==上下文管理器协议包含enter和exit两个方法==. with 语句开

  • Python深入学习之上下文管理器

    上下文管理器(context manager)是Python2.5开始支持的一种语法,用于规定某个对象的使用范围.一旦进入或者离开该使用范围,会有特殊操作被调用 (比如为对象分配或者释放内存).它的语法形式是with...as... 关闭文件 我们会进行这样的操作:打开文件,读写,关闭文件.程序员经常会忘记关闭文件.上下文管理器可以在不需要文件的时候,自动关闭文件. 下面我们看一下两段程序: 复制代码 代码如下: # without context manager f = open("new.t

  • python 上下文管理器使用方法小结

    上下文管理器最常用的是确保正确关闭文件, with open('/path/to/file', 'r') as f: f.read() with 语句的基本语法, with expression [as variable]: with-block expression是一个上下文管理器,其实现了enter和exit两个函数.当我们调用一个with语句时, 依次执行一下步骤, 1.首先生成一个上下文管理器expression, 比如open('xx.txt'). 2.执行expression.en

  • 深入解析Python中的上下文管理器

    1. 上下文管理器是什么? 举个例子,你在写Python代码的时候经常将一系列操作放在一个语句块中: (1)当某条件为真 – 执行这个语句块 (2)当某条件为真 – 循环执行这个语句块 有时候我们需要在当程序在语句块中运行时保持某种状态,并且在离开语句块后结束这种状态. 所以,事实上上下文管理器的任务是 – 代码块执行前准备,代码块执行后收拾. 上下文管理器是在Python2.5加入的功能,它能够让你的代码可读性更强并且错误更少.接下来,让我们来看看该如何使用. 2. 如何使用上下文管理器? 看

  • 深入学习Python中的上下文管理器与else块

    前言 本文主要个大家介绍了关于Python上下文管理器与else块的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 在开始之前,我们先来看看下面这段话: 最终,上下文管理器可能几乎与子程序(subroutine)本身一样重要.目前,我们只了解了上下文管理器的皮毛--Basic 语言有with 语句,而且很多语言都有.但是,在各种语言中 with 语句的作用不同,而且做的都是简单的事,虽然可以避免不断使用点号查找属性,但是不会做事前准备和事后清理.不要觉得名字一样,就意

  • Python中的with语句与上下文管理器学习总结

    0.关于上下文管理器 上下文管理器是可以在with语句中使用,拥有__enter__和__exit__方法的对象. with manager as var: do_something(var) 相当于以下情况的简化: var = manager.__enter__() try: do_something(var) finally: manager.__exit__() 换言之,PEP 343中定义的上下文管理器协议允许将无聊的try...except...finally结构抽象到一个单独的类中,

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

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

  • 教你用python提取txt文件中的特定信息并写入Excel

    目录 问题描述: 工具: 操作: 源代码: Reference: 总结 问题描述: 我有一个这样的数据集叫test_result_test.txt,大概几百上千行,两行数据之间隔一个空行. N:505904X:0.969wsecY:0.694wsec N:506038X:4.246wsecY:0.884wsec N:450997X:8.472wsecY:0.615wsec ... 现在我希望能提取每一行X:和Y:后面的数字,然后保存进Excel做进一步的数据处理和分析 就拿第一行来说,我只需要0

  • 如何在postman中添加cookie信息步骤解析

    在测试工作中,很多的接口都依赖于登录接口,即在调用该接口前必须有登录的信息,否则调用会报错,那如何在postman中添加cookie信息呢?主要分为两个步骤,下面为大家详细介绍: 第一步:我们首先使用postman访问登录接口,在response中找到返回的cookie信息,并拷贝: 第二步:这里我们为大家介绍两种方法 方法1:在请求接口的headers中将cookie值设置为拷贝的cookie信息: 方法2:将cookie设置为全局变量,请求接口时直接调用即可. 全局变量的配置如下: 以上就是

  • 如何在 iOS 应用中添加位置信息

    最近要在 iOS 应用中添加位置信息, 需要满足的需求如下: 应用在前台时能够获取位置信息: 通过切换. Home 按键将应用切换到后台时,停止获取位置信息: 应用程序在前台运行, 直接锁定屏幕时,能够继续获取位置信息: 接下来逐步实现这三个需求. 获取设备位置信息 在 iOS 上获取位置信息是很容易的, 网上的资料也很多, 我的代码如下: // make sure location service is enabled. if (!CLLocationManager.LocationServi

  • 详解python日志输出使用配置文件格式

    python脚本日志输出使用配置文件的形式,不需要在每个脚本里面配置日志. 需求简述: 如我要写2个脚本(a.py和b.py),a.py日志输出到/var/log/a.log,b.py日志输出到/var/log/b.log,并且日志按日期切割.如果每个脚本都去配置一遍日志的话,浪费时间也不利于后期维护. 现在我要使用配置文件的格式去统一管理python脚本的代码日志输出,后续所有python脚本日志都在这个配置文件里面配置,脚本读取.方便后续维护和增加脚本的可读性. 需求实现: 我配置文件路径及

  • python将print输出的信息保留到日志文件中

    具体代码如下所示: import sys import os import sys import io import datetime def create_detail_day(): ''' :return: ''' # 年-月-日 # daytime = datetime.datetime.now().strftime('day'+'%Y-%m-%d') # 年_月_日 daytime = datetime.datetime.now().strftime('day'+'%Y_%m_%d')

  • 使用Filter过滤python中的日志输出的实现方法

    事情是这样的,我写了一个tornado的服务,过程当中我用logging记录一些内容,由于一开始并没有仔细观察tornado自已的日志管理,所以我就一般用debug来记录普通日志,error记录有问题的日志,但是当服务跑起来以后才发现,tornado的访问日志的级别是info,也就是20,debug是10的,所以如果我定义了日志的级别是debug,那么默认情况下肯定也会输出到日志文件中的. 但是我现在并不关心访问日志,而且由于我这个服务可能每时每刻都会有访问,这样在我对日志信息进行搜索的时候就会

  • Python输出PowerPoint(ppt)文件中全部文字信息的方法

    本文实例讲述了Python输出PowerPoint(ppt)文件中全部文字信息的方法.分享给大家供大家参考.具体分析如下: 下面的代码依赖于windows com,所以必须在机器上安装PowerPoint才能用,可以将PPT文件中的所有纯文本信息进行输出到指定的文件,非常实用. import win32com from win32com.client import Dispatch, constants ppt = win32com.client.Dispatch('PowerPoint.App

  • Python使用日志模块快速调试代码并记录异常信息

    目录 一.日志层级 二.创建模块 三.使用日志的优点 大家好,为了进行调试和错误跟踪,人们在整个代码库中广泛使用日志,今天来看看如何在代码中定义日志,并探讨日志的权限. 一.日志层级 在开始之前,需要注意的是,在日志记录中存在一个层次结构,称为日志树或日志者层次结构.该层次结构由几个级别组成,每个级别代表了日志信息的不同严重程度.最常见的层次是: CRITICAL #A critical error occurred, the program may not be able to continu

随机推荐