码农技术炒股之路——配置管理器、日志管理器

        配置管理器和日志管理器是项目中最为独立的模块。我们可以很方便将其剥离出来供其他Python工程使用。文件的重点将是介绍Python单例和logging模块的使用。(转载请指明出于breaksoftware的csdn博客)

配置管理器

        在《码农技术炒股之路——架构和设计》中我们介绍过,配置管理将作为一个单例而存在。我尝试过各种Python单例的实现方法,发现都存在一些问题,不能保证单例的特性。后来对一些方案进行修改,得出下面一种可靠的方式:

instances = {}
def singleton(cls, *args, **kw):
    global instances
    def _singleton(*args, **kw): 
        if cls.__name__ not in instances:  
            instances[cls.__name__] = cls(*args, **kw)
        return instances[cls.__name__]  
    return _singleton

        我们可以使用下面方法进行测试

    @singleton
    class singleton_test(object):
        def __init__(self, s_data):
            print "init"
            self._data = s_data
 
        def run(self):
            print self._data

    a = singleton_test("AAAAAAA")
    print a
    a.run()
    b = singleton_test("BBBBBBB")
    print b
    b.run()

        其结果是

init
<__main__.singleton_test object at 0x021C57D0>
AAAAAAA
<__main__.singleton_test object at 0x021C57D0>
AAAAAAA

        可见我们对通过修饰符修饰后的singleton_test类“构造”两次后得到的是同一个对象。

        配置管理的实现其实非常简单,它就是一个使用singleton修饰、封装了ConfigParser的单例类

import ConfigParser
from singleton import singleton

@singleton
class scheduler_frame_conf_inst():
    def __init__(self):
        self._cp = None

    def load(self, conf_path):
        print("load frame conf %s" % conf_path)
        self._cp = ConfigParser.SafeConfigParser()
        self._cp.read(conf_path)

    def has_option(self, section_name, option_name):
        if self._cp:
            return self._cp.has_option(section_name, option_name)
        return False

    def get(self, section_name, option_name):
        if self._cp:
            return self._cp.get(section_name, option_name)
        else:
            print("get conf %s %s" % (section_name, option_name))

        load方法用于从指定路径加载工程配置。因为子模块都有自己的配置,且可能格式不一致,所以如果这些配置都放在一个文件中会显得非常杂乱。故工程的主配置文件保存是一组子模块配置文件路径的信息。子模块通过自己的配置解释规则去解释这些文件。

[frame_job]
conf_path = ./conf/frame_job.conf

[frame_log]
conf_path = ./conf/log.conf

[strategy_job]
conf_path = ./conf/strategy_job.conf

[mysql_manager]
conf_path = ./conf/mysql_manager.conf

[regulars]
conf_path = ./conf/regulars_manager.conf

        上面配置分别对应于:系统任务管理器配置、日志管理器配置、普通任务管理器配置、数据库管理配置和正则管理器配置。

日志管理器

        日志管理是通过封装Python的logging实现的。官方说明并没有对如何配置logging进行详细且准确的说明,所以我在完善这个模块时进行了若干次尝试,才得出正确的使用方法。

        一般来说日志可以分为如下五种等级:

  • Info。用于记录一般性日志,如执行流程或者运行中的中间结果。如果线上日志量比较大,这种日志在上线前是可以关闭的。
  • Debug。用于记录辅助调试的信息。如果线上日志量比较大,这种日志在上线前是可以关闭的。
  • Warning。用于记录运行中我们可以接受的错误。一般发生这种错误只是一种预告,预示着某些方面出现了异常。
  • Error。用于记录运行中我们可以勉强接受的错误。这种错误的发生并不代表我们整个工程不可用,而是某些功能已经受限了。
  • Fatal。用于记录运行中我们不可以接收的错误。比如我们发现内存分配失败,就可以打印Fatal错误,并退出程序。因为内存都耗尽了,之后发生的什么事都不好预测,不如记录下错误信息后退出。

        Python的logging库也支持上述等级。我们先看下封装后的日志类初始化操作

