Logging 日志记录最佳实践

红薯 发布于 2012/03/15 17:12
阅读 21K+
收藏 226

介绍

本文解释应用程序中的错误和调试日志的最佳实践。应用日志是一个非常重要的“工具”用来帮你查明具问题。如果有一个基础架构用来记录应用中的日志和事件,那么调试和问题的解决就会变得很简单。

在开始文章之前,我们要介绍两个相关的工具:

 

不同的日志含义

日志 Logging 可以代表不同的含义,有着不同的目的,在这篇文章中主要指应用程序的日志。

应用日志,或者也叫做调试信息和错误日志,主要的目的是为了跟踪应用程序的底层行为,跟踪应用内部所执行的过程。还有另外一些日志,例如是 Google 的 Analytics 是用来统计网站的访问量等信息,这二者的目的是决然不同的。

为什么要做日志

审计跟踪

但错误发生时,你需要清除知道该如何处理,通过对日志跟踪,你可以获取该错误发生的具体环境,你需要确切知道什么是什么引起该错误,什么对该错误不会造成影响。

跟踪应用的警告和错误

为了识别错误,我们将日志分为警告和错误信息,这些都是可以跟踪并予以解决的。例如在 PHP 中 deprecated、strict 和 notices 类别的信息是需要引起注意的。

跟踪崩溃bug

在开发过程中,日志可以帮助开发者和软件测试人员跟踪程序崩溃的原因。

跟踪性能下降的问题范围

产品所反映出来的性能问题,很难在开发过程中暴露出来,这需要进行全方位的测试跟踪,而通过日志提供的详细执行时间记录可以很方便的找出应用的性能瓶颈。

一些日志实例

日志的内容和格式并没有人做严格的规定,但下面是我们给出的一些建议以供参考:

  • 日志发生的日期和时间,包含时区信息和毫秒,建议使用 ISO-8601 时间格式
  • 日志级别,例如 debug、info 和 error
  • 相应的会话标识,能知道是哪个客户端或者是其他请求所产生的日志,例如在 Web 日志中 IP 地址就可以用来标识一个客户端
  • 一个有用的日志内容

下面是一个好日志的例子

[2012-03-02T20:20:49.003+01:00][43gg84][info] Bootstrapping application (v 2.1b)

这条日志中包含:

  1. 日期、时间,提供毫秒信息和时区
  2. 会话标识 43gg84,不同的客户端会有不同的值
  3. 日志级别,在这里是 info
  4. 日志详情:Bootstrapping application
通过这条日志我们就可以知道应用程序在什么时候启动了,这是一个非常重要的时间点。

下面是一个不好的日志例子,光知道错误,不知道发生什么错误:
[Thu, 01 Jan 1970 00:00:00 +0100] Error: ""

(这是我在实际中遇见过的日志,真实的问题是 Beanstalkd 服务器没启动导致的,但这个错误信息中完全没有提及。)

一旦发生错误,使用堆栈的跟踪是非常有用的,当然这也是一个巨大的开销,因此要谨慎使用,到处抛出异常并不是一个好主意。

一个成功启动过程的日志信息

让我们继续看一些好的例子,下面日志包含了应用程序启动完整过程:

[2012-03-02T20:20:49.003+02:00][43gg84][info] Bootstrapping application (v 2.1b)
[2012-03-02T20:20:49.013+02:00][43gg84][info] Request cycle startup
[2012-03-02T20:20:49.093+02:00][43gg84][info] User session found, user ID: 13414564
[2012-03-02T20:20:49.123+02:00][43gg84][info] Requested URI '/fu/bar/index'
[2012-03-02T20:20:49.133+02:00][43gg84][info] Requested forwarded to URI '/fu/bar/overview'
[2012-03-02T20:20:49.273+02:00][43gg84][info] Sending HTTP headers
[2012-03-02T20:20:49.283+02:00][43gg84][info] Sending HTTP Body
[2012-03-02T20:20:49.313+02:00][43gg84][info] Request cycle shutdown
[2012-03-02T20:20:49.323+02:00][43gg84][info] Request cycle completed in 0.320 seconds.

