(可在我的博客閱讀更多文章)
最近有個需求是把以前字符串輸出的log 改為json 格式,看了別人的例子,還是有些比較茫然,索性就把logging 整個翻了一邊,做點小總結.
初看log
在程序中, log 的用處寫代碼的你用你知道,log 有等級,DEBUG, INFO,...之類,還會記錄時間,log 發(fā)生的位置,在Python 中用的多的就是logging 這個標準庫中的包了.當打log 的時候究竟發(fā)生了什么? 是如何把不同級別的log 輸出到不同文件里,還能在控制臺輸出.......
最簡單的用法
import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')
1,第一行導入包
2,第二行利用basicConfig 對輸出的格式,和輸出級別做了限制
3, 后面分別輸出了三條不同級別的 log
Logging Levels
Level | Numeric value |
---|---|
CRITICAL | 50 |
ERROR | 40 |
WARNING | 30 |
INFO | 20 |
DEBUG | 10 |
NOTSET | 0 |
共有幾個等級, 每個等級對應一個Int 型整數(shù) ,每個等級都會有一個方法與之對應,這樣輸出的內容就有了不同的等級.
logger 流程,
整個過程,還是不是很詳細,貼個圖吧, 現(xiàn)在看還太早,也說不清真?zhèn)€過程到底發(fā)生了什么,先放著,回頭來看會比較好懂.
![loger flow](http://7xsb9p.com1.z0.glb.clouddn.com/c563a7ee487c395283da502061e15020a15d2a5ac07a6ed429eea0c16de70de0.jpg)
讀代碼
代碼結構
logging 在源碼中有三個文件,結構如下:
├── config.py
├── handlers.py
└── __init__.py
_int.py中實現(xiàn)了基礎功能,主要的邏輯就在這個文件中
handlers.py 是一些Handlers (用處后面會明白)用起來很方便的.
config.py 是對配置做處理的方法.
objects
LogRecord Objects
每一次log 都會實例化一個Record 對象岳枷,這個對象有很多屬性,最后對LogRecord 做一下format 就輸出了氛驮,格式化的log 俄讹,里面就基本就是這個對象的屬性了蒋譬。
class LogRecord(object):
def __init__(self, name, level, pathname, lineno,
msg, args, exc_info, func=None):
ct = time.time()
self.name = name
self.msg = msg
if (args and len(args) == 1 and isinstance(args[0], collections.Mapping)
and args[0]):
args = args[0]
self.args = args
self.levelname = getLevelName(level)
self.levelno = level
self.pathname = pathname
try:
self.filename = os.path.basename(pathname)
self.module = os.path.splitext(self.filename)[0]
except (TypeError, ValueError, AttributeError):
self.filename = pathname
self.module = "Unknown module"
self.exc_info = exc_info
self.exc_text = None # used to cache the traceback text
self.lineno = lineno
self.funcName = func
self.created = ct
self.msecs = (ct - long(ct)) * 1000
self.relativeCreated = (self.created - _startTime) * 1000
if logThreads and thread:
self.thread = thread.get_ident()
self.threadName = threading.current_thread().name
else:
self.thread = None
self.threadName = None
if not logMultiprocessing:
self.processName = None
else:
self.processName = 'MainProcess'
mp = sys.modules.get('multiprocessing')
if mp is not None:
try:
self.processName = mp.current_process().name
except StandardError:
pass
if logProcesses and hasattr(os, 'getpid'):
self.process = os.getpid()
else:
self.process = None
def __str__(self):
return '<LogRecord: %s, %s, %s, %s, "%s">'%(self.name, self.levelno,
self.pathname, self.lineno, self.msg)
def getMessage(self):
pass
看代碼就發(fā)現(xiàn), 這個類沒做什么事情尤蒿,就是一個model 而已, 有一個得到msg 的方法
Formatter Objects
Formatter 就是對Record 專門格式化的對象吗氏,它有一個format 方法,我們實現(xiàn)這個方法就能 做到不同的輸出揽乱,我的需求是做json 格式的log 其實關鍵就在寫一個Formatter 就好了
class Formatter(object):
converter = time.localtime
def __init__(self, fmt=None, datefmt=None):
if fmt:
self._fmt = fmt
else:
self._fmt = "%(message)s"
self.datefmt = datefmt
def formatTime(self, record, datefmt=None):
pass
def formatException(self, ei):
pass
def usesTime(self):
return self._fmt.find("%(asctime)") >= 0
def format(self, record):
pass
刪掉源代碼中的實現(xiàn)細節(jié)名眉,這個類里面主要的是format 方法,這是默認最基本的Formater 锤窑,還有專門對exception 璧针,時間做格式化的方法。具體是哪個渊啰,看方法名就很清楚了探橱,具體每個方法怎么實現(xiàn)的,一眼也就懂了绘证。fmt 是制定格式化的隧膏,具體怎么指定在最基礎的用法中就有例子,datefmt 是對時間格式的指定嚷那。
Filter Objects
這個類是Logger 和Handler 的基類胞枕,主要有一個Filter 方法,和一個filters 屬性
Handler Objects
叫Handler 的類還真的不少,在SocketServer 中也有看到,具體的功能都在Handler 中.在這里,組合所有的Formatter ,和控制log 的輸出的方向魏宽,繼承自Filter.
def __init__(self, level=NOTSET):
Filterer.__init__(self)
self._name = None
self.level = _checkLevel(level)
self.formatter = None
_addHandlerRef(self)
self.createLock()
在init方法中看到腐泻,Handler 也有一個屬性,通過把自身的屬性和LogRecord 的level對比來決定是否處理這個LogRecord 的队询。每個Handler 都有一個Formatter 屬性派桩,其實就是上面介紹的Formatter 。Handler 就是來控制LogRecord 和Formatter 的蚌斩,它還可以控制輸出的方式铆惑,在后面會有,StreamHandler,FileHandler等送膳。通過名稱也就能明白具體能干什么员魏,這就是編程取名的智慧。
Logger Objects
這個類通常會通過getLogger()或者getLogger(name)來得到,不會直接new 一個出來.它會有*info(msg, args, kwargs) ,warn(msg, *args, kwargs)等方法,
def __init__(self, name, level=NOTSET):
Filterer.__init__(self)
self.name = name
self.level = _checkLevel(level)
self.parent = Noneou
self.handlers = []
self.disabled = 0
從init方法中能看到handlers 屬性,這是一個list 叠聋,每個LogRecord 通過Handlers 不同的handlers 就能以不同的格式輸出到不同的地方了撕阎。每個Logger 可以通過addHandler(hdlr)方法來添加各種Handler,
知道這些你就基本可以隨意定制化了
下面就是我實現(xiàn)的json 格式的Formater,支持控制臺顏色變化碌补,當然前提是你的控制終端支持(Ubuntu14.04測試通過)
import re
import logging
import socket
import json
import traceback
import datetime
import time
try:
from collections import OrderedDict
except ImportError:
pass
RESERVED_ATTRS = (
'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename',
'funcName', 'levelname', 'levelno', 'lineno', 'module',
'msecs', 'message', 'msg', 'name', 'pathname', 'process',
'processName', 'relativeCreated', 'stack_info', 'thread', 'threadName')
RESERVED_ATTR_HASH = dict(zip(RESERVED_ATTRS, RESERVED_ATTRS))
COLORS ={
'HEADER' : '\\033[95m',
'INFO' : '\\033[94m',
'DEBUG' : '\\033[92m',
'WARNING' : '\\033[93m',
'ERROR' : '\\033[91m',
'ENDC' : '\\033[0m',
}
def merge_record_extra(record, target, reserved=RESERVED_ATTR_HASH):
for key, value in record.__dict__.items():
if (key not in reserved
and not (hasattr(key, "startswith")
and key.startswith('_'))):
target[key] = value
return target
def get_host_info():
host_name = ''
local_ip = ''
try:
host_name = socket.gethostname()
local_ip = socket.gethostbyname(host_name)
except Exception, e:
pass
return host_name, local_ip
class JsonFormatterBase(logging.Formatter):
def __init__(self, *args, **kwargs):
logging.Formatter.__init__(self, *args, **kwargs)
self._required_fields = self.parse()
self._skip_fields = dict(zip(self._required_fields,self._required_fields))
self._skip_fields.update(RESERVED_ATTR_HASH)
def parse(self):
standard_formatters = re.compile(r'\\((.+?)\\)', re.IGNORECASE)
return standard_formatters.findall(self._fmt)
def add_fields(self, record ):
log_record = {}
for field in self._required_fields:
log_record[field] = record.__dict__.get(field)
host_name , local_ip = get_host_info()
log_record[u'@hostName'] = host_name
log_record[u'@localIp'] = local_ip
return log_record
#merge_record_extra(record, log_record, reserved=self._skip_fields)
def process_log_record(self, log_record):
"""
Override this method to implement custom logic
on the possibly ordered dictionary.
"""
try:
new_record = OrderedDict()
except Exception, e:
return log_record
key_list = [
'asctime',
'levelname',
'@hostName',
'@localIp',
'threadName',
'thread',
'name',
'pathname',
'lineno',
'message',
]
for k in key_list:
new_record[k] = log_record.get(k)
new_record.update(log_record)
return new_record
def jsonify_log_record(self, log_record):
"""Returns a json string of the log record."""
return json.dumps(log_record, ensure_ascii=False)
def format_col(self, message_str, level_name):
"""
是否需要顏色
"""
return message_str
def formatTime(self, record, datefmt=None):
ct = self.converter(record.created)
if datefmt:
s = time.strftime(datefmt, ct)
else:
t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
s = "%s.%03d" % (t, record.msecs)
return s
def format(self, record):
if isinstance(record.msg, dict):
record.message = record.msg
elif isinstance(record.msg, list) or isinstance(record.msg, tuple):
record.message = record.msg
elif isinstance(record.msg, basestring):
record.message = record.getMessage().split('\\n')
elif isinstance(record.msg, Exception):
record.message = traceback.format_exc(record.msg).split('\\n')
else :
record.message = repr(record.msg)
if "asctime" in self._required_fields:
record.asctime = self.formatTime(record, self.datefmt)
#
# if record.exc_info and not message_dict.get('exc_info'):
# message_dict['message'] = traceback.format_exception(*record.exc_info)
log_record = self.add_fields(record)
log_record = self.process_log_record(log_record)
message_str = self.jsonify_log_record(log_record)
message_str = self.format_col(message_str, level_name=record.levelname)
return message_str
class ConsoleFormater(JsonFormatterBase):
def __init__(self, *args, **kwargs):
JsonFormatterBase.__init__(self, *args, **kwargs)
def format_col(self, message_str, level_name):
if level_name in COLORS.keys():
message_str = COLORS.get(level_name) + message_str + COLORS.get('ENDC')
return message_str
def jsonify_log_record(self, log_record):
return json.dumps(log_record, ensure_ascii=False, indent=4)
class JsonFileFormater(JsonFormatterBase):
def __init__(self, *args, **kewars):
JsonFormatterBase.__init__(self, *args, **kewars)
def jsonify_log_record(self, log_record):
return json.dumps(log_record, ensure_ascii=False)
配置
很多時候我們并不是這樣自己去實現(xiàn)一些Handler 闻书,F(xiàn)ormater ,之類的代碼名斟,用logging 提供的config 就能做到了,如何寫config下面舉個例子解釋下魄眉,
SC_LOGGING_CONF = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
"format": "%(asctime)s [%(levelname)s] [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"info_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
"formatter": "simple",
"filename": PATH + "info-" + date.today().isoformat() + ".log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
},
"error_file_handler": {
"class": "logging.handlers.RotatingFileHandler",
"level": "ERROR",
"formatter": "simple",
"filename": PATH + "errors-" + date.today().isoformat() + ".log",
"maxBytes": 10485760,
"backupCount": 20,
"encoding": "utf8"
}
},
"": {
"level": "INFO",
"handlers": ["console", "info_file_handler", "error_file_handler"]
}
}
}
首先定義了一個formater 叫simaple , 然后定義了三個Handler ,分別是輸出到控制臺,輸出到文件和info,error的闷袒。
logging.config.dictConfig(CONFIG.SC_LOGGING_CONF)
通過這句就能讓這些配置產生效果了坑律,這也是config.py做的事情,不需要寫很多代碼也能定制個性化的log.囊骤。