Python日志教程

本文翻译自logging howto

基础教程

日志是跟踪软件运行时发生事件的一种手段。Python开发者在代码中调用logging模块的方法,用于记录某些事件已经发生。事件一般是描述性信息,其中可能会包含一些数据。事件中也包含了开发者对事件的重视程度;重视程度也可以称为级别或者严重性。

记录时机

logging模块提供了一组纪录日志的功能,有debug(), info(), warning(),error()critical()。下表说明了这些功能的使用场景。

任务 该使用的功能
显示控制台输出,用于一般性的命令行脚本和程序 print()
报告程序正常运行期间发生的事件 logging.info()或者logging.debug()
特定运行事件的警告 如果问题是可以避免的,并且应该修改客户端应用程序以消除警告,使用warnings.warn();
客户端不需要作出改变,但是事件仍然应该注意,使用logging.warning()
报告运行时的错误 抛出异常
非异常形式报告运行时的错误 logging.error()logging.exception()或者logging.critical()

logging模块中的函数根据事件的严重性命名,如下表。

级别 使用时机
DEBUG 获取详细信息,通常时诊断问题时
INFO 确认程序按预期工作
WARNING 表明发生了一些意外的事情,或者用来指示在不久的将来会发生的问题。软件依然如预期工作
ERROR 因为更严重的问题,软件无法执行某些功能
CRITICAL 一个严重的问题,表示程序本身可能无法继续运行下去

默认的级别是WARNING,这意味着除非logging模块有其他的配置,否则只记录此级别或以上级别的事件。

记录的事件可以用不同的方式处理。最简单的方法是将这些事件打印到控制台。另外一个常用的方法是将事件记录到文件中。

简单的例子

一个非常简单的例子如下:

import logging
logging.warning('Watch out!') # 控制台打印
logging.info('I told you so') # 不会打印任何信息

如果运行上面的脚本,会在控制台输出如下信息:

WARNING:root:Watch out!

注意INFO级别信息没有打印出来,因为默认的级别是WARNING。打印的消息包括日志级别和描述的信息。先不用管其中的"root"信息,后面会解释。如果需要的话,可以配置输出格式;格式也会在后面提到。

写入文件

将日志写入文件是非常常见的场景,请看下面的例子。一定要在新的Python解释器中运行下面的脚本:

import logging
logging.basicConfig(filename='example.log', level=logging.DEBUG)
logging.debug('This message should go to the log file')
logging.info('So should this')
logging.warning('And this, too')

如果打开这个文件,会看到下面的信息:

DEBUG:root:This message should go to the log file
INFO:root:So should this
WARNING:root:And this, too

这个例子还显示了如何设置日志的级别。由于级别设置为DEBUG,所有的信息都会记录下来。

如果想要通过类似--log=INFO命令行参数设置日志级别,并且将传入的级别存入loglevel变量,然后通过getattr(logging, loglevel.upper())获取级别变量,并传入basicConfig(),可以通过下面的代码对参数进行校验:

numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numberic_level, int):
    raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level, ...)

应该在debug(), info()等方法之前调用basicConfig()。因为它是一次性的配置,只有第一次的调用是有效的。

如果多次运行上面的脚本,日志将会追加到文件example.log中。可以通过修改filemode参数修改这一行为。

logging.basicConfig(filename='example.log', filemode='w', level=logging.DEBUG)

输出和之前是一样的,但是每次运行会丢失之前的消息。

多模块日志

如果程序由多个模块组成,可以参考下面的例子组织日志

# myapp.py
import logging
import mylib

def main():
    logging.basicConfig(filename='myapp.log', level=logging.INFO)
    logging.info('Started')
    mylib.do_something()
    logging.info('Finished')
    
if __name__ == '__main__':
    main()
# mylib.py
import logging

def do_something():
    logging.info('Doing something')

运行myapp.py,将会在myapp.log中看到:

INFO:root:Started
INFO:root:Doing something
INFO:root:Finished

可以通过类似的模式将其推广到多个模块。不过,对于这种简单的使用,只能通过查看事件描述来判断日志的来源。

记录变量

要记录变量数据,需要使用事件描述消息的格式,并附加变量作为参数。比如:

import logging
logging.warning('%s before you %s', 'Look', 'leap!')

会显示

WARNING:root:Look before you leap!

可以看到,使用的是%风格的字符串格式。这是为了向后兼容。logging模块也支持更新的格式化选项,但是探索这些不在本教程的范围之内。