这个日志自我解释的非常清楚,你能知道当前应用的版本,URI 处理等详细信息。

一个包含堆栈信息的日志

下面例子展示了如何输出日志堆栈,你可以看到堆栈信息的级别和错误信息是不同的,当然这不是固定的,有时候发生错误的时候,你需要记录详细的错误堆栈信息以便跟踪错误发生的具体位置:

[2012-03-02T20:20:49.003+02:00][43gg84][info] Bootstrapping application (v 2.1b)
[2012-03-02T20:20:49.013+02:00][43gg84][info] Request cycle startup
[2012-03-02T20:20:49.123+02:00][43gg84][info] Requested URI '/fu/bar/index'
[2012-03-02T20:20:49.273+02:00][43gg84][info] Sending HTTP headers
[2012-03-02T20:20:49.283+02:00][43gg84][erro] Cannot modify header information – headers already sent by X on line Y
[2012-03-02T20:20:49.293+02:00][43gg84][dbug] Stack trace:
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]   1. {main}() /tmp/fu/bar/httpdocs/index.php:0
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]   2. Zend_Application->run() /tmp/fu/bar/httpdocs/index.php:31
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]   3. Zend_Application_Bootstrap_Bootstrap->run() /tmp/fu/bar/library/Zend/Application.php:366
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]   4. Zend_Controller_Front->dispatch() /tmp/fu/bar/library/Zend/Application/Bootstrap/Bootstrap.php:97
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]   5. Zend_Controller_Dispatcher_Standard->dispatch() /tmp/fu/bar/library/Zend/Controller/Front.php:954
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]   6. Zend_Controller_Action->dispatch() /tmp/fu/bar/library/Zend/Controller/Dispatcher/Standard.php:295
[2012-03-02T20:20:49.293+02:00][43gg84][dbug]   7. IndexController->indexAction() /tmp/fu/bar/library/Zend/Controller/Action.php:513
[2012-03-02T20:20:49.313+02:00][43gg84][info] Sending HTTP Body
[2012-03-02T20:20:49.323+02:00][43gg84][info] Request cycle shutdown
[2012-03-02T20:20:49.333+02:00][43gg84][info] Request cycle completed in 0.330 seconds.

几乎每个 PHP 开发者都会碰到这个 "headers sent" 的错误,在这里堆栈跟踪信息很小,当你使用 debug_backtrace() 方法可以指定不输出对象信息(传递 FALSE 做参数)。不过对象信息提供了非常有用的信息,但是它也会因此而生成了更多的噪音来干挠你对日志的阅读。如果对象的调试正确,那其实就没必要输出对象信息了。

要记录的重要事件

并不是每个事件都有记录的必要,我们将在下一小节里描述这个问题,在这里你需要了解的是日志应该记录什么。

完成某项操作所需的时间

通过它可以跟踪为什么系统响应变慢或者太快

  • 完成一个请求处理所需的时间,精确到毫秒
  • 什么时候接收到的请求
  • 执行数据库查询的时间
  • 从磁盘或者存储系统获取数据的时间
  • 等等

异常和堆栈跟踪

异常信息和堆栈跟踪信息是很有价值的,但要处理好相当不易,特别是当你通过重新抛出异常来控制应用流程。在异常发生时记录详细的堆栈跟踪信息对问题的确定是很有帮助的。

会话

知道一个问题是由谁引起的非常重要,因此在日志中使用会话标识符就变得必不可少。它可以简单到是一个 IP 地址或者是一个跟复杂的 UUID,只要能区分不同的用户就足够。IP 地址有时候不能确认问题,因为很多公司都会使用同一个 IP 上互联网。

版本号