@singleton
class loggingex():
    def __init__(self, conf_path):
        error = 0
        while True:
            try:
                logging.config.fileConfig(conf_path)
            except IOError as e:
                if error > 1:
                    raise e
                if 2 == e.errno:
                    if os.path.isdir(e.filename):
                        os.makedirs(e.filename)
                    else:
                        os.makedirs(os.path.dirname(e.filename))
                error = error + 1
            except Exception as e: 
                raise e
            else:
                break

        其最核心的就是logging.config.fileConfig(conf_path)这行。它让logging库通过一个配置文件进行初始化,其中包括日志类型、日志格式和日志输出方式等信息。这些配置如何编写,以及如何结合代码使用,将是后文介绍的重点。

        为了让封装的日志管理器有更强大的功能。我提出以下设计要求:

  1. Debug等级日志只打印在Console中。
  2. Info等级日志只打印在普通日志文件中。按小时切分。
  3. Warning、Error和Fatal等级日志只打印在错误日志文件中。按小时切分。

        我们先看下针对Debug等级日志的配置方式。

打印在Console中的Debug等级日志

        首先我们需要定义日志输出的格式。我们希望日志可以打印出:时间、等级、进程ID、线程ID和用户自定义消息。这样在配置文件中我们需要加入如下的内容

[formatter_LogFormatter]
format=%(asctime)s ^ %(levelname)s ^ %(process)d ^ %(thread)d ^ %(message)s
datefmt=
class=logging.Formatter

        注意一下,这个节的名称是formatter_LogFormatter。但是实际我们之后要使用的名称只有下划线之后一节——LogFormatter。“formatter_”是格式配置名的固有信息,即任何格式配置都要使用它来开头。

        然后我们要声明一个叫formatters节,其下keys包含了之前声明的格式配置名称

[formatters]
keys=LogFormatter

        下一步我们要声明日志输出方式。因为Debug日志是输出到Console中的,所以我们使用的类是StreamHandler

[handler_ConsoleHandler]
class=StreamHandler
formatter=LogFormatter
level=DEBUG
args=(sys.stdout,)

        上面一节记录了日志输出所使用的类名、所使用的格式、日志等级和输出参数。注意一下节的名称——handler_ConsoleHandler,和格式配置节名要以“formatter_”开始类似,输出方式的节名要以“handler_”开头,而实际的名称则是下划线之后的ConsoleHandler。

        接下来我们需要声明一个叫handlers的节,其下keys包含了之前声明的输出方式配置名称

[handlers]
keys=ConsoleHandler

        最后我们要声明一个叫loggers的节,其下keys字段它包含了日志对象的名称。这些名称用逗号分隔。在定义Debug等级日志对象名称前,我们先要定义一个叫root的日志对象

[loggers]
keys=root,LogDebug

        root日志对象的配置要包含所有声明的初始方式信息,当前我们只有ConsoleHandler,于是这样配置

[logger_root]
level=NOTSET
handlers=ConsoleHandler

        LogDebug的配置如下

[logger_LogDebug]
handlers=ConsoleHandler
qualname=logger_LogDebug
level=DEBUG
propagate=0

        节名是以logger_开头,其后跟着在Loggers中声明的Debug日志对象名称LogDebug。handler指向向Console输出的输出方式名ConsoleHandler;qualname指定为该节节名。level设置为DEBUG。

        在Python中,我们可以通过下面的方式使用该日志对象

    def log_debug(self, msg):
        log_debug = logging.getLogger('logger_LogDebug')        #https://docs.python.org/2/howto/logging.html
        log_debug.debug(msg)

打印在文件中、按时间切分的Info等级日志

        数据的内容格式我们还是借用LogFormatter定义。因为这次是要往文件中输出,所以我们需要重新定义一种输出方式——FileNomalHandler。

[handler_FileNomalHandler]
class=logging.handlers.TimedRotatingFileHandler
formatter=LogFormatter
level=INFO
args=('./log/nomal.log.i', 'H', 1, 60)

        因为要按时间维度切分,所以这次使用的类是logging.handlers.TimedRotatingFileHandler。然后我们再args中指定文件生成的路径和通用名,以及按多久时间切分一次。上述写法,将导致logging在工程的log目录下生成nomal.log.i.2017-01-01_23这样格式的数据

        别忘记修改handlers下的keys信息,要把新增的handler给加进去

[handlers]
keys=ConsoleHandler,FileNomalHandler,

        以及在logger_root加入它

[logger_root]
level=NOTSET
handlers=ConsoleHandler,FileNomalHandler

        相应的我们需要定义一个日志对象配置