改变日志格式

要改变日志格式,需要指定新的格式:

import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should appear on the console')
logging.info('So should this')
logging.warning('And this, too')

上面的脚本会打印:

DEBUG:This message should appear on the console
INFO:So should this
WARNING:And this, too

请注意,前面例子中的"root"已经消失了。完整的格式说明见LogRecord attribute,但是对于简单的应用来说,只需要记录事件级别,信息和时间就足够了。

消息中显示时间信息

要展示事件的时间,可以在格式串中加入'%(asctime)s':

import logging
logging.basicConfig(format='%(asctime)s %(message)s')
logging.warning('is when this event was logged.')

上面的脚本会打印

2010-12-12 11:41:42,612 is when this event was logged.

默认展示的时间格式是ISO8601。如果需要更多的控制,可以通过传递datafmt参数给basicConfig来实现,比如:

import logging
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.warning('is when this event was logged.')

上面的脚本会打印以下信息:

12/12/2010 11:46:36 AM is when this event was logged.

参数datefmt支持的格式time.strftime()也支持。

后续教程

以上就是logging模块的基础教程,掌握上面的内容就可以进行常规日志记录。logging模块提供了更多的功能,想要充分利用它,请看下面的章节。

进阶教程

logging模块采用模块化的设计,并提供了几类组件:记录器,处理器,过滤器和格式器。

  • 记录器提供应用程序代码使用的接口。
  • 处理器将记录器生成的记录发送到相应的位置。
  • 过滤器对记录进行过滤,决定哪些日志会被输出。
  • 格式器决定输出的格式。

日志事件以LogRecord实例的形式在记录器,处理器,过滤器和格式器之间传递。

一般通过调用Logger实例(下面简称为记录器)中的方法来记录日志。每个实例都有名字,这些名字在命名空间层级中由点连接。比如,名为'scan'的记录器是'scan.text','scan.html'和'scan.pdf'的父记录器。记录器的名字可以任意取。

一个命名记录器的良好习惯是使用模块名,比如下面:

logger = logging.getLogger(__name__)

使用这种命名可以记录模块的继承关系。

记录器层次结构的根被称之为根记录器。函数debug(), info(), warning(), error(), critical()其实调用的就是根记录器中同名的方法。这些函数和方法记录的日志中,记录器名字为'root'。

可以将不同的日志信息记录到不同的地方。logging模块支持将日志写入多个目的位置,其中包括文件、HTTP GET/POST地址、邮件(SMTP协议)、通用socket、操作系统日志机制(比如syslog和Windows NT的事件记录)。目的位置由处理器来指定。可以根据需求构造自己的处理器类。

默认情况下,任何日志都不设置目的地。可以通过basicConfig()指定一个目的地。debug(), info(), warning(), error()critical()会在被调用时检查是否设置了目的地;如果没有设置,则在将消息委托给根记录器之前,将目的器设置为控制台(sys.stderr),并使用默认格式输出消息。

basicConfig()的默认设置是:

日志级别:记录器名字:消息

日志流

记录器和处理器对日志事件的处理流程如下图:

记录器

Logger对象有三个功能。第一,提供接口给应用程序代码,应用程序代码在运行时通过调用这些接口记录日志。第二,根据设置的日志等级(默认过滤规则)或者过滤器来确定输出的日志消息。第三,将日志消息传递给所有与之相关的处理器。

记录器对象中使用最广泛的方法分为两类:配置和发送消息。

下面是最常用的配置方法:

不需要每次创建记录器时都调用这些方法。

配置好记录器对象后,可以通过下面的方法创建日志消息:

  • Logger.debug(), Logger.info(), Logger.warning(), Logger.error()Logger.critical()都用来创建和方法名对应级别的日志记录。消息实际上是格式字符串,可能包含标准字符串替换语法%s, %d, %f等等。其余的参数是消息中的替换字段对应的对象列表。这些方法只关心**kwargs中的exc_info关键字,并使用它来确定是否记录异常信息。
  • Logger.exception()Logger.error()差不多。不过Logger.exception()会转储堆栈跟踪信息。只能在异常处理中调用这个方法。
  • Logger.log()将日志级别作为显式参数。和上面列出的方法相比,这个方法有点啰嗦,不过可以使用该方法记录自定义级别日志。