我经常碰到这样的问题,一个 bug 在服务器 A 上没事,但在服务器 B 上就经常出现,原因肯定就是版本的差别导致的,因此在日志中你应该记录当前软件的版本号。

哪些内容无需记录

日志是用来标识问题的好方法,但不可过度使用,不要为了日志而日志。太多的日志也会为查找问题带来困难,只应该继续应用的关键信息,当然这个“关键”很难去量化,你必须依照自身系统的情况来决定。

不要包含一些不相干的信息,我看过很多系统把所有的环境变量都记录到日志中,包括整个请求的 dump 等等,这些东西没有任何帮助。

当然,哪些内容无需记录你可以在监控过程中不断的优化。

日志记录后该干什么?

如果你还在阅读此文,我非常感动 :) 因为这篇文章可能真的很枯燥

但你记录了很多日志信息,这些信息包含大量有用的而且是更新的信息,你必须监控它们,并从中找出存在的问题或者潜在的问题予以解决,那么这些日志就成了无用而且占据着大量的系统资源。

异常处理

异常发生时必须详细的进行记录。在异常对象中创建一个日志信息在语义上是不正确的。一些逻辑和函数不应该放在异常内部去定义。但一个异常被重新抛出时,你应该记录这个异常的详细信息,包括:堆栈跟踪、错误码和描述。

异常不应该改变应用的流程,只是用来指示问题的发生。

下面的代码很显然只有开发者自己才能知道什么情况是异常,什么情况不是。下面这种情况我倾向于当没找到用户时不要抛出异常。

// ..

// Fetching the user by the received ID.
try {
    $user = $service->findById( $id );
} catch (SpecificServiceException $e) {
    // Only the specific exception "SpecificServiceException" is being caught here.
    // Other possible exceptions are caught later, so they are not logged here.
    $this->getLogger()->logError( $e->getMessage() );
}


// Sorry, no data found
if ( ! $user instanceof User) {
    return false;
}

// ..

处理 PHP 错误

但发生什么问题时,PHP 会报错,从 E_FATAL 到 E_STRICT,或者是用户自定义的 E_USER_*. 你也可以通过自定义错误处理起来捕获这些错误,例如在 php.ini 中通过 error_log = syslog 来将日志写到 syslog 中。

在所有环境中,除了开发环境,display_errors 和相关的设置都应该关闭,而在开发环境中必须打开,error_reporting 应该设置为包含所有项。

贴士: 在 PHP 运行环境之外配置 PHP 的错误报告时,例如 Apache或者是SHELL脚本,应使用整数值,这在PHP文档中是没有说明的,例如 error_reporting 设置为 -1 标识 E_ALL。

处理 E_STRICT 和 E_DEPRECATED

在 PHP 中有很多日志级别,在开发过程中 E_STRICT 和 E_DEPRECATED 很重要,但没有任何理由在产品环境中记录这个信息。

最后一点是应该在开发过程中解决所有 E_STRICT violation 的问题。

处理 E_NOTICE

Notices 相当于 PHP 的 Achilles' heel (不知道该怎么翻译). 它们并不被记录,但能指示应用程序的健康状态。如果你收到一个 notice 信息,说明开发过程并不顺利,可能会有不少潜在问题。你应该重视 E_NOTICE ,把它们当成是错误来对待,是需要修复的问题。

处理 @ 操作符

错误控制操作符 ("@") 是不推荐使用的,我经常看到很多偷懒的开发者使用它,强烈建议不要使用,除非你真的需要。

不同的消息级别以及什么时候该用

如果你看 Zend Framework 是,你会注意到有 8 个错误级别,从 0 ~ 7,0 是最高级别。我个人觉得下面5种就足够了:

常量名 描述说明
EMERG 灾难 Emergency
ERR 错误 Error
WARN 警告 Warnings
INFO 基本信息 Informational
DEBUG 调试信息 Debug

