python找寻合适的日志库logging Handler——Handler自定义实现

最近在用python tornado开发一个app的服务端。投产的系统肯定需要包含日志功能,这里就自然想到了用python自带的logging库。
 
logging中日志内容的输出都交由Handler来实现,但是logging中的自带的Handler都不能满足我们的需求。
我们希望能按时间段分割日志,如果使用FileHandler日志只能保存在一个文件,到后期日志文件会非常大,读写都成问题;而TimedRotatingFileHandler虽然可分割日志但是多进程时可能会造成日志文件被相互覆盖,导致日志丢失。
如此我便开始踏上找寻合适Handler的路上。
 
首先尝试使用FileHandler,然后写个脚本(比如用supervisord)定时切分日志的方式。但这里有一个问题是日志文件会被正在运行FileHandler保持,无法重命名,日志还是会一直写到同一个文件,尝试失败。
 
然后尝试使用继承logging自带的切分文件处理TimedRotatingFileHandler再重写处理切分日志的方法。
这里使用了一个网友所写的类
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2012 Ethan Zhang<http://github.com/Ethan-Zhang> 
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.


import time
import os
from logging.handlers import TimedRotatingFileHandler

class MultiProcessTimedRotatingFileHandler(TimedRotatingFileHandler):

    def doRollover(self):
        """ 
        do a rollover; in this case, a date/time stamp is appended to the filename
        when the rollover happens.  However, you want the file to be named for the
        start of the interval, not the current time.  If there is a backup count,
        then we have to get a list of matching filenames, sort them and remove
        the one with the oldest suffix.
        """
        if self.stream:
            self.stream.close()
        # get the time that this sequence started at and make it a TimeTuple
        t = self.rolloverAt - self.interval
        if self.utc:
            timeTuple = time.gmtime(t)
        else:
            timeTuple = time.localtime(t)
        dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
        #if os.path.exists(dfn):
        #    os.remove(dfn)
        if not os.path.exists(dfn):
            os.rename(self.baseFilename, dfn)
        if self.backupCount > 0:
            # find the oldest log file and delete it
            #s = glob.glob(self.baseFilename + ".20*")
            #if len(s) > self.backupCount:
            #    s.sort()
            #    os.remove(s[0])
            for s in self.getFilesToDelete():
                os.remove(s)
        #print "%s -> %s" % (self.baseFilename, dfn)
        self.mode = 'a'
        self.stream = self._open()
        currentTime = int(time.time())
        newRolloverAt = self.computeRollover(currentTime)
        while newRolloverAt <= currentTime:
            newRolloverAt = newRolloverAt + self.interval
        #If DST changes and midnight or weekly rollover, adjust for this.
        if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
            dstNow = time.localtime(currentTime)[-1]
            dstAtRollover = time.localtime(newRolloverAt)[-1]
            if dstNow != dstAtRollover:
                if not dstNow:  # DST kicks in before next rollover, so we need to deduct an hour
                    newRolloverAt = newRolloverAt - 3600
                else:           # DST bows out before next rollover, so we need to add an hour
                    newRolloverAt = newRolloverAt + 3600
        self.rolloverAt = newRolloverAt
View Code
最后经过验证还是会出现日志文件被覆盖的情况,这个方案又被自己给否定。
 
“文件式的Handler不能满足需求,要就尝试则用数据库存日志吧。” 经过以上的失败后这个想法在浮现在脑海。
要实现这个方式需要两个步骤;第一步,自定义一个Handler;第二步,选择一中数据库存放日志。
 
如何自定义一个Handler呢?这个可用用顺藤摸瓜的方式找到方案。
首先,到python logging库中找到其自带的handler;对比找到一个自定义Handler最基本的约束,也就是找到其中最简单Handler的实现。
在handlers.py 文件下找到了最简单的SMTPHandler
 
其中getSubject只是给emit内部调用,所以很容易就得出实现自定义Handler的方式为:继承logging.Handler 然后再实现emit方法(写入日志时会调用)。
 
第一步已经实现,第二步就是选数据库了。
首先想选sqlite,查了一下其性能不太好,后面就尝试选mongodb。
在要自力更生的写一个之前,手贱的到网上一搜果然已经有人写好的mongodb handler,唉...
import logging

from bson.timestamp import Timestamp
from pymongo import Connection
from pymongo.collection import Collection
from pymongo.errors import OperationFailure, PyMongoError