getLogger()返回具有指定名字的记录器的引用,如果没有指定名字,则返回root。用'.'连接命名空间的层次构成了这些名字。使用同样的名字多次调用getLogger()会返回指向同一记录器的引用。分层列表中位置靠下的记录器是位置靠上的记录器的子记录器。比如,对名字为foo的记录器来说,名字为foo.bar, foo.bar.bazfoo.bam的记录器是它的子记录器。

记录器有有效级别的概念。如果没有显式设置级别,则使用父记录器的级别作为其有效级别。如果父记录器也没有设置,则一直向上寻找,直到找到显式设置的级别。根记录器始终具有显式级别设置(默认为WARNING)。在决定是否处理事件时,使用记录器的有效级别来确定是否将事件传递给处理器。

子记录器将消息传播到与其祖先记录器相关联的处理器。因此,无需为所有的日志记录器定义和配置处理器。为顶级记录器配置处理器,并根据需要创建子记录器就足够了。(不过,也可以将propagate属性设置为False来关闭这一行为)。

处理器

处理器负责将适当的日志消息(基于日志的级别)分派到指定的目标。Logger对象可以通过使用addHandler()方法添加零或者更多的处理器对象。比如,应用程序可能希望将所有的日志消息发送到日志文件,error及更高级别的日志输出到stdout,critical级别的日志通过邮件告警。这个场景需要三个单独的处理器,其中每个处理器负责将特定级别的消息发送到特定位置。

标准库包含相当多不同类型的处理器(参阅Useful Handlers)。

开发者需要关心的处理器方法很少。使用内置处理器需要关心以下的配置方法:

  • setLevel()方法,和记录器中的同名方法一样,这个方法设置日志能分派到目的地的最低级别。
  • setFormatter()设置处理器使用的格式器。
  • addFilter()removeFilter()分别用于在配置中添加和移除过滤器对象。

应用代码不应该直接初始化并使用Handler类的实例。Handler类是一个基类,用于定义所有处理器应具有的接口,并定义一些子类可以使用(或覆盖)的默认行为。

格式器

格式器配置日志消息显示的最终顺序,结构和内容。和基础的logging.Handler类不同,应用程序代码可以实例化格式器类。如果应用程序需要特殊行为,可以自定义子类。构造函数使用两个可选参数--消息格式字符串和日期格式字符串。

logging.Formatter.__init__(fmt=None, datefmt=None)

如果没有消息格式字符串,默认使用原始消息。如果没有日期格式字符串,默认的格式为:

%Y-%m-%d %H:%M:%S

下面的消息格式字符串记录的顺序为:人类可读的时间格式,消息的级别,消息的内容。

'%(asctime)s - %(levelname)s - %(message)s'

配置

程序员可以通过三种方式配置日志记录:

  • 在代码中显示创建记录器,处理器和格式器
  • 创建配置文件,并用fileConfig()函数读取
  • 创建配置字典,并传入dictConfig()方法。

后两种方法的文档见Configuration functions。下面的示例使用Python代码配置了一个非常简单的记录器,一个控制台处理器和一个简单的格式器:

# coding=utf8
import logging

# 创建记录器
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)

# 创建控制台处理器,将级别设置为DEBUG
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# 创建格式器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# 处理器中添加格式器
ch.setFormatter(formatter)

# 记录器中添加处理器
logger.addHandler(ch)

# 应用代码
logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')

运行上面的脚本会产生如下输出:

$ python simple_logging.module.py 
2017-04-25 15:59:13,321 - simple_example - DEBUG - debug message
2017-04-25 15:59:13,321 - simple_example - INFO - info message
2017-04-25 15:59:13,321 - simple_example - WARNING - warn message
2017-04-25 15:59:13,321 - simple_example - ERROR - error message
2017-04-25 15:59:13,321 - simple_example - CRITICAL - critical message

下面的Python模块创建一个记录器,处理器和格式器,除了对象名字和上面例子不同外,其他都一样:

import logging
import logging.config

logging.config.fileConfig('logging.conf')

logger = logging.getLogger('simpleExample')

logger.debug('debug message')
logger.info('info message')
logger.warn('warn message')
logger.error('error message')
logger.critical('critical message')

logging.conf文件如下:

