码农技术炒股之路——配置管理器、日志管理器
? ? ? ? 配置管理器和日志管理器是項目中最為獨立的模塊。我們可以很方便將其剝離出來供其他Python工程使用。文件的重點將是介紹Python單例和logging模塊的使用。(轉載請指明出于breaksoftware的csdn博客)
配置管理器
? ? ? ? 在《碼農技術炒股之路——架構和設計》中我們介紹過,配置管理將作為一個單例而存在。我嘗試過各種Python單例的實現方法,發現都存在一些問題,不能保證單例的特性。后來對一些方案進行修改,得出下面一種可靠的方式:
instances = {}
def singleton(cls, *args, **kw):global instancesdef _singleton(*args, **kw): if cls.__name__ not in instances: instances[cls.__name__] = cls(*args, **kw)return instances[cls.__name__] return _singleton
? ? ? ? 我們可以使用下面方法進行測試
@singletonclass singleton_test(object):def __init__(self, s_data):print "init"self._data = s_datadef run(self):print self._dataa = singleton_test("AAAAAAA")print aa.run()b = singleton_test("BBBBBBB")print bb.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 = Nonedef 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 Falsedef 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 = 0while True:try:logging.config.fileConfig(conf_path)except IOError as e:if error > 1:raise eif 2 == e.errno:if os.path.isdir(e.filename):os.makedirs(e.filename)else:os.makedirs(os.path.dirname(e.filename))error = error + 1except Exception as e: raise eelse:break
? ? ? ? 其最核心的就是logging.config.fileConfig(conf_path)這行。它讓logging庫通過一個配置文件進行初始化,其中包括日志類型、日志格式和日志輸出方式等信息。這些配置如何編寫,以及如何結合代碼使用,將是后文介紹的重點。
? ? ? ? 為了讓封裝的日志管理器有更強大的功能。我提出以下設計要求:
- Debug等級日志只打印在Console中。
- Info等級日志只打印在普通日志文件中。按小時切分。
- 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.htmllog_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庫進行?;厮荨S谑窃谟脩糇远x消息的基礎上,在調用日志方法前,對原消息做些修改(除了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 = 0while True:try:logging.config.fileConfig(conf_path)except IOError as e:if error > 1:raise eif 2 == e.errno:if os.path.isdir(e.filename):os.makedirs(e.filename)else:os.makedirs(os.path.dirname(e.filename))error = error + 1except Exception as e: raise eelse:breakdef log_debug(self, msg):log_debug = logging.getLogger('logger_LogDebug') #https://docs.python.org/2/howto/logging.htmllog_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_objlogger_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) + " ^ " + msgreturn new_msgdef LOG_DEBUG(msg):new_msg = modify_msg(msg)try:logger_obj.log_debug(new_msg)except Exception as e:print new_msgdef LOG_INFO(msg):new_msg = modify_msg(msg)try:logger_obj.log_info(new_msg)except Exception as e:print new_msgdef LOG_WARNING(msg):new_msg = modify_msg(msg)try:logger_obj.log_warning(new_msg)except Exception as e:print new_msgdef LOG_ERROR(msg):new_msg = modify_msg(msg)try:logger_obj.log_error(new_msg)except Exception as e:print new_msgdef LOG_CRITICAL(msg):new_msg = modify_msg(msg)try:logger_obj.log_critical(new_msg)except Exception as e:print new_msgdef LOG_ERROR_SQL(msg):try:logger_obj.log_error_sql(msg)except Exception as e:print msgif __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")
總結
以上是生活随笔為你收集整理的码农技术炒股之路——配置管理器、日志管理器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++拾趣——类构造函数的隐式转换
- 下一篇: 码农技术炒股之路——数据库管理器、正则表