Java日志那些事

日志系统的发展我们日常接触到的日志系统有很多种,log4j,JUL(jdk自带),logback等,我们可以直接根据对象的日志API进行使用。但是考虑到API各不相同,所以出现了JCL(Jakarta Commons Logging)、slf4j等日志API框架。日志API框架只是统一的API,其底层的具体的日志记录工作还是由log4j、JUL、logback等承担。如何选择和搭配日志系统目前来说,新应用使用logback是首选,一些老系统中很可能使用的是log4j等。目前slf4j对logback和log4j都支持,对JCL也提供了桥接方法,将JCL的api转化slf4j的API。贴一张Webx中的图足以说明一切组装日志系统由于存在JCL,SLF4j两大日志框架,logback、log4j、JUL日志系统所以理论上有这么多种日志系统的搭配。JULlog4jlogbackjcl+log4jslf4j+slf4j-log4j12+log4jslf4j+logbackjcl-over-slf4j+slf4j+logbackjcl-over-slf4j+slf4j+slf4j-log4j12+log4j其中slf4j+slf4j-log4j12+log4j、slf4j+logback是主流,推荐使用。多种日志工具共存时的解决方案当依赖了一些三方库时,可能会出现多种日志共存的问题,无法保证每种日志抽象库都使用一样的实现类,此时需要制定固定的日志库。主流的日志库都提供了扩展方式JUL(java.util.logging)通过LogManager.getLogManager().getLogger("").addHandler()方法,可以添加日志具体实现LogManager.getLogManager().getLogger("").addHandler(new SLF4JBridgeHandler());继承java.util.logging.Handler 类,在实现中使用具体的Logger库即可实现例子:jul-to-slf4j(https://www.slf4j.org/api/org…)JCL(org.apache.commons.logging)j通过制定环境变量LogFactory,org.apache.commons.logging.LogFactorySystem.setProperty(“org.apache.commons.logging.LogFactory”,“com.answern.claimv2.framework.log.LogFactoryImplAdapter”);继承org.apache.commons.logging.LogFactory类,实现自己的LogFactory即可还有另外一种暴力的方式:不引入commons-logging包,而是创建jcl的一些同名类,在实现中直接使用具体的日志库。jcl-over-slf4j(https://mvnrepository.com/art…)就是一个典型的例子.相同功能的还有spring-jcl(https://mvnrepository.com/art…)log4jlog4j提供了java对原生spi机制的支持建立MEATA-INF/services/org.apache.logging.log4j.spi.Provider文件继承org.apache.logging.log4j.spi.Provider类,实现自己的LoggerContextFactory即可实现例子:log4j-to-slf4j(https://logging.apache.org/lo…)SLFJ4Jslf4j官方介绍了使用方式,通过引入不同的jar包来使用具体的日志库。由于slf4j拆分做的很好,当多种日志库共存时,若不引入slf4j-xxx.jar时,不会加载相应的日志库。所以若日志冲突时,使用slf4j的三方库只需要include/exclude相应的实现库即可。编写自己的框架/类库时该如何选择日志库由于日志库多种多样,如果盲目引入jcl或者slf4j时,可能会对具体使用的项目造成影响。所以最合适的方式是内嵌一套日志抽象,内部动态的去选择加载哪个日志库。主流的成熟框架都会这么做,尽可能的保持兼容性。例如spring/mybatis/freemarker/dubbo这些框架,都有一套内嵌的日志抽象,打印日志时只需要调用内嵌的日志即可做到全兼容。参考https://www.jianshu.com/p/54c…https://www.slf4j.org/

February 25, 2019 · 1 min · jiezi

学习 python logging(1): 基本用法

简介日志在编程中是十分重要,可以帮助我们跟踪事件、应用的运行情况、查问题、统计数据等。在记录日志时,通常表示某件事情的发生。python 中 logging 模块提供记录的基础方法:debug, info,warning, error, critical这五个方法的严重等级依次增加,对应关系:LEVELvalueused timeDEBUG10分析问题的时候INFO20确定程序是否在按预想的运行WARNING30程序运行超出预设,但是,程序还可以运行ERROR40有严重的错误,程序无法正常运行一些方法CRITICAL50一个严重的错误,导致程序无法继续运行了默认等级为 WARNING,只有高于你所指定的等级,才会被日志模块输出。基本使用直接打印 import logging logging.info(‘info log’) logging.warning(‘warning log’)运行之后看到的是 WARNING:root:warning log ,因为默认等级是 WARNING, 所以 info log 是不会显示的。将日志记录到文件中import logginglogging.basicConfig(filename=‘logging_example.log’,level=logging.DEBUG)logging.debug(‘Write debug to file’)logging.info(‘Write info to file’)logging.warning(‘Write warning to file’)可以在日志文件中看到:DEBUG:root:Write debug to fileINFO:root:Write info to fileWARNING:root:Write warning to fileDEBUG:root:Write debug to fileINFO:root:Write info to fileWARNING:root:Write warning to file参考:https://docs.python.org/3/how…https://docs.python.org/3/lib...https://docs.python.org/3/how…

February 14, 2019 · 1 min · jiezi

Python logging调用Logger.info方法的处理过程

本次分析一下Logger.info的流程1. Logger.info源码: def info(self, msg, *args, **kwargs): """ Log ‘msg % args’ with severity ‘INFO’. To pass exception information, use the keyword argument exc_info with a true value, e.g. logger.info(“Houston, we have a %s”, “interesting problem”, exc_info=1) """ if self.isEnabledFor(INFO): self._log(INFO, msg, args, **kwargs)注释中反应了可以通过 msg和不定参数args来进行日志的格式化。真实的调用为:_log方法:2. Logger._log方法: def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False): """ Low-level logging routine which creates a LogRecord and then calls all the handlers of this logger to handle the record. """ sinfo = None if _srcfile: #IronPython doesn’t track Python frames, so findCaller raises an #exception on some versions of IronPython. We trap it here so that #IronPython can use logging. try: fn, lno, func, sinfo = self.findCaller(stack_info) except ValueError: # pragma: no cover fn, lno, func = “(unknown file)”, 0, “(unknown function)” else: # pragma: no cover fn, lno, func = “(unknown file)”, 0, “(unknown function)” if exc_info: if isinstance(exc_info, BaseException): exc_info = (type(exc_info), exc_info, exc_info.traceback) elif not isinstance(exc_info, tuple): exc_info = sys.exc_info() record = self.makeRecord(self.name, level, fn, lno, msg, args, exc_info, func, extra, sinfo) self.handle(record)最后两行:生成日志记录:record = self.makeRecord(self.name, level, fn, lno, msg, args, exc_info, func, extra, sinfo)处理日志记录self.handle(record)2 生成日志记录: def makeRecord(self, name, level, fn, lno, msg, args, exc_info, func=None, extra=None, sinfo=None): """ A factory method which can be overridden in subclasses to create specialized LogRecords. """ rv = _logRecordFactory(name, level, fn, lno, msg, args, exc_info, func, sinfo) if extra is not None: for key in extra: if (key in [“message”, “asctime”]) or (key in rv.dict): raise KeyError(“Attempt to overwrite %r in LogRecord” % key) rv.dict[key] = extra[key] return rv调用_logRecordFactory初始化一个日志记录实例,_logRecordFactory 其实就是LogRecord类,初始化时,可能包含logger的name, level、调用的函数、行号、日志字符串、模板参数、堆栈信息等。再看extra信息,extra到底有何用?现在从代码中可以看到,只是更新到生成的日志记录实例的__dict__中去.猜测:肯定会在生成最终的日志字符串的时候会用到。继续往下看。3 处理日志记录self.handle(record):Logger继承自Filterer, def handle(self, record): """ Call the handlers for the specified record. This method is used for unpickled records received from a socket, as well as those created locally. Logger-level filtering is applied. """ if (not self.disabled) and self.filter(record): self.callHandlers(record)3.1 if语句中有一self.filter(record)的判断,看函数名,是来筛选是否要继续处理消息的,其核心源码如下: def filter(self, record): """ Determine if a record is loggable by consulting all the filters. The default is to allow the record to be logged; any filter can veto this and the record is then dropped. Returns a zero value if a record is to be dropped, else non-zero. .. versionchanged:: 3.2 Allow filters to be just callables. """ rv = True for f in self.filters: if hasattr(f, ‘filter’): result = f.filter(record) else: result = f(record) # assume callable - will raise if not if not result: rv = False break return rv可以看到, 如果在handler中的filter中如果有返回为False或空,则会屏蔽对应的record,返回True或部位空的值,则会将record放行。那么我们就可以自定义自己的filter。3.2 让Logger中所有的handles去处理record: def callHandlers(self, record): """ Pass a record to all relevant handlers. Loop through all handlers for this logger and its parents in the logger hierarchy. If no handler was found, output a one-off error message to sys.stderr. Stop searching up the hierarchy whenever a logger with the “propagate” attribute set to zero is found - that will be the last logger whose handlers are called. """ c = self found = 0 while c: for hdlr in c.handlers: found = found + 1 if record.levelno >= hdlr.level: hdlr.handle(record) if not c.propagate: c = None #break out else: c = c.parent if (found == 0): if lastResort: if record.levelno >= lastResort.level: lastResort.handle(record) elif raiseExceptions and not self.manager.emittedNoHandlerWarning: sys.stderr.write(“No handlers could be found for logger” " "%s"\n" % self.name) self.manager.emittedNoHandlerWarning = True代码中会去循环调用当前logger的所有handlers去处理record,for循环部分,之后,如果当前的logger的propagate的值为False或空,则不向logger的父logger传递,即向上传递。4. Handler 中的 handler(record) 部分: def handle(self, record): """ Conditionally emit the specified logging record. Emission depends on filters which may have been added to the handler. Wrap the actual emission of the record with acquisition/release of the I/O thread lock. Returns whether the filter passed the record for emission. """ rv = self.filter(record) if rv: self.acquire() try: self.emit(record) finally: self.release() return rv可以看到, Handler在处理record时, 会去加锁,然后调用self.emit(record)方法去处理。4.1 emit(record) def emit(self, record): """ Do whatever it takes to actually log the specified logging record. This version is intended to be implemented by subclasses and so raises a NotImplementedError. """ raise NotImplementedError(’emit must be implemented ’ ‘by Handler subclasses’)看到需要由子类去实现,以StreamHandler为例子: def emit(self, record): """ Emit a record. If a formatter is specified, it is used to format the record. The record is then written to the stream with a trailing newline. If exception information is present, it is formatted using traceback.print_exception and appended to the stream. If the stream has an ’encoding’ attribute, it is used to determine how to do the output to the stream. """ try: msg = self.format(record) stream = self.stream stream.write(msg) stream.write(self.terminator) self.flush() except Exception: self.handleError(record)4.2 Handler.format(record): def format(self, record): """ Format the specified record. If a formatter is set, use it. Otherwise, use the default formatter for the module. """ if self.formatter: fmt = self.formatter else: fmt = _defaultFormatter return fmt.format(record)如果handler有自定义的formatter就用自定义的,如果没有则用默认的Formatter的实例, 初始化元源码为: def init(self, fmt=None, datefmt=None, style=’%’): """ Initialize the formatter with specified format strings. Initialize the formatter either with the specified format string, or a default as described above. Allow for specialized date formatting with the optional datefmt argument (if omitted, you get the ISO8601 format). Use a style parameter of ‘%’, ‘{’ or ‘$’ to specify that you want to use one of %-formatting, :meth:str.format ({}) formatting or :class:string.Template formatting in your format string. .. versionchanged:: 3.2 Added the style parameter. """ if style not in _STYLES: raise ValueError(‘Style must be one of: %s’ % ‘,’.join( _STYLES.keys())) self._style = _STYLES[style]0 self._fmt = self._style._fmt self.datefmt = datefmt有三个参数:fmt: 格式化模板datefmt: 时间格式化参数style: 日志格式化的样式。style有三种:_STYLES = { ‘%’: (PercentStyle, BASIC_FORMAT), ‘{’: (StrFormatStyle, ‘{levelname}:{name}:{message}’), ‘$’: (StringTemplateStyle, ‘${levelname}:${name}:${message}’),可以看出对应到:% 操作符的格式化, format方法的格式化以及Template的格式化。Formatter的format方法源码为: def format(self, record): """ Format the specified record as text. The record’s attribute dictionary is used as the operand to a string formatting operation which yields the returned string. Before formatting the dictionary, a couple of preparatory steps are carried out. The message attribute of the record is computed using LogRecord.getMessage(). If the formatting string uses the time (as determined by a call to usesTime(), formatTime() is called to format the event time. If there is exception information, it is formatted using formatException() and appended to the message. """ record.message = record.getMessage() if self.usesTime(): record.asctime = self.formatTime(record, self.datefmt) s = self.formatMessage(record) if record.exc_info: # Cache the traceback text to avoid converting it multiple times # (it’s constant anyway) if not record.exc_text: record.exc_text = self.formatException(record.exc_info) if record.exc_text: if s[-1:] != “\n”: s = s + “\n” s = s + record.exc_text if record.stack_info: if s[-1:] != “\n”: s = s + “\n” s = s + self.formatStack(record.stack_info)看到会调用record.getMessage(),这里仅仅是获取我们需要的日志信息。之后会调用s = self.formatMessage(record): def formatMessage(self, record): return self._style.format(record)其实是调用了当前style的format方法,以%这一类型为例PercentStyle:class PercentStyle(object): default_format = ‘%(message)s’ asctime_format = ‘%(asctime)s’ asctime_search = ‘%(asctime)’ def init(self, fmt): self._fmt = fmt or self.default_format def usesTime(self): return self._fmt.find(self.asctime_search) >= 0 def format(self, record): return self._fmt % record.__dict__从其中的format方法可以看出,是针对record的__dict__属性中的所有参数进行格式化,这下,就清楚了之前的extra参数是干嘛用的了:可以在formatter中加入自己自定义的一些参数,如固定的用户信息等等。之后,将最终的message flush到对应的Stream里面去就行了,就是整个流程:请大家多多指点。 ...