[logger_LogInfo]
handlers=FileNomalHandler
qualname=logger_LogInfo
level=INFO
propagate=0

        并在loggers下的keys中新增该对象名称

[loggers]
keys=root,LogDebug,LogInfo

打印在文件中、按时间切分的Warning、Error和Fatal等级日志

        相应的配置修改和上面类似,当时要注意文件名称需要换一下。我把整个配置放在这面区域中

###############################################################################
[loggers]
keys=root,LogDebug,LogInfo,LogWarningErrorCritical,SQL_ERROR

[logger_root]
level=NOTSET
handlers=ConsoleHandler,FileNomalHandler,FileErrorHandler

[logger_LogDebug]
handlers=ConsoleHandler
qualname=logger_LogDebug
level=DEBUG
propagate=0

[logger_LogInfo]
handlers=FileNomalHandler
qualname=logger_LogInfo
level=INFO
propagate=0

[logger_LogWarningErrorCritical]
handlers=FileErrorHandler
qualname=logger_LogWarningErrorCritical
level=WARNING
propagate=0

[logger_SQL_ERROR]
handlers=SaveErrorSQL_FileHandler
qualname=logger_SQL_ERROR
level=WARNING
propagate=0
###############################################################################

###############################################################################
[handlers]
keys=ConsoleHandler,FileNomalHandler,FileErrorHandler,SaveErrorSQL_FileHandler

[handler_ConsoleHandler]
class=StreamHandler
formatter=LogFormatter
level=DEBUG
args=(sys.stdout,)

[handler_FileNomalHandler]
class=logging.handlers.TimedRotatingFileHandler
formatter=LogFormatter
level=INFO
args=('./log/nomal.log.i', 'H', 1, 60)

[handler_FileErrorHandler]
class=logging.handlers.TimedRotatingFileHandler
formatter=LogFormatter
level=WARNING
args=('./log/nomal.log.wec', 'H', 1, 60)

[handler_SaveErrorSQL_FileHandler]
class=logging.handlers.TimedRotatingFileHandler
formatter=SQLLogFormatter
level=WARNING
args=('./log/sql_error.log', 'H', 1, 60)

###############################################################################

###############################################################################
[formatters]
keys=LogFormatter,SQLLogFormatter

[formatter_LogFormatter]
format=%(asctime)s ^ %(levelname)s ^ %(process)d ^ %(thread)d ^ %(message)s
datefmt=
class=logging.Formatter

[formatter_SQLLogFormatter]
format=%(asctime)s ^ %(message)s
datefmt=
class=logging.Formatter
###############################################################################

        上面配置中,我新增了一个打印SQL的日志对象配置。因为我们之后维护时可能需要把执行失败的SQL重新执行一遍,所以需要一个尽量简洁的文件格式。         因为在日志中,我需要知道是哪个文件哪行出错,所以需要使用inspect库进行栈回溯。于是在用户自定义消息的基础上,在调用日志方法前,对原消息做些修改(除了SQL日志)以扩充信息。

        完整的代码如下:

import os
import sys
import inspect
import logging
import logging.config
from singleton import singleton

@singleton
class loggingex():
    def __init__(self, conf_path):
        error = 0
        while True:
            try:
                logging.config.fileConfig(conf_path)
            except IOError as e:
                if error > 1:
                    raise e
                if 2 == e.errno:
                    if os.path.isdir(e.filename):
                        os.makedirs(e.filename)
                    else:
                        os.makedirs(os.path.dirname(e.filename))
                error = error + 1
            except Exception as e: 
                raise e
            else:
                break
    
    def log_debug(self, msg):
        log_debug = logging.getLogger('logger_LogDebug')        #https://docs.python.org/2/howto/logging.html
        log_debug.debug(msg)
        
    def log_info(self, msg):
        log_info = logging.getLogger('logger_LogInfo')
        log_info.info(msg)
        
    def log_warning(self, msg):
        log_warning_error_critical = logging.getLogger('logger_LogWarningErrorCritical')
        log_warning_error_critical.warning(msg)
        
    def log_error(self, msg):
        log_warning_error_critical = logging.getLogger('logger_LogWarningErrorCritical')
        log_warning_error_critical.error(msg)      
        
    def log_critical(self, msg):
        log_warning_error_critical = logging.getLogger('logger_LogWarningErrorCritical')
        log_warning_error_critical.critical(msg)
 
    def log_error_sql(self, msg):
        log_error_sql = logging.getLogger('logger_SQL_ERROR')
        log_error_sql.critical(msg)

