Python向日志輸出中添加上下文信息
除了傳遞給日志記錄函數(shù)的參數(shù)(如msg)外,有時(shí)候我們還想在日志輸出中包含一些額外的上下文信息。比如,在一個(gè)網(wǎng)絡(luò)應(yīng)用中,可能希望在日志中記錄客戶(hù)端的特定信息,如:遠(yuǎn)程客戶(hù)端的IP地址和用戶(hù)名。這里我們來(lái)介紹以下幾種實(shí)現(xiàn)方式:
- 通過(guò)向日志記錄函數(shù)傳遞一個(gè)extra參數(shù)引入上下文信息
- 使用LoggerAdapters引入上下文信息
- 使用Filters引入上下文信息
一、通過(guò)向日志記錄函數(shù)傳遞一個(gè)extra參數(shù)引入上下文信息
前面我們提到過(guò),可以通過(guò)向日志記錄函數(shù)傳遞一個(gè)extra參數(shù)來(lái)實(shí)現(xiàn)向日志輸出中添加額外的上下文信息,如:
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!
但是用這種方式來(lái)傳遞信息并不是那么方便,因?yàn)槊看握{(diào)用日志記錄方法都要傳遞一個(gè)extra關(guān)鍵詞參數(shù)。即便沒(méi)有需要插入的上下文信息也是如此,因?yàn)樵搇ogger設(shè)置的formatter格式中指定的字段必須要存在。所以,我們推薦使用下面兩種方式來(lái)實(shí)現(xiàn)上下文信息的引入。
也許可以嘗試在每次創(chuàng)建連接時(shí)都創(chuàng)建一個(gè)Logger實(shí)例來(lái)解決上面存在的問(wèn)題,但是這顯然不是一個(gè)好的解決方案,因?yàn)檫@些Logger實(shí)例并不會(huì)進(jìn)行垃圾回收。盡管這在實(shí)踐中不是個(gè)問(wèn)題,但是當(dāng)Logger數(shù)量變得不可控將會(huì)非常難以管理。
二、使用LoggerAdapters引入上下文信息
使用LoggerAdapter類(lèi)來(lái)傳遞上下文信息到日志事件的信息中是一個(gè)非常簡(jiǎn)單的方式,可以把它看做第一種實(shí)現(xiàn)方式的優(yōu)化版--因?yàn)樗鼮閑xtra提供了一個(gè)默認(rèn)值。這個(gè)類(lèi)設(shè)計(jì)的類(lèi)似于Logger,因此我們可以像使用Logger類(lèi)的實(shí)例那樣來(lái)調(diào)用debug(), info(), warning(),error(), exception(), critical()和log()方法。當(dāng)創(chuàng)建一個(gè)LoggerAdapter的實(shí)例時(shí),我們需要傳遞一個(gè)Logger實(shí)例和一個(gè)包含上下文信息的類(lèi)字典對(duì)象給該類(lèi)的實(shí)例構(gòu)建方法。當(dāng)調(diào)用LoggerAdapter實(shí)例的一個(gè)日志記錄方法時(shí),該方法會(huì)在對(duì)日志日志消息和字典對(duì)象進(jìn)行處理后,調(diào)用構(gòu)建該實(shí)例時(shí)傳遞給該實(shí)例的logger對(duì)象的同名的日志記錄方法。下面是LoggerAdapter類(lèi)中幾個(gè)方法的定義:
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)
通過(guò)分析上面的代碼可以得出以下結(jié)論:
- 上下文信息是在LoggerAdapter類(lèi)的process()方法中被添加到日志記錄的輸出消息中的,如果要實(shí)現(xiàn)自定義需求只需要實(shí)現(xiàn)LoggerAdapter的子類(lèi)并重寫(xiě)process()方法即可;
- process()方法的默認(rèn)實(shí)現(xiàn)中,沒(méi)有修改msg的值,只是為關(guān)鍵詞參數(shù)插入了一個(gè)名為extra的 key,這個(gè)extra的值為傳遞給LoggerAdapter類(lèi)構(gòu)造方法的參數(shù)值;
- LoggerAdapter類(lèi)構(gòu)建方法所接收的extra參數(shù),實(shí)際上就是為了滿(mǎn)足logger的formatter格式要求所提供的默認(rèn)上下文信息。
關(guān)于上面提到的第3個(gè)結(jié)論,我們來(lái)看個(gè)例子:
import logging import sys # 初始化一個(gè)要傳遞給LoggerAdapter構(gòu)造方法的logger實(shí)例 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) # 初始化一個(gè)要傳遞給LoggerAdapter構(gòu)造方法的上下文字典對(duì)象 extra_dict = {"ip": "IP", "username": "USERNAME"} # 獲取一個(gè)LoggerAdapter類(lèi)的實(shí)例 logger = logging.LoggerAdapter(init_logger, extra_dict) # 應(yīng)用中的日志記錄方法調(diào)用 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!")
輸出結(jié)果:
# 使用extra默認(rèn)值:{"ip": "IP", "username": "USERNAME"} 2017-05-14 17:23:15,148 - myPro - IP - USERNAME - User Login! # info(msg, extra)方法中傳遞的extra方法沒(méi)有覆蓋默認(rèn)值 2017-05-14 17:23:15,148 - myPro - IP - USERNAME - User Login! # extra默認(rèn)值被修改了 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!
根據(jù)上面的程序輸出結(jié)果,我們會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題:傳遞給LoggerAdapter類(lèi)構(gòu)造方法的extra參數(shù)值不能被LoggerAdapter實(shí)例的日志記錄函數(shù)(如上面調(diào)用的info()方法)中的extra參數(shù)覆蓋,只能通過(guò)修改LoggerAdapter實(shí)例的extra屬性來(lái)修改默認(rèn)值(如上面使用的logger.extra=xxx),但是這也就意味著默認(rèn)值被修改了。
解決這個(gè)問(wèn)題的思路應(yīng)該是:實(shí)現(xiàn)一個(gè)LoggerAdapter的子類(lèi),重寫(xiě)process()方法。其中對(duì)于kwargs參數(shù)的操作應(yīng)該是先判斷其本身是否包含extra關(guān)鍵字,如果包含則不使用默認(rèn)值進(jìn)行替換;如果kwargs參數(shù)中不包含extra關(guān)鍵字則取默認(rèn)值。來(lái)看具體實(shí)現(xiàn):
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__': # 初始化一個(gè)要傳遞給LoggerAdapter構(gòu)造方法的logger實(shí)例 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) # 初始化一個(gè)要傳遞給LoggerAdapter構(gòu)造方法的上下文字典對(duì)象 extra_dict = {"ip": "IP", "username": "USERNAME"} # 獲取一個(gè)自定義LoggerAdapter類(lèi)的實(shí)例 logger = MyLoggerAdapter(init_logger, extra_dict) # 應(yīng)用中的日志記錄方法調(diào)用 logger.info("User Login!") logger.info("User Login!", extra={"ip": "113.208.78.29", "username": "Petter"}) logger.info("User Login!") logger.info("User Login!")
輸出結(jié)果:
# 使用extra默認(rèn)值:{"ip": "IP", "username": "USERNAME"} 2017-05-22 17:35:38,499 - myPro - IP - USERNAME - User Login! # info(msg, extra)方法中傳遞的extra方法已覆蓋默認(rèn)值 2017-05-22 17:35:38,499 - myPro - 113.208.78.29 - Petter - User Login! # extra默認(rèn)值保持不變 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! 問(wèn)題解決了。
其實(shí),如果我們想不受formatter的限制,在日志輸出中實(shí)現(xiàn)自由的字段插入,可以通過(guò)在自定義LoggerAdapter的子類(lèi)的process()方法中將字典參數(shù)中的關(guān)鍵字信息拼接到日志事件的消息中。很明顯,這些上下文中的字段信息在日志輸出中的位置是有限制的。而使用'extra'的優(yōu)勢(shì)在于,這個(gè)類(lèi)字典對(duì)象的值將被合并到這個(gè)LogRecord實(shí)例的__dict__中,這樣就允許我們通過(guò)Formatter實(shí)例自定義日志輸出的格式字符串。這雖然使得上下文信息中的字段信息在日志輸出中的位置變得與內(nèi)置字段一樣靈活,但是前提是傳遞給構(gòu)造器方法的這個(gè)類(lèi)字典對(duì)象中的key必須是確定且明了的。
三、使用Filters引入上下文信息
另外,我們還可以使用自定義的Filter.Filter實(shí)例的方式,在filter(record)方法中修改傳遞過(guò)來(lái)的LogRecord實(shí)例,把要加入的上下文信息作為新的屬性賦值給該實(shí)例,這樣就可以通過(guò)指定formatter的字符串格式來(lái)輸出這些上下文信息了。
我們模仿上面的實(shí)現(xiàn),在傳遞個(gè)filter(record)方法的LogRecord實(shí)例中添加兩個(gè)與當(dāng)前網(wǎng)絡(luò)請(qǐng)求相關(guān)的信息: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')
輸出結(jié)果:
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
需要說(shuō)明的是: 實(shí)際的網(wǎng)絡(luò)應(yīng)用程序中,可能還要考慮多線程并發(fā)時(shí)的線程安全問(wèn)題,此時(shí)可以把連接信息或者自定義過(guò)濾器的實(shí)例通過(guò)threading.local保存到到一個(gè)threadlocal中。
以上所述是小編給大家介紹的Python向日志輸出中添加上下文信息,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
numpy庫(kù)ndarray多維數(shù)組的維度變換方法(reshape、resize、swapaxes、flatten)
這篇文章主要介紹了numpy庫(kù)ndarray多維數(shù)組的維度變換方法(reshape、resize、swapaxes、flatten),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04使用PyCharm配合部署Python的Django框架的配置紀(jì)實(shí)
這篇文章主要介紹了使用PyCharm配合部署Python的Django框架的配置紀(jì)實(shí),PyCharm是一款強(qiáng)大的Python的IDE,需要的朋友可以參考下2015-11-11python中threading開(kāi)啟關(guān)閉線程操作
這篇文章主要介紹了python中threading開(kāi)啟關(guān)閉線程操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-05-05