最近在用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
最后经过验证还是会出现日志文件被覆盖的情况,这个方案又被自己给否定。
 
“文件式的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)
还好我发现了他的连接不支持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的自定义实现方式,第三、选择日志的存储载体(这里思路似乎走的有点远,远离了最初的设想)。值得欣慰的是这个过程终于可以告一段落了。
 

最新文章

  1. Exception in thread &quot;main&quot; java.lang.NoSuchMethodError: org.objectweb.asm.ClassWriter.&lt;init&gt;(I)V
  2. Eclipse CDT: Shortcut to switch between .h and .cpp
  3. C#.NET万能数据库访问封装类(ACCESS、SQLServer、Oracle)
  4. [NHibernate]一对多关系(关联查询)
  5. Mac下体验Hexo与Github Pages搭建
  6. Hive数据仓库
  7. WEB 业务测试中需要关注的问题
  8. 查看真机的系统中sdk的版本
  9. Python:类属性,实例属性,私有属性与静态方法,类方法,实例方法
  10. .net中Web.config文件的基本原理及相关设置
  11. SAX解析
  12. HDU 3338 Kakuro Extension
  13. 用VUEJS做一个猫眼电影web app
  14. this web application instance has been stopped already.
  15. 如何把百度统计代码放入JS文件中?百度统计的JS脚本原理分析
  16. [WinForm]dataGridView自定动态设定序号列框
  17. [20190409]pre_page_sga=true与连接缓慢的问题.txt
  18. Hibernate处理事务并发问题
  19. 学习大数据基础框架hadoop需要什么基础
  20. ArcGIS for android访问天地图

热门文章

  1. swift 3新特性总结
  2. HDU2043 密码
  3. 深入理解java虚拟机-第13章-线程安全与锁优化
  4. C++中atof函数的实现和atoi的实现
  5. Jmeter聚合报告
  6. sssp-springmvc+spring+spring-data-jpa问题总结
  7. Java8新特性——StreamAPI(一)
  8. turtle海龟作图
  9. Cascalog了解
  10. (高级篇)jQuery学习之jQuery Ajax用法详解