Java日志API管理最佳实践详解
概述
对于现在的应用程序来说,日志的重要性是不言而喻的。很难想象没有任何日志记录功能的应用程序运行在生产环境中。日志所能提供的功能是多种多样的,包括记录程序运行时产生的错误信息、状态信息、调试信息和执行时间信息等。在生产环境中,日志是查找问题来源的重要依据。应用程序运行时的产生的各种信息,都应该通过日志API来进行记录。
很多开发人员习惯于使用System.out.println、System.err.println以及异常对象的printStrackTrace方法来输出相关信息。这些使用方式虽然简便,但是所产生的信息在出现问题时并不能提供有效的帮助。这些使用方式都应该改为使用日志API。使用日志API并没有增加很多复杂度,但是所提供的好处是显著的。
尽管记录日志是应用开发中并不可少的功能,在JDK的最初版本中并不包含日志记录相关的API和实现。相关的API(java.util.logging包,JUL)和实现,直到JDK1.4才被加入。因此在日志记录这一个领域,社区贡献了很多开源的实现。其中比较流行的包括log4j及其后继者logback。除了真正的日志记录实现之外,还有一类与日志记录相关的封装API,如ApacheCommonsLogging和SLF4J。
这类库的作用是在日志记录实现的基础上提供一个封装的API层次,对日志记录API的使用者提供一个统一的接口,使得可以自由切换不同的日志记录实现。比如从JDK的默认日志记录实现JUL切换到log4j。这类封装API库在框架的实现中比较常用,因为需要考虑到框架使用者的不同需求。在实际的项目开发中则使用得比较少,因为很少有项目会在开发中切换不同的日志记录实现。本文对于这两类库都会进行具体的介绍。
记录日志只是有效地利用日志的第一步,更重要的是如何对程序运行时产生的日志进行处理和分析。典型的情景包括当日志中包含满足特定条件的记录时,触发相应的通知机制,比如邮件或短信通知;还可以在程序运行出现错误时,快速地定位潜在的问题源。这样的处理和分析的能力对于实际系统的维护尤其重要。当运行系统中包含的组件过多时,日志对于错误的诊断就显得格外重要。
本文首先介绍关于日志API的基本内容。
Java日志API
从功能上来说,日志API本身所需求的功能非常简单,只需要能够记录一段文本即可。API的使用者在需要进行记录时,根据当前的上下文信息构造出相应的文本信息,调用API完成记录。一般来说,日志API由下面几个部分组成:
- 记录器(Logger):日志API的使用者通过记录器来发出日志记录请求,并提供日志的内容。在记录日志时,需要指定日志的严重性级别。
- 格式化器(Formatter):对记录器所记录的文本进行格式化,并添加额外的元数据。
- 处理器(Handler):把经过格式化之后的日志记录输出到不同的地方。常见的日志输出目标包括控制台、文件和数据库等。
记录器
当程序中需要记录日志时,首先需要获取一个日志记录器对象。一般的日志记录API都提供相应的工厂方法来创建记录器对象。每个记录器对象都是有名称的。一般的做法是使用当前的Java类的名称或所在包的名称作为记录器对象的名称。记录器的名称通常是具有层次结构的,与Java包的层次结构相对应。比如Java类“com.myapp.web.IndexController”中使用的日志记录器的名称一般是“com.myapp.web.IndexController”或“com.myapp.web”。除了使用类名或包名之外,还可以根据日志记录所对应的功能来进行划分,从而选择不同的名称。
比如用“security”作为所有与安全相关的日志记录器的名称。这样的命名方式对于某些横切的功能比较实用。开发人员一般习惯于使用当前的类名作为日志记录器的名称,这样可以快速在日志记录中定位到产生日志的Java类。使用有意义的其他名称在很多情况下也是一个不错的选择。
在通过日志记录器对象记录日志时,需要指定日志的严重性级别。根据每个记录器对象的不同配置,低于某个级别的日志消息可能不会被记录下来。该级别是日志API的使用者根据日志记录中所包含的信息来自行决定的。不同的日志记录API所定义的级别也不尽相同。日志记录封装API也会定义自己的级别并映射到底层实现中相对应的实际级别。
比如JDK标准的日志API使用的级别包括OFF、SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST和ALL等,Log4j使用的级别则包括OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE和ALL等。一般情况下,使用得比较多的级别是FATAL、ERROR、WARN、INFO、DEBUG和TRACE等。这6个级别所对应的情况也有所不同:
- FATAL:导致程序提前结束的严重错误。
- ERROR:运行时异常以及预期之外的错误。
- WARN:预期之外的运行时状况,不一定是错误的情况。
- INFO:运行时产生的事件。
- DEBUG:与程序运行时的流程相关的详细信息。
- TRACE:更加具体的详细信息。
在这6个级别中,以ERROR、WARN、INFO和DEBUG作为常用。
日志记录API的使用者通过记录器来记录日志消息。日志消息在记录下来之后只能以文本的形式保存。不过有的实现(如Log4j)允许在记录日志时使用任何Java对象。非String类型的对象会被转换成String类型。由于日志记录通常在出现异常时使用,记录器在记录消息时可以把产生的异常(Throwable类的对象)也记录下来。
每个记录器对象都有一个运行时对应的严重性级别。该级别可以通过配置文件或代码的方式来进行设置。如果没有显式指定严重性级别,则会根据记录器名称的层次结构关系往上进行查找,直到找到一个设置了严重性级别的名称为止。比如名称为“com.myapp.web.IndexController”的记录器对象,如果没有显式指定其严重性级别,则会依次查找是否有为名称“com.myapp.web”、“com.myapp”和“com”指定的严重性级别。如果仍然没有找到,则使用根记录器配置的值。
通过记录器对象来记录日志时,只是发出一个日志记录请求。该请求是否会完成取决于请求和记录器对象的严重性级别。记录器使用者产生的低于记录器对象严重性级别的日志消息不会被记录下来。这样的记录请求会被忽略。除了基于严重性级别的过滤方式之外,日志记录框架还支持其他自定义的过滤方式。比如JUL可以通过实现java.util.logging.Filter接口的方式来进行过滤。Log4j可以通过继承org.apache.log4j.spi.Filter类的方式来过滤。
格式化器
实际记录的日志中除了使用记录器对象时提供的消息之外,还包括一些元数据。这些元数据由日志记录框架来提供。常用的信息包括记录器的名称、时间戳、线程名等。格式化器用来确定所有这些信息在日志记录中的展示方式。不同的日志记录实现提供各自默认的格式化方式和自定义支持。
JUL中通过继承java.util.logging.Formatter类来自定义格式化的方式,并提供了两个标准实现SimpleFormatter类和XMLFormatter类。清单1中给出了JUL中自定义格式化器的实现方式,只需要继承自Formatter类并实现format方法即可。参数LogRecord类的对象中包含了日志记录中的全部信息。
清单1.JUL中自定义格式化器的实现
publicclassCustomFormatterextendsFormatter{ publicStringformat(LogRecordrecord){ returnString.format("<%s>[%s]:%s",newDate(record.getMillis()),record.getLoggerName(),record.getMessage()); } }
对于自定义的格式化器类,需要在JUL的配置文件中进行指定,如清单2所示。
清单2.在JUL配置文件中指定自定义的格式化器类
java.util.logging.ConsoleHandler.formatter=logging.jul.CustomFormatter
Log4j在格式化器的实现上要简单一些,由org.apache.log4j.PatternLayout类来负责完成日志记录的格式化。在自定义时不需要创建新的Java类,而是通过配置文件指定所需的格式化模式。在格式化模式中,不同的占位符表示不同类型的信息。比如“%c”表示记录器的名称,“%d”表示日期,“%m”表示日志的消息文本,“%p”表示严重性级别,“%t”表示线程的名称。清单3给出了Log4j配置文件中日志记录的自定义方式。
清单3.Log4j中日志记录的自定义方式
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-ddHH:mm:ss}[%p]%c-%m%n
日志处理器
日志记录经过格式化之后,由不同的处理器来进行处理。不同的处理器有各自不同的处理方式。比如控制台处理器会把日志输出到控制台中,文件处理器把日志写入到文件中。除了这些之外,还有写入到数据库、通过邮件发送、写入到JMS队列等各种不同的处理方式。
日志处理器也可以配置所处理日志信息的最低严重性级别。低于该级别的日志不会被处理。这样可以控制所处理的日志记录数量。比如控制台处理器的级别一般设置为INFO,而文件处理器则一般设置为DEBUG。
日志记录框架一般提供了比较多的日志处理器实现。开发人员也可以创建自定义的实现。
Java日志封装API
除了JUL和log4j这样的日志记录库之外,还有一类库用来封装不同的日志记录库。这样的封装库中一开始以ApacheCommonsLogging框架最为流行,现在比较流行的是SLF4J。这样封装库的API都比较简单,只是在日志记录库的API基础上做了一层简单的封装,屏蔽不同实现之间的区别。由于日志记录实现所提供的API大致上比较相似,封装库的作用更多的是达到语法上的一致性。
在ApacheCommonsLogging库中,核心的API是org.apache.commons.logging.LogFactory类和org.apache.commons.logging.Log接口。LogFactory类提供了工厂方法用来创建Log接口的实现对象。比如LogFactory.getLog可以根据Java类或名称来创建Log接口的实现对象。Log接口中为6个不同的严重性级别分别定义了一组方法。比如对DEBUG级别,定义了isDebugEnabled()、debug(Objectmessage)和debug(Objectmessage,Throwablet)三个方法。从这个层次来说,Log接口简化了对于日志记录器的使用。
SLF4J库的使用方式与ApacheCommonsLogging库比较类似。SLF4J库中核心的API是提供工厂方法的org.slf4j.LoggerFactory类和记录日志的org.slf4j.Logger接口。通过LoggerFactory类的getLogger方法来获取日志记录器对象。与ApacheCommonsLogging库中的Log接口类似,Logger接口中的方法也是按照不同的严重性级别来进行分组的。Logger接口中有同样isDebugEnabled方法。不过Logger接口中发出日志记录请求的debug等方法使用String类型来表示消息,同时可以使用包含参数的消息,如清单4所示。
清单4.SLF4J的使用方式
publicclassSlf4jBasic{ privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(Slf4jBasic.class); publicvoidlogBasic(){ if(LOGGER.isInfoEnabled()){ LOGGER.info("Mylogmessagefor%s","Alex"); } } }
MDC
MDC(MappedDiagnosticContext,映射调试上下文)是log4j和logback提供的一种方便在多线程条件下记录日志的功能。某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是Web应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。
一种解决的办法是采用自定义的日志格式,把用户的信息采用某种方式编码在日志记录中。这种方式的问题在于要求在每个使用日志记录器的类中,都可以访问到用户相关的信息。这样才可能在记录日志时使用。这样的条件通常是比较难以满足的。MDC的作用是解决这个问题。
MDC可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的MDC的内容。当需要记录日志时,只需要从MDC中获取所需的信息即可。MDC的内容则由程序在适当的时候保存进去。对于一个Web应用来说,通常是在请求被处理的最开始保存这些数据。清单5中给出了MDC的使用示例。
清单5.MDC使用示例
publicclassMdcSample{ privatestaticfinalLoggerLOGGER=Logger.getLogger("mdc"); publicvoidlog(){ MDC.put("username","Alex"); if(LOGGER.isInfoEnabled()){ LOGGER.info("Thisisamessage."); } } }
清单5中,在记录日志前,首先在MDC中保存了名称为“username”的数据。其中包含的数据可以在格式化日志记录时直接引用,如清单6所示,“%X{username}”表示引用MDC中“username”的值。
清单6.使用MDC中记录的数据
log4j.appender.stdout.layout.ConversionPattern=%X{username}%d{yyyy-MM-ddHH:mm:ss}[%p]%c-%m%n
日志记录最佳实践
下面主要介绍一些在记录日志时的比较好的实践。
检查日志是否可以被记录
当日志记录器收到一个日志记录请求时,如果请求的严重性级别低于记录器对象的实际有效级别,则该请求会被忽略。在日志记录方法的实现中会首先进行这样的检查。不过推荐的做法是在调用API进行记录之前,首先进行相应的检查,这样可以避免不必要的性能问题,如清单7所示。
清单7.检查日志是否可以被记录
if(LOGGER.isDebugEnabled()){ LOGGER.debug("Thisisamessage."); }
清单7中的做法的作用在于避免了构造日志记录消息所带来的开销。日志消息中通常包含与当前上下文相关的信息。为了获取这些信息并构造相应的消息文本,不可避免会产生额外的开销。尤其对于DEBUG和TRACE级别的日志消息来说,它们所出现的频率很高,累加起来的开销比较大。因此在记录INFO、DEBUG和TRACE级别的日志时,首先进行相应的检查是一个好的实践。而WARN及其以上级别的日志则一般不需要进行检查。
日志中包含充分的信息
日志中所包含的信息应该是充分的。在记录日志消息时应该尽可能多的包含当前上下文中的各种信息,以方便在遇到问题时可以快速的获取到所需的信息。比如在网上支付功能中,与支付相关的日志应该完整的包含当前用户、订单以及支付方式等全部信息。一种比较常见的做法是把相关的日志记录分散在由不同日志记录器所记录的日志中。当出现问题之后,需要手工查找并匹配相关的日志来定位问题,所花费的时间和精力会更多。因此,应该尽可能在一条日志记录中包含足够多的信息。
使用合适的日志记录器名称
一般的日志记录实践是使用当前Java类的全名作为其使用的日志记录器的名称。这样做可以得到一个与Java类和包的层次结构相对应的日志记录器的层次结构。可以很方便的按照不同的模块来设置相应的日志记录级别。
不过对于某些全局的或是横切的功能,如安全和性能等,则推荐使用功能相关的名称。比如程序中可能包含用来提供性能剖析信息的日志记录。对于这样的日志记录,应该使用同一名称的日志记录器,如类似“performance”或“performance.web”。这样当需要启用和禁用性能剖析时,只需要配置这些名称的记录器即可。
使用半结构化的日志消息
在介绍日志记录API中的格式化器时提到过,日志记录中除了基本的日志消息之外,还包括由日志框架提供的其他元数据。这些数据按照给定的格式出现在日志记录中。这些半结构化的格式使得可以通过工具提取日志记录中的相关信息进行分析。在使用日志API进行记录时,对于日志消息本身,也推荐使用半结构化的方式来组织。
比如一个电子商务的网站,当用户登录之后,该用户所产生的不同操作所对应的日志记录中都可以包含该用户的用户名,并以固定的格式出现在日志记录中,如清单8所示。
清单8.使用半结构化的日志消息
[user1]用户登录成功。 [user1]用户成功购买产品A。 [user2]订单003付款失败。
当需要通过日志记录来排查某个用户所遇到的问题时,只需要通过正则表达就可以很快地查询到用户相关的日志记录。
日志聚合与分析
在程序中正确的地方输出合适的日志消息,只是合理使用日志的第一步。日志记录的真正作用在于当有问题发生时,能够帮助开发人员很快的定位问题所在。不过一个实用的系统通常由很多个不同的部分组成。这其中包括所开发的程序本身,也包括所依赖的第三方应用程序。以一个典型的电子商务网站为例,除了程序本身,还包括所依赖的底层操作系统、应用服务器、数据库、HTTP服务器和代理服务器和缓存等。当一个问题发生时,真正的原因可能来自程序本身,也可能来自所依赖的第三方程序。这就意味着开发人员可能需要检查不同服务器上不同应用程序的日志来确定真正的原因。
日志聚合的作用就在于可以把来自不同服务器上不同应用程序产生的日志聚合起来,存放在单一的服务器上,方便进行搜索和分析。在日志聚合方面,已经有不少成熟的开源软件可以很好的满足需求。本文中要介绍的是logstash,一个流行的事件和日志管理开源软件。logstash采用了一种简单的处理模式:输入->过滤器->输出。
logstash可以作为代理程序安装到每台需要收集日志的机器上。logstash提供了非常多的插件来处理不同类型的数据输入。典型的包括控制台、文件和syslog等;对于输入的数据,可以使用过滤器来进行处理。典型的处理方式是把日志消息转换成结构化的字段;过滤之后的结果可以被输出到不同的目的地,比如ElasticSearch、文件、电子邮件和数据库等。
Logstash在使用起来很简单。从官方网站下载jar包并运行即可。在运行时需要指定一个配置文件。配置文件中定义了输入、过滤器和输出的相关配置。清单9给出了一个简单的logstash配置文件的示例。
清单9.logstash配置文件示例
input{ file{ path=>["/var/log/*.log","/var/log/messages","/var/log/syslog"] type=>'syslog' } } output{ stdout{ debug=>true debug_format=>"json" } }
清单9中定义了logstash收集日志时的输入(input)和输出(output)的相关配置。输入类型是文件(file)。每种类型输入都有相应的配置。对于文件来说,需要配置的是文件的路径。对每种类型的输入,都需要指定一个类型(type)。该类型用来区分来自不同输入的记录。代码中使用的输出是控制台。配置文件完成之后,通过“java-jarlogstash-1.1.13-flatjar.jaragent-flogstash-simple.conf”就可以启动logstash。
在日志分析中,比较重要的是结构化的信息。而日志信息通常只是一段文本,其中的不同字段表示不同的含义。不同的应用程序产生的日志的格式并不相同。在分析时需要关注的是其中包含的不同字段。比如Apache服务器会产生与用户访问请求相关的日志。
在日志中包含了访问者的各种信息,包括IP地址、时间、HTTP状态码、响应内容的长度和UserAgent字符串等信息。在logstash收集到日志信息之后,可以根据一定的规则把日志信息中包含的数据提取出来并命名。logstash提供了grok插件可以完成这样的功能。grok基于正则表达式来工作,同时提供了非常多的常用类型数据的提取模式,如清单10所示。
清单10.使用grok提取日志记录中的内容
//Apache访问日志 49.50.214.136GET/index.html2001150"Mozilla/5.0(WindowsNT6.1;WOW64)AppleWebKit/537.17(KHTML,likeGecko)Chrome/24.0.1312.57Safari/537.17" //grok提取模式 %{IP:client}%{WORD:method}%{URIPATHPARAM:request}%{NUMBER:status}%{NUMBER:bytes}%{QS:useragent}
在经过上面grok插件的提取之后,Apache访问日志被转换成包含字段client、method、request、status、bytes和useragent的格式化数据。可以根据这些字段来进行搜索。这对于分析问题和进行统计都是很有帮助的。
当日志记录通过logstash进行收集和处理之后,通常会把这些日志记录保存到数据库中进行分析和处理。目前比较流行的方式是保存到ElasticSearch中,从而可以利用ElasticSearch提供的索引和搜索能力来分析日志。已经有不少的开源软件在ElasticSearch基础之上开发出相应的日志管理功能,可以很方便的进行搜索和分析。本文中介绍的是Graylog2。
Graylog2由服务器和Web界面两部分组成。服务器负责接收日志记录并保存到ElasticSearch之中。Web界面则可以查看和搜索日志,并提供其他的辅助功能。logstash提供了插件gelf,可以把logstash收集和处理过的日志记录发送到Graylog2的服务器。这样就可以利用Graylog2的Web界面来进行查询和分析。只需要把清单9中的logstash的配置文件中的output部分改成清单11中所示即可。
清单11.配置logstash输出到Graylog2
output{ gelf{ host=>'127.0.0.1' } }
在安装Graylog2时需要注意,一定要安装与Graylog2的版本相对应的版本的ElasticSearch,否则会出现日志记录无法保存到ElasticSearch的问题。本文中使用的是Graylog2服务器0.11.0版本和ElasticSearch0.20.4版本。
除了Graylog2之外,另外一个开源软件Kibana也比较流行。Kibana可以看成是logstash和ElasticSearch的Web界面。Kibana提供了更加丰富的功能来显示和分析日志记录。与代码清单中的logstash的配置相似,只需要把输出改为elasticsearch就可以了。Kibana可以自动读取ElasticSearch中包含的日志记录并显示。
小结
日志记录是应用程序开发中的重要一环。不过这一环比较容易被开发人员忽视,因为它所产生的影响在程序运行和维护时。对于一个生产系统来说,日志记录的重要性是不言而喻的。本文首先以java.util.logging包和log4j为例介绍了Java日志API的主要组成部分和使用方式,同时也介绍了ApacheCommonsLogging和SLF4J两种日志封装API。本文也给出了一些记录日志时应该采用的最佳实践。最后介绍了如何使用开源工具对日志进行聚合和分析。通过本文,开发人员可以了解如何在开发中有效的使用日志。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。