最近有個需求是把以前字符串輸出的log 改為json 格式,看了別人的例子,還是有些比較茫然,索性就把logging 整個翻了一邊,做點小總結.
初看log
在程序中, log 的用處寫代碼的你用你知道,log 有等級,DEBUG, INFO,...之類,還會記錄時間,log 發生的位置,在Python 中用的多的就是logging 這個標準庫中的包了.當打log 的時候究竟發生了什么? 是如何把不同級別的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

共有幾個等級, 每個等級對應一個Int 型整數 ,每個等級都會有一個方法與之對應,這樣輸出的內容就有了不同的等級.
logger 流程,

整個過程,還是不是很詳細,貼個圖吧, 現在看還太早,也說不清真個過程到底發生了什么,先放著,回頭來看會比較好懂. loger flow
讀代碼
代碼結構
logging 在源碼中有三個文件,結構如下:
├── config.py
├── handlers.py
└── __init__.py
_int.py中實現了基礎功能,主要的邏輯就在這個文件中 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看代碼就發現, 這個類沒做什么事情,就是一個model 而已, 有一個得到msg 的方法
Formatter Objects
Formatter 就是對Record 專門格式化的對象,它有一個format 方法,我們實現這個方法就能 做到不同的輸出,我的需求是做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刪掉源代碼中的實現細節,這個類里面主要的是format 方法,這是默認最基本的Formater ,還有專門對exception ,時間做格式化的方法。具體是哪個,看方法名就很清楚了,具體每個方法怎么實現的,一眼也就懂了。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, 知道這些你就基本可以隨意定制化了 下面就是我實現的json 格式的Formater,支持控制臺顏色變化,當然前提是你的控制終端支持(Ubuntu14.04測試通過)
import reimport loggingimport socketimport jsonimport tracebackimport datetimeimport timetry: from collections import OrderedDictexcept ImportError: passRESERVED_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 targetdef 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_ipclass 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_strclass 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)配置
很多時候我們并不是這樣自己去實現一些Handler ,Formater ,之類的代碼,用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.。
以上就是本文的全部內容,希望對大家的學習有所幫助。
新聞熱點
疑難解答
圖片精選