简介
在代码量比较小的程序里追踪bug可以直接进行断点调试;但对于较大的软件系统这通常是一个低效的办法,尤其是软件系统包含UI交互的时候,断点常常使得UI卡死,使得追踪bug变得难以进行;另一种情形则是在多线程或者多进程的应用场景里,断点也很难发挥作用;一般书上讲printf是最好的调试方法,通过在关键的地方使用printf可以把软件运行的内部状态暴露出来,从而定位可能存在的问题,但带UI的程序通常是不能显示printf输出的,对于所有这些情况,日志都是更好的选择.
除了上边提到的部分场景没有日志几乎就没办法定位,使用日志还有额外的好处就是能够定制软件输出的信息;从输出信息的格式[输出日志的时间戳、文件名及行号、进程ID等]和为每一个模块指定单独的日志输出位置[输出到console,文件,数据库,发送到指定邮箱等];不仅是可以定制,而且是在不改变软件代码的情况下通过修改日志库的配置文件做到这些.无需再次编译代码.
这里介绍的日志库是用于c++程序的log4cxx,这个库还有java版本的log4j,和c#版本的log4net, java和c#的不太了解就不多说.
Log4cxx的配置文件
Log4cxx的配置文件是一个xml文件或者properties属性文件,properties文件的相关设置网上有很多讲的,但我觉得这种格式不够清晰,更倾向于使用xml,典型的xml配置文件由两部分组成,一是logger节点,用来对应代码中的模块;一是appender,用来指定日志输出的格式和输出位置;一般还会有一个root节点,可以作为默认的logger来使用;
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <!-- Note that this file is read by the sdk every 60 seconds --> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <!-- %m 输出代码中指定的消息 %p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL %r 输出自应用启动到输出该log信息耗费的毫秒数 %c 输出所属的类目,通常就是所在类的全名 %t 输出产生该日志事件的线程名 %n 输出一个回车换行符,Windows平台为“\r\n”,Unix平台为“\n” %d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出2008年11月14日 15:16:17,890 %l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。 Log4j提供的appender有以下几种: org.apache.log4j.ConsoleAppender 控制台 org.apache.log4j.FileAppender 文件 org.apache.log4j.DailyRollingFileAppender 每天产生一个日志文件 org.apache.log4j.RollingFileAppender 文件大小到达指定尺寸的时候产生一个新的文件 org.apache.log4j.WriterAppender 将日志信息以流格式发送到任意指定的地方 Log4j提供的Layout有以下几种: org.apache.log4j.HTMLLayout 以HTML表格形式布局 org.apache.log4j.PatternLayout 可以灵活地指定布局模式 org.apache.log4j.SimpleLayout 包含日志信息的级别和信息字符串 org.apache.log4j.TTCCLayout 包含日志产生的时间、线程、类别等等信息 --> <!-- 性能统计日志输出 --> <appender name="PerformanceAppender" additivity="false" class="org.apache.log4j.RollingFileAppender"> <!-- The file to log Performance calls --> <param name="file" value="d:/log/performance.log" /> <param name="append" value="true" /> <param name="BufferedIO" value="true"/> <param name="maxFileSize" value="10000KB" /> <param name="maxBackupIndex" value="1" /> <layout class="org.apache.log4j.PatternLayout"> <!-- The log message pattern --> <param name="ConversionPattern" value="%5p %d{ISO8601} [%t] [%c] %m%n"/> </layout> </appender> <!-- 性能统计日志输出 --> <appender name="defaultAppender" additivity="false" class="org.apache.log4j.RollingFileAppender"> <!-- The file to log Performance calls --> <param name="file" value="d:/log/alg.log" /> <param name="append" value="true" /> <param name="BufferedIO" value="true"/> <param name="maxFileSize" value="10000KB" /> <param name="maxBackupIndex" value="1" /> <layout class="org.apache.log4j.PatternLayout"> <!-- The log message pattern --> <param name="ConversionPattern" value="%5p %d{ISO8601} [%F, %L] %m%n"/> </layout> </appender> <!-- 命令行日志输出 --> <appender name="Console" additivity="false" class="org.apache.log4j.ConsoleAppender"> <!-- Logs to Console --> <layout class="org.apache.log4j.PatternLayout"> <!-- The log message pattern --> <param name="ConversionPattern" value="%5p %d{ISO8601} [%F, %L] %m%n"/> </layout> </appender> <logger name="Performance_logger" additivity="false"> <!-- set logger setting --> <!-- 设置级别 --> <level value="all"/> <!-- 设置日志输出--> <!-- 输出到命令行 --> <!--<appender-ref ref="Console" />--> <!--输出到默认日志文件 --> <appender-ref ref="defaultAppender" /> <!--根据需要可单独输出到特定文件 --> <!--<appender-ref ref="PerformanceAppender" />--> </logger> <root> <priority value="all" /> <appender-ref ref="defaultAppender" /> <!-- <appender-ref ref="Console" /> --> </root> </log4j:configuration>
这个例子里包含两个logger: root 和 Performance_logger, 三个appender: Console\ defaultAppender\ PerformanceAppender;
Root作为默认logger保留,虽然我一般不使用它; Performance_logger对应我想在程序中输出的性能统计信息;对于我的代码里的需要统计性能的地方,我都是用Performance_logger,这样,通过为它指定一个appender,我们可以将性能统计信息汇集在同一个日志文件里;其他的用于调试bug的日志输出信息可以通过添加更多的logger来进行精细的控制;三个appender分别对应输出日志到控制台(Console),默认的输出(defaultAppender);单独为性能统计输出准备的PerformanceAppender;这样一来在需要的时候我们只需要在配置文件里把Performance_logger 的appender-ref属性改为Console,就可以让日志输出到控制台而无需修改任何软件相关的代码;经过了测试阶段可能不需要再输出任何性能统计信息,那么我们可以把Performance_logger的Level属性改为Fatal,那么就不会有任何日志信息输出了;{我们需要在代码里输出日志信息时指定相应的Level,如果日志信息的Level高于配置文件里设定的Level,则该日志信息会被输出};
如果我们有很多模块在同一个软件系统里使用,他们都需要使用日志,那么我们在配置文件里为每一个模块添加对应的logger和appender就可以实现对不同的模块输出的日志信息进行精确的控制了.具体的控制在于每一个logger有name属性;在代码中输出日志信息时可以
以如下方式以getlogger指定logger以进行日志输出控制:
LoggerPtr Perf_logger (Logger::getLogger("Performance_logger ")); log4cxx::logstream logstream(Perf_logger, Level::getInfo()); logstream << Level::getTrace() << "<log message>"<< LOG4CXX_ENDMSG;
或者偷懒的办法:
LoggerPtr Perf_logger (Logger:: getRootLogger ()); log4cxx::logstream logstream(Perf_logger, Level::getInfo()); logstream << Level::getTrace() << "<log message>"<< LOG4CXX_ENDMSG;
偷懒时使用的logger对应xml文件的root节点;使用默认日志输出的坏处在于无法通过修改配置文件来隔离日志输出了;比如软件系统有A,B,C三个模块都使用getRootLogger得到的logger进行日志输出;那么在发现问题的时候就只能在同一个日志文件里去分析了,这个文件是包含了三个模块的所有日志输出,不能进行有效的日志隔离;如果我们在配置文件为我们的模块准备了相应的logger,并在代码里按logger名指定输出;那么需要时我们可以通过设置相应的appender让各个模块输出到不同的日志文件以便于进行单独的分析;如果软件运行稳定,我们也可以让各个logger都输出到同一个默认的appender,避免产生太多临时文件.
前文提到日志可以输出到数据库或者自动发送到指定的email,这需要将log4cxx与相应的邮件发送库和数据库的驱动一起编译,我未作尝试,就不多说.
在代码里输出日志信息
为了在代码里输出信息,首先应该用配置文件初始化日志库,这需要调用log4cxx的
log4cxx::xml::DOMConfigurator::configure(config);
接口,指定xml配置文件的路径即可.需要注意的是,由多个模块构建的应用程序,初始化一次即可.
如果代码里写了输出日志却没有正常初始化日志库,那么就会出现这样一个错误;这个错误导致所有与该logger相关的日志信息无法输出,但不会影响程序运行;
在准备好了配置文件并正确初始化日志库后,可以在代码里输出任意想输出的信息了,一般输出的步骤是:获取对应模块的logger,然后使用log4cxx对应的四个宏对该logger进行日志输出:
LoggerPtr logger = Logger::getLogger("<logger-name>"); LOG4CXX_INFO(logger, ("<info-message>")); LOG4CXX_DEBUG(logger, "<DEBUG-message>"); LOG4CXX_WARN(logger, "<WARN-message>"); LOG4CXX_ERROR(logger, "<ERROR-message>");
我个人常使用另一个方式输出日志,获得logger之后,用该logger初始化logstream,然后使用logstream来输出日志信息,效果是一样的:
log4cxx::logstream logstream(logger, Level::getInfo()); logstream << Level::getTrace() << "<trace-message>"<< LOG4CXX_ENDMSG;
前文提到如果软件系统由多个模块构成,那么只需要初始化一次;所以日志初始化的过程不应该在模块里完成,而应该由调用模块的主程序来完成日志的初始化;那么暴露一个初始化的接口给主程序主用就是很有必要的,dll库模块可能给c调用,也可能给其他语言如c#调用,所以我是会把原始的接口藏起来做一个c的接口来完成日志初始化,如后文里的loginit所示,更具体点说是我把这个接口和log4cxx库编译在一起了,从库里导出一个初始化的接口,其他接口维持不变即可(如果用dependency walk查看log4cxx库会发现所有接口都是c++的导出接口).
其他问题
日志的级别如何定? 应该在什么地方输出日志?
这个问题只能依靠个人经验了,遇到的bug多了就自然会知道;一般的做法就是函数入口和退出的地方都打日志,函数的参数和返回值可以记日志,如果发生崩溃能大致定位出在哪个函数里出的问题,通过分析函数输入参数值和代码逻辑应该能对定位出bug起到较大的帮助.
Log4cxx库在哪里? 怎么编译?
出门右拐找google/baidu/bing都可以
// 我编译到log4cxx里的日志初始化代码// loginit.h #ifndef _zstang_loginit_h_ #define _zstang_loginit_h_ #ifdef _WIN32 #ifdef LOG_EXPORT #define DLL_EXPORT __declspec(dllexport) #else #define DLL_EXPORT __declspec(dllimport) #endif #else #define DLL_EXPORT #endif #define CALL_CONVENTION _cdecl #ifdef __cplusplus extern "C"{ #endif ///<---------------------------------------------------------------> ///< 日志初始化接口,参数为log4cxx日志库配置文件 log4cxx.xml的路径 > ///<---------------------------------------------------------------> void DLL_EXPORT CALL_CONVENTION init_log(char* config_path); #ifdef __cplusplus }; #endif #endif // loginit.cpp #include "logInit.h" #include <string> using namespace std; #include "../src/main/include/log4cxx/xml/domconfigurator.h" void DLL_EXPORT CALL_CONVENTION init_log(char* config_path) { string config=string(config_path); log4cxx::xml::DOMConfigurator::configure(config); }