[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
datefmt=

产生的输出和上面例子相同:

$ python simple_logging_config.py 
2017-04-25 16:08:46,631 - simpleExample - DEBUG - debug message
2017-04-25 16:08:46,631 - simpleExample - INFO - info message
2017-04-25 16:08:46,631 - simpleExample - WARNING - warn message
2017-04-25 16:08:46,631 - simpleExample - ERROR - error message
2017-04-25 16:08:46,631 - simpleExample - CRITICAL - critical message

可以看到,相比使用Python代码,配置文件有隔离配置与代码和易于修改这些优点。

没有配置的情况

如果没有配置日志,可能会出现需要输出日志但是没有处理器的情况。在这种情况下,logging模块的行为取决于Python版本。

对于Python2.x版本,行为如下:

  • 如果logging.raiseExceptionsFalse(生产模式),日志事件被丢弃。
  • 如果logging.raiseExceptionsTrue(开发模式),会打印消息'No handlers could be found for logger X.Y.Z'。

为库配置日志

在开发的库中记录日志时,应该留下文档,注明记录日志的方式--比如所使用记录器的名字。同时也需要考虑日志的配置。如果应用程序没有配置日志,而库代码又调用了logging接口,那么一条错误消息将被打印到sys.stderr

如果不希望打印这条消息,可以在库的顶层记录器中添加一个空处理器。日志事件总是会找到处理器,而空处理器不会产生任何输出,因此可以避免打印错误消息。如果库的使用者在应用中配置了日志并且添加了处理器,那么库中的日志调用将会被发送到这些处理器。

logging模块包块了空处理器:NullHandler(Python2.7版本或以上)。如果要在不配置日志的情况下阻止错误信息发送到sys.stderr,可以将该处理器实例添加到命名空间中的顶层记录器中。如果foo库的记录器名为'foo.x','foo.x.y'之类的话,那么下面的代码

import logging
logging.getLogger('foo').addHandler(logging.NullHandler())

将会有预期的效果。如果组织生成多个库,则指定记录器的名字应该是'orgname.foo'。

日志级别

日志级别的数值如下表所示。如果要自定义级别,并且需要自定义级别具有特定值,那么需要关注这些。如果自定义的级别和预定义级别有相同的数值,它将覆盖预定义的值;预定义的名字将会丢失。

名字 数值
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
NOTSET 0

级别还可以和记录器相关联,由开发人员设置或者通过加载保存的日志记录配置。如果记录器的日志级别高于方法调用的级别,则不会生成日志消息。这是控制日志输出的基本机制。

日志消息封装在LogRecord对象中。当记录器决定记录事件时,会创建一个LogRecord实例。

消息由处理器(Handler子类)来调度处理。处理器负责确保日志(以LogRecord的形式)位于特定位置(或一组位置),这对于该日志的目标受众(例如终端用户,支持人员,系统管理员,开发人员)是很有用的。处理器被用于将LogRecord传递到特定位置。每个记录器都可以有零个,一个或多个与它相关联的处理器(通过LoggeraddHandler()方法)。除了与记录器直接关联的处理器外,还会调用与该记录器的祖先记录器关联的所有处理器来分派消息(除非记录器的传播标识设置为false值)。

就像记录器一样,处理器可以具有与它们相关联的级别。处理器的级别和记录器的级别都用作过滤。如果处理器决定调度事件,emit()方法会将消息发送到其目标。绝大多数Handler的子类需要重写emit()方法。

自定义级别

可以自定义日志级别,但是没有必要那样做,因为现有的级别都是根据实际经验选择的。不过,如果确实需要自定义级别,那么在操作的时候应该特别小心,如果正在开发一个库,自定义级别可能是一个非常糟糕的主意。这是因为如果多个库作者都自定义级别,那么这种多个库的记录输出可能会一起使用,这对开发人员来说是非常难控制和解释的,因为给定的数值在不同的库中可能有不同的意义。

有用的处理器

除了基类Handler,logging模块也提供了很多的子类:

  • StreamHandler实例将消息发送到流(类文件对象)。
  • FileHandler实例将消息发送到磁盘文件。
  • BaseRotatingHandler是处理器的基类。一般不直接使用这个类。而是使用RotatingFileHandler或者TimedRotatingFileHandler
  • RotatingFileHandler实例将日志发送到磁盘文件,同时支持最大文件大小和日志文件轮转。
  • TimedRotatingFileHandler实例将日志发送到磁盘文件,并在给定时间间隔内轮转日志文件。
  • SocketHandler实例通过TCP/IP套接字发送日志。
  • DatagramHandler实例通过UDP套接字发送日志。
  • SMTPHandler实例将日志发送到指定的电子邮件地址。
  • SysLogHandler实例将日志发送到Unix syslog守护程序中,这个守护程序可能在远程计算机上。
  • NTEventLogHandler实例将日志发送到Windows NT/2000/XP的日志事件中。
  • MemoryHandler实例将日志发送到内存缓冲区中,满足特定条件会刷新该缓冲区。
  • HTTPHandler实例使用GET或者POST语意向HTTP服务器发送日志。
  • WatchedFileHandler实例监视正在记录日志的文件。如果文件有了更改,则使用文件名关闭并重新打开。此处理器仅适用于类Unix系统;Windows不支持使用这样的机制。
  • NullHandler实例对错误消息什么都不做。当库的开发者想使用logging,但是想避免'No handlers could be found for logger XXX'时,可以在记录器上添加这个处理器。

NullHandler在Python2.7中加入。

NullHandlerStreamHandlerFileHandler在logging核心部分中定义。其他的处理器在logging.handlers这个字模块中定义。

日志在通过Formatter类的格式化后进行展示。

可以使用BufferingFormatter批量格式化多个消息。除了格式化字符串外,还提供了处理头部和尾部字符串格式的方法。

当基于日志级别的过滤满足不了需求时,可以通过addFilter()方法为Logger实例添加Filter实例。在决定进一步处理消息前,记录器和处理器都会查阅所有过滤器的权限。如果任何过滤器返回false值,则不会进一步处理该消息。

Filter的基本功能允许通过特定名字的记录器进行过滤。如果使用此功能,则只有指定名字的记录器和其子孙的消息将通过,其他的消息将被丢弃。

异常处理

设计上,logging模块在生产模式下不会抛出异常。这样的好处是,在logging模块出现配置错误,网络或者其他类似的错误时,不会导致应用程序过早终止。

SystemExitKeyboardInterrupt会正常抛出。其他Handler子类的emit()方法抛出的异常会传递给handleError()方法。

handleError()的默认实现会检查raiseExceptions变量是否设置。如果设置,栈回溯信息将打印到sys.stderr。如果没有设置,则不会抛出异常。

记录任意对象

在前面的例子中,假定了记录事件时传递的消息是一个字符串。但是,这不是唯一的可能性。可以将任意对象作为消息传递,当日志系统需要将其转换成字符串表示时,将调用对象的__str__()方法。

优化

消息将会最后被格式化输出。如果记录器只是丢弃事件,那么调用带参数的logging方法开销可能会很大。要避免这样的开销,可以使用isEnabledFor()方法。该方法接受级别作为参数,如果记录器创建该级别的参数,则返回true。可以通过下面的方式调用:

if logger.isEnabledFor(logging.DEBUG):
    logger.debug('Message with %s, %s', expensive_func1(), expensive_func2())

当记录器的级别设置为DEBUG时,expensive_func1expensive_func2不会被调用。

对于需要对采集日志有更精确控制的应用程序,还有其他的优化策略。对于一些开发者可能不需要的信息,下表给出了一些优化建议:

不想采集的信息 优化策略
不想知道调用的来源 logging._srcfile设置为None。这个配置会避免sys._getframe()被调用,在PyPy环境下运行的代码会更快
线程信息 logging.logThreads设置为0
进程信息 logging.logProcesses设置为0
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,706评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,002评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,462评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,375评论 0 216
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,763评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,849评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,033评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,768评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,490评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,734评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,204评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,566评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,227评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,137评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,934评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,926评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,774评论 2 274

推荐阅读更多精彩内容

  • 本文参考自:https://www.cnblogs.com/Nicholas0707/p/9021672.html...
    handsomePeng阅读 1,346评论 1 4
  • (一)、日志相关概念 1、日志的作用 通过log的分析,可以方便用户了解系统或软件、应用的运行情况;如果你的应用l...
    Rainy丶Wang阅读 577评论 0 0
  • logging模块 日志基础 日志级别(从低到高): logging.NOTSET:不设置 loging.debu...
    jxvl假装阅读 328评论 0 0
  • 在应用程序中添加日志记录总的来说基于三个目的:监视代码中变量的变化情况,周期性的记录到文件中供其他应用进行统计分析...
    时待吾阅读 4,911评论 0 6
  • 经济学上有一个投资乘数原理:如果一个人投资工业,用20万元购买设备和雇佣工人,这20万元中的一部分就流入了他雇佣的...
    百科铭说阅读 1,301评论 0 2