"""
Example format of generated bson document:
{
    'thread': -1216977216,
    'threadName': 'MainThread',
    'level': 'ERROR',
    'timestamp': Timestamp(1290895671, 63),
    'message': 'test message',
    'module': 'test_module',
    'fileName': '/var/projects/python/log4mongo-python/tests/test_handlers.py',
    'lineNumber': 38,
    'method': 'test_emit_exception',
    'loggerName':  'testLogger',
    'exception': {
        'stackTrace': 'Traceback (most recent call last):
                       File "/var/projects/python/log4mongo-python/tests/test_handlers.py", line 36, in test_emit_exception
                       raise Exception(\'exc1\')
                       Exception: exc1',
        'message': 'exc1',
        'code': 0
    }
}
"""


class MongoFormatter(logging.Formatter):

    DEFAULT_PROPERTIES = logging.LogRecord('', '', '', '', '', '', '', '').__dict__.keys()

    def format(self, record):
        """Formats LogRecord into python dictionary."""
        # Standard document
        document = {
            'timestamp': Timestamp(int(record.created), int(record.msecs)),
            'level': record.levelname,
            'thread': record.thread,
            'threadName': record.threadName,
            'message': record.getMessage(),
            'loggerName': record.name,
            'fileName': record.pathname,
            'module': record.module,
            'method': record.funcName,
            'lineNumber': record.lineno
        }
        # Standard document decorated with exception info
        if record.exc_info is not None:
            document.update({
                'exception': {
                    'message': str(record.exc_info[1]),
                    'code': 0,
                    'stackTrace': self.formatException(record.exc_info)
                }
            })
        # Standard document decorated with extra contextual information
        if len(self.DEFAULT_PROPERTIES) != len(record.__dict__):
            contextual_extra = set(record.__dict__).difference(set(self.DEFAULT_PROPERTIES))
            if contextual_extra:
                for key in contextual_extra:
                    document[key] = record.__dict__[key]
        return document


class MongoHandler(logging.Handler):

    def __init__(self, level=logging.NOTSET, host='localhost', port=27017, database_name='logs', collection='logs',
                 username=None, password=None, fail_silently=False, formatter=None, capped=False,
                 capped_max=1000, capped_size=1000000, **options):
        """Setting up mongo handler, initializing mongo database connection via pymongo."""
        logging.Handler.__init__(self, level)
        self.host = host
        self.port = port
        self.database_name = database_name
        self.collection_name = collection
        self.username = username
        self.password = password
        self.fail_silently = fail_silently
        self.connection = None
        self.db = None
        self.collection = None
        self.authenticated = False
        self.formatter = formatter or MongoFormatter()
        self.capped = capped
        self.capped_max = capped_max
        self.capped_size = capped_size
        self.options = options
        self._connect()

    def _connect(self):
        """Connecting to mongo database."""

        try:
            self.connection = Connection(host=self.host, port=self.port, **self.options)
        except PyMongoError:
            if self.fail_silently:
                return
            else:
                raise

        self.db = self.connection[self.database_name]
        if self.username is not None and self.password is not None:
            self.authenticated = self.db.authenticate(self.username, self.password)

        if self.capped:
            try:  # We don't want to override the capped collection (and it throws an error anyway)
                self.collection = Collection(self.db, self.collection_name, capped=True, max=self.capped_max, size=self.capped_size)
            except OperationFailure:
                # Capped collection exists, so get it.
                self.collection = self.db[self.collection_name]
        else:
            self.collection = self.db[self.collection_name]

    def close(self):
        """If authenticated, logging out and closing mongo database connection."""
        if self.authenticated:
            self.db.logout()
        if self.connection is not None:
            self.connection.disconnect()

    def emit(self, record):
        """Inserting new logging record to mongo database."""
        if self.collection is not None:
            try:
                self.collection.save(self.format(record))
            except Exception:
                if not self.fail_silently:
                    self.handleError(record)
View Code
还好我发现了他的连接不支持mongodb 的主从模式和副本集模式。哈哈,找到了发泄我写代码情绪的地方了(虽然只写了两三行代码)。
mongodb_url='mongodb://192.168.10.200:10001,192.168.10.201:10001'
handler=MongoHandler(url=mongodb_url)
logger.addHandler(handler)
加上这几句就可以开始使用啦。
 
