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

本文介绍Python项目中配置管理器和日志管理器的设计与实现。重点讲解Python单例模式的应用及logging模块的高级配置,包括不同级别的日志如何区分输出。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        配置管理器和日志管理器是项目中最为独立的模块。我们可以很方便将其剥离出来供其他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")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

breaksoftware

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值