February 11, 2019 · 5 min · jiezi

如何优雅的在flask中记录log

背景记录日志,在任何项目中,都是很重要的。在Flask项目中,即有Flask提供的logger可以用来记录log,也可以通过直接使用Python的logging模块自定义logger来记录。那么这两者是什么关系,又该怎么使用呢?思路Python的logging模块先看下对于logging模块的官方介绍Loggers have the following attributes and methods. Note that Loggers are never instantiated directly, but always through the module-level function logging.getLogger(name). Multiple calls to getLogger() with the same name will always return a reference to the same Logger object.The name is potentially a period-separated hierarchical value, like foo.bar.baz (though it could also be just plain foo, for example). Loggers that are further down in the hierarchical list are children of loggers higher up in the list. For example, given a logger with a name of foo, loggers with names of foo.bar, foo.bar.baz, and foo.bam are all descendants of foo. The logger name hierarchy is analogous to the Python package hierarchy, and identical to it if you organise your loggers on a per-module basis using the recommended construction logging.getLogger(name). That’s because in a module, name is the module’s name in the Python package namespace.https://docs.python.org/3/lib…上面主要告诉我们两点,可以通过logging.getLogger(name)来获取一个logger,相同名字的logger,其实是同一个logger。logger是通过name进行继承的,比如foo.bar就是foo 的子logger。就可以是实现我们通过配置一个rootLogger,然后直接使用rootLogger.sublogger来记录一下内容,而不需要单独再配置一遍。当使用logging.getLogger(name)时,__name__就是这个模块所在的python package的namespace。flask提供的logger再看下flask中的logging模块:Flask uses standard Python logging. All Flask-related messages are logged under the ‘flask’ logger namespace.Flask.logger returns the logger named ‘flask.app’, and can be used to log messages for your application.Depending on the situation, an extension may choose to log to app.logger or its own named logger. Consult each extension’s documentation for details.http://flask.pocoo.org/docs/1…我们可以知道flask的logger就是一个标准的Python logging,它的命名是flask。我们既可以使用app.logger,也可以自己定义一个logger。那么如何使用app.logger呢?有两种方式:直接调用logger = logging.getLogger(‘flask.app’)logger.info(‘flask.app’)使用Flask提供的接口from flask import current_appcurrent_app.logger.info(’logged by current_app from main’)这里推荐还是使用第二种,current_app是一个单例,可以直接引用到app.logger。通过修改app.logger的name,可以实现子logger的继承么?答案是否定的。修改app.logger的name:# app/init.pyapp.logger.name = ‘app’然后在子模块中定义一个app.module的logger来记录:from flask import current_appimport logginglogger = logging.getLogger(‘app.module’)@module.route(’/test’, methods=[‘GET’])def test(): logger.info(’logged by app.module’) current_app.logger.info(’logged by current_app.logger’)输出结果:2019-02-01 10:56:01,877 - Thread-2 - app - INFO - logged by current_app.logger只有current_app.logger的输出。修改app.logger的name是不是无效呢?我们把子模块中的logger的name修改为flask.app.module:from flask import current_appimport logginglogger = logging.getLogger(‘flask.app.module’)@module.route(’/test’, methods=[‘GET’])def test(): logger.info(’logged by flask.app.module’) current_app.logger.info(’logged by current_app.logger’)输出结果:2019-02-01 11:00:10,944 - Thread-2 - flask.app.module - INFO - logged by flask.app.module2019-02-01 11:00:10,946 - Thread-2 - app - INFO - logged by current_app.logger两个logger均输出了。可见,通过修改app.logger.name可以在记录的时候显示为我们设置的名称,但实际上这个logger还是flask.app。__name__的使用在自定义logger的情况下,为了方便起见,我们可以利用__name__这个参数。前面说到:当使用logging.getLogger(name)时,__name__就是这个模块所在的python package的namespace。一般Flask的工厂模式结构如下:app├── init.py├── main│ ├── init.py│ ├── functions.py│ └── views.py└── module ├── init.py ├── functions.py └── views.py那么我们在先在app.__init__中定义rootLogger,然后再在app.module.functions.py中定义子Logger,均使用logging.getLogger(name):# app.init.py 初始化rootloggerrootLogger = logging.getLogger(name) rootLogger.setLevel(logging.DEBUG) socketHandler = logging.handlers.SocketHandler(’localhost’,logging.handlers.DEFAULT_TCP_LOGGING_PORT) rootLogger.addHandler(socketHandler) rootLogger.setLevel(logging.DEBUG)# app.module.functions.pyimport logginglogger = logging.getLogger(name)def record_from_logging(): logger.info(’logged by logging from name’)输出:2019-02-01 12:18:34,743 - MainThread - app - INFO - register root logger by __name__2019-02-01 12:19:24,954 - Thread-4 - app.module.functions - INFO - logged by logging from name__可以发现输出的logger.name就是所在的文件目录,logger之间的继承关系与整个程序包保持一致。总结根据上面分析,那么怎么优雅的记录logger呢?如果没有对模块进行分logger记录要求的话。可以直接使用在程序初始化的时候配置app.logger(可以自行设置logger.name)。在模块中通过import current_app来记录:# app.init.pydef register_logging(app): app.logger.name = ‘app’ # logstash_handler stashHandler = logstash.LogstashHandler(‘app.config.get(‘ELK_HOST’)’, ‘app.config.get(‘ELK_PORT’)’) app.logger.addHandler(stashHandler) # socket_handler socketHandler = logging.handlers.SocketHandler(’localhost’, logging.handlers.DEFAULT_TCP_LOGGING_PORT) app.logger.addHandler(socketHandler) # app.module.function.pyfrom flask import current_app@module.route(’/test’, methods=[‘GET’])def test(): current_app.logger.info(’logging someting’) return ’logged by current_app.logger’输出效果:2019-02-01 13:49:28,998 - Thread-2 - app - INFO - logged by current_app from main2019-02-01 13:49:38,346 - Thread-3 - app - INFO - logged by current_app of functions__注意: 对于current_app.logger的引用不能通过如下方式,会有RuntimeError的报错。from flask import current_applogger = current_app.logger## 异常 raise RuntimeError(_app_ctx_err_msg)RuntimeError: Working outside of application context.This typically means that you attempted to use functionality that neededto interface with the current application object in some way. To solvethis, set up an application context with app.app_context(). See thedocumentation for more information.如果希望按自己的实际需求,对模块进行分logger记录要求的话。那么建议自己设置logger。# app.init.pydef register_logging(): # set own root logger rootLogger = logging.getLogger(name) rootLogger.setLevel(logging.DEBUG) # socketHandler socketHandler = logging.handlers.SocketHandler(’localhost’,logging.handlers.DEFAULT_TCP_LOGGING_PORT) rootLogger.addHandler(socketHandler) # logstash_handler stashHandler = logstash.LogstashHandler(‘app.config.get(‘ELK_HOST’)’, ‘app.config.get(‘ELK_PORT’)’) rootLogger.addHandler(stashHandler) rootLogger.setLevel(logging.DEBUG)# app.module.function.pyimport logginglogger = logging.getLogger(name)@module.route(’/test’, methods=[‘GET’])def test(): logger.info(’logging someting’) return ’logged by logging module’输出效果:2019-02-01 13:49:49,297 - Thread-5 - app.module.views - INFO - logged by flask.app.module2019-02-01 13:50:01,013 - Thread-7 - app.module.functions - INFO - logged by logging module of functions完整代码可参考:https://github.com/keejo125/flask_logging_demo注意关于python中logging的配置可参考官网:https://docs.python.org/3/lib…在配置handler时,经常会希望日志可以按时间分割(TimedRotatingFileHandler)或者按大小分割(RotatingFileHandler).但是在flask项目中,尤其开启多线程之后,在分割日志(doRollover())时会有文件读写的异常:WindowsError: [Error 32]建议使用SocketHandler,将日志发送给单独的LogServer来进行二次处理。简易的接收socketlog的LogServer可参考:https://github.com/keejo125/f…或者现在流行的stashHandler,将日志发送给ELK来进行二次处理。 ...