本来这应该算高一段落了,但我又想到了这个方案的缺欠——这个日志系统要依赖以数据库,而我们的日志系统是整个系统的基础模块,这个方案又不太合适了。
让我们回到最初的梦想,日志还是直接写到文件中,logging只带的不能有效的分割文件我们就自己写一个。
于是有了这一个完全自己写、支持按时间分割的Handler
#!/usr/bin/env python
# -*- coding:utf-8 -*-

import logging
import os,os.path
import datetime

_filefmt=os.path.join("logs","%Y-%m-%d","%H.log")
class MyLoggerHandler(logging.Handler):
    def __init__(self,filefmt=None):
        self.filefmt=filefmt
        if filefmt is None:
            self.filefmt=_filefmt
        logging.Handler.__init__(self)
    def emit(self,record):
        msg=self.format(record)
        _filePath=datetime.datetime.now().strftime(self.filefmt)
        _dir=os.path.dirname(_filePath)
        try:
            if os.path.exists(_dir) is False:
                os.makedirs(_dir)
        except Exception:
            print "can not make dirs"
            print "filepath is "+_filePath
            pass
        try:
            _fobj=open(_filePath,'a') 
            _fobj.write(msg)
            _fobj.write("\n")
            _fobj.flush()
            _fobj.close()
        except Exception:
            print "can not write to file"
            print "filepath is "+_filePath
            pass
这里的思路是每次写日志完后马上释放文件的句柄,这样这多进程中就不会照成冲突了。当然这里还可以最一下优化就是先缓冲一部分内容、或一段时间再一次性写入(这个后面觉得性能不敢再做吧)。
 
回顾一下这个找寻合适Handler的过程,主要集中在三步。第一、确认python自带的handler不合适,第二、确认handler的自定义实现方式,第三、选择日志的存储载体(这里思路似乎走的有点远,远离了最初的设想)。值得欣慰的是这个过程终于可以告一段落了。
 

转载于:https://www.cnblogs.com/shizioo/p/python_logging_handler_custom.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python 日志 logging 是一个非常强大、灵活的工具,可以帮助我们记录应用程序运行时的各种信息,包括错误信息、调试信息、警告信息等。logging 提供了很多功能和配置选项,可以让我们根据实际需求灵活地控制日志的输出。 日志长度是指在日志文件中每条日志消息的字符数。一般来说,日志长度应该控制在一定范围内,过长的日志会导致日志文件变得异常庞大,不便于查看和分析。因此,我们需要对日志长度进行限制。 下面介绍一些关于 Python 日志 logging 的理解和实践经验: 1. 日志级别 logging 提供了 5 个日志级别:DEBUG、INFO、WARNING、ERROR 和 CRITICAL,分别代表调试信息、普通信息、警告信息、错误信息和致命错误信息。我们可以根据实际需求设置不同的日志级别,以便过滤出需要的信息。 2. 日志格式 logging 提供了多种日志格式,包括简单格式、详细格式、自定义格式等。我们可以根据实际需求选择合适日志格式,以便更好地记录和分析日志。 3. 控制日志输出 logging 提供了多种输出方式,包括输出到控制台、输出到文件、输出到网络等。我们可以根据实际需求选择合适的输出方式,以便更好地记录和分析日志。 4. 设置日志长度 我们可以通过设置日志处理器的 maxBytes 属性和 backupCount 属性来限制日志文件的大小和数量。例如,我们可以设置每个日志文件最大为 10MB,最多保留 5 个日志文件,超过这个限制后就会自动删除旧的日志文件。 下面是一个示例代码,演示如何使用 logging 记录日志并限制日志长度: ```python import logging from logging.handlers import RotatingFileHandler # 创建日志处理器,限制日志文件最大为 10MB,最多保留 5 个日志文件 handler = RotatingFileHandler(filename='app.log', maxBytes=10*1024*1024, backupCount=5) # 设置日志格式 formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') handler.setFormatter(formatter) # 创建日志记录器,并设置日志级别和处理器 logger = logging.getLogger('myapp') logger.setLevel(logging.INFO) logger.addHandler(handler) # 记录日志 logger.info('This is a test message.') ``` 在上面的示例代码中,我们使用 RotatingFileHandler 日志处理器来限制日志文件的大小和数量。通过设置 maxBytes 属性和 backupCount 属性,我们可以限制日志文件最大为 10MB,最多保留 5 个日志文件。同时,我们还设置了日志格式和日志级别,以便更好地记录和分析日志。最后,我们使用 logger.info() 方法记录一条日志信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值