def LOG_INIT(conf_path):
    global logger_obj
    logger_obj = loggingex(conf_path)

def modify_msg(msg):
    stack_info = inspect.stack()
    if len(stack_info) > 2:
        file_name = inspect.stack()[2][1]
        line = inspect.stack()[2][2]
        function_name = inspect.stack()[2][3]
        new_msg = file_name + " ^ " + function_name + " ^ " + str(line) + " ^ " + msg
    return new_msg

def LOG_DEBUG(msg):
    new_msg = modify_msg(msg)
    try:
        logger_obj.log_debug(new_msg)
    except Exception as e:
        print new_msg
    
def LOG_INFO(msg):
    new_msg = modify_msg(msg)
    try:
        logger_obj.log_info(new_msg)
    except Exception as e:
        print new_msg
    
def LOG_WARNING(msg):
    new_msg = modify_msg(msg)
    try:
        logger_obj.log_warning(new_msg)
    except Exception as e:
        print new_msg
    
def LOG_ERROR(msg):
    new_msg = modify_msg(msg)
    try:
        logger_obj.log_error(new_msg)
    except Exception as e:
        print new_msg
    
def LOG_CRITICAL(msg):
    new_msg = modify_msg(msg)
    try:
        logger_obj.log_critical(new_msg)
    except Exception as e:
        print new_msg

def LOG_ERROR_SQL(msg):
    try:
        logger_obj.log_error_sql(msg)
    except Exception as e:
        print msg


if __name__ == "__main__":
    LOG_INIT("../../conf/log.conf")
    LOG_DEBUG('LOG_DEBUG')
    LOG_INFO('LOG_INFO')
    LOG_WARNING('LOG_WARNING')
    LOG_ERROR('LOG_ERROR')
    LOG_CRITICAL('LOG_CRITICAL')
    LOG_ERROR_SQL("Create XXX Error")

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Python专栏

Python | 用机器学习搞定数字验证码,还有谁?!

来源:http://www.hi-roy.com/2017/09/19/Python验证码识别

58950
来自专栏Python中文社区

Python数据可视化:2018年北上广深空气质量分析(附完整代码)

92.5是年均AQI值,从上面科普知识里可以知道,2017年天津整体空气质量只能是「良」中的下下等水平,与轻度污染近在咫尺。

41310
来自专栏量子位

机器学习和AI的区别是什么?| 今日吐槽

上面这个段子来自微软工程师Mat Velloso,他发在Twitter上之后,引发大量用户转发和评论。后来Velloso说,这个笑话过拟合了~

13420
来自专栏不二小段

Python自动统计微博抽奖中奖男女比例(附代码)

今天来写一个自动计算微博抽奖男女比例的代码,很早就应该发这篇了,只是不想蹭热点(才不是拖更)。

23730
来自专栏大数据文摘

业界 | Dropbox力荐!我们如何应对Python桌面应用程序的崩溃

揭秘Crashpad系统如何帮助Dropbox这样复杂的桌面程序捕获并报告崩溃,且兼容Python的多种语言。

11810
来自专栏Python中文社区

如何简单高效地部署和监控分布式爬虫项目

1、请先确保所有主机都已经安装和启动 Scrapyd,如果需要远程访问 Scrapyd,则需将 Scrapyd 配置文件中的 bind_address 修改为 ...

26440
来自专栏Web项目聚集地

到底是 Java 好还是 Python 好?

语言的优劣之争是个永恒的话题,有时候一次偶然地“擦枪走火”甚至可能会引发一场铁杆粉丝之间的“战争”。

12920
来自专栏Python爬虫与数据挖掘

在Windows下如何创建指定的虚拟环境

前几天给大家分享了如何在默认的情况下创建虚拟环境,没来得及上车的伙伴,可以戳这篇文章:在Windows下如何创建虚拟环境(默认情况下)。今天小编给大家分享一下,...

9910
来自专栏CDA数据分析师

业界 | 除了R、Python,还有这些重要的数据科学工具

在你向一些大神请教的时候,他可能也会推荐你学习这两个高级编程语言,然后顺便在推荐你了解一下SQL以及Math。如果讲究点的,可能还会传授你一些Spark、AWS...

13030
来自专栏项勇

Python : ex1

10130

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励