February 1, 2019 · 3 min · jiezi

python日志配置

先来一段程序告知python怎样配置日志。logging.basicConfig(level=logging.DEBUG) # 设置日志级别# 创建日志记录器,指明日志保存的路径、每个日志文件的最大大小、保存的日志文件个数上限file_log_handler = RotatingFileHandler(“logs/log”, maxBytes=1024 * 1024 * 100, backupCount=10)# 创建日志记录的格式 日志等级 输入日志信息的文件名 行数 日志信息formatter = logging.Formatter(’%(levelname)s %(filename)s:%(lineno)d %(message)s’)# 为刚创建的日志记录器设置日志记录格式file_log_handler.setFormatter(formatter)# 为全局的日志工具对象(flask app使用的)添加日志记录器logging.getLogger().addHandler(file_log_handler)分析代码:basicConfig():对日志系统进行一次性配置,以下是可以指定的关键字参数 filename:指定日志输出目标文件的文件名,指定该设置项后日志信心就不会被输出到控制台了 filemode:指定日志文件的打开模式,默认为’a’。需要注意的是,该选项要在filename指定时才有效 format:指定日志格式字符串,即指定日志输出时所包含的字段信息以及它们的顺序。logging模块定义的格式字段下面会列出。 datefmt:指定日期/时间格式。需要注意的是,该选项要在format中包含时间字段%(asctime)s时才有效 level:指定日志器的日志级别 stream:指定日志输出目标stream,如sys.stdout、sys.stderr以及网络stream。需要说明的是,stream和filename不能同时提供,否则会引发 ValueError异常 handlers:Python 3.3中新添加的配置项。该选项如果被指定,它应该是一个创建了多个Handler的可迭代对象,这些handler将会被添加到root logger。需要说明的是:filename、stream和handlers这三个配置项只能有一个存在,不能同时出现2个或3个,否则会引发ValueError异常。 RotatingFileHandler():轮转文件处理器,将日志消息发送到磁盘文件,并支持日志文件按大小切割FileHandler():将日志消息发送到磁盘文件,默认情况下文件大小会无限增长StreamHandler():将日志消息发送到输出到Stream,如std.out, std.err或任何file-like对象。Formatter():格式器,决定日志记录的最终输出格式 %(levelname)s:日志记录的级别 %(message)s:日志记录的内容 %(asctime)s:日志记录的时间 %(pathname)s:调用日志记录文件的全路径 %(filename)s:pathname的文件名部分 %(module)s:pathname的文件名,不包含后缀 %(funcName)s:调用日志记录文件的函数名getLogger():创建一个Logger对象 addHandler():添加一个处理器日志级别以下是由低到高,默认设置等级后不显示低等级的消息DEBUGINFOWARNINGERRORCRITICAL(很少使用)参考文档:https://www.cnblogs.com/yyds/…

January 1, 2019 · 1 min · jiezi