Emergency

但灾难发生,系统完全不可用的时候,应该使用 EMERG 常量,要求维护人员立即处理。

例如:

  • 后端服务器不可访问
  • 数据库挂了,无法访问

 

Error

当碰到一些例如数据丢失,请求未处理完成等相关严重错误时使用,要求重视并应该采取行动。

例如:

  • 请求未完成,而且没法恢复
  • 请求完成,数据丢失
  • 某些情况下保存数据失败
  • 接收到无效的数据
  • 超时或者响应时间太长
  • 处理异常
  • 安全相关问题

 

Warnings

但系统有些警告信息需要注意时使用 WARN 常量,它表示某些比较奇怪的问题,这些问题应该在下一个版本中解决,系统运行还是正常无误的,不需要立即采取行动。

例如:

  • 接收不到数据,或者计划外的数据
  • 处理过程返回的状态未定义,但无误
  • E-mails 延迟,但仍可送达
  • 响应时间比较长,但不会太严重
  • 其他非错误问题

 

Informational

应用程序中一些跟错误和警告无关的信息记录应使用 INFO 常量。

例如:

  • 事务启动,事务结束
  • 发送某个 email 到某个地址
  • 接收到来自 127.0.0.2 的 PUT 类型的 HTTP 请求
  • 获取ID为42的用户详细资料

 

Debug

调试信息,仅仅用于跟踪应用运行状态。

例如:

  • 使用 Mailer 适配器的参数值
  • 请求参数名和值
  • 数据库查询执行了多长时间以及返回多少条记录
  • 堆栈跟踪

 

聪明的日志实现

前面我们提到,不应该在产品环境中启用调试信息,因为会让系统占用更多的资源。

日志记录应该使用如下伪码的做法:

// If we have errors, include debug events in our logs. Else don't include them.
if ($errorEventCount > 0) {
    $allEvents = array_merge(
        $criticalEvents, $errorEvents, $warningEvents, $infoEvents, $debugEvents
    );
} else {
    $allEvents = array_merge(
        $criticalEvents, $errorEvents, $warningEvents, $infoEvents
    );
}

结论

在完美世界中,日志是不需要的,但现实不存在完美。尽管记录日志很简单,但你应该谨慎处之,让下一个开发者能非常轻松的就理解日志具体的意思。

最后非常感谢您阅读这篇罗里罗嗦的文章!

英文原文,OSCHINA 原创翻译

红薯注:在翻译这篇文章时,里面有不少小节我并没有领会其中具体的意思,特别是 PHP 相关的,请大家见谅,如果有实在看不懂的可以参考英文原文。

加载中
1
中山野鬼
中山野鬼
日志信息的摆放位置也很重要。不恰当的位置,会遗漏或造成日志信息过多的问题。日志信息,我个人认为主要在检测和DEBUG时有用。但两者的摆放位置不同。检测主要在模块状态发生改变时或时间窗内,对模块进行统计。而DEBUG则要在模块内对重要区域进行边界检测和判断。
0
游客
游客
先收藏,再慢慢看。
0
m
maxfong
先收藏,再慢慢看。
0
lxbzmy
lxbzmy
spring的日志就很好,像好多服务器的日志告诉你web.xml有错误,但是就是不告诉你哪一行有错。
0
马儿
马儿
不错!
0
难易
难易
意思就是这个意思了。。。但能认真写日志的程序员很少,因为这个不产生直接效益,而且要费很多心思
0
mallon
mallon
我最关心的日志能否定位到具体程序的函数、类、方法,或者行、列,没有这个功能肯定不会用的
0
mallon
mallon
还有函数、类、方法不能手写,而应该是日志工具自动完成
0
zkool
zkool

红薯英语就是好,翻译这么长的文章。但是没看懂

0
大道至簡
大道至簡
PHP的是一定要顶了,先MARK了  工作先....
返回顶部
顶部