Java Web 高级编程 - 第十一章 使用日志监控应用程序
本章内容
- 日志相关内容
- 日志级别
- 选择日志工具
- 在应用程序中集成日志
- 在客户支持应用程序中添加日志
11.1 了解日志的概念
11.1.1 记录日志的原因
11.1.2 在日志中记录的内容
11.1.3 日志的写入方式
- 控制台
- 平面文件
- 套接字
- SMTP和SMS
- 数据库
11.2 使用日志级别和分类
11.2.1 使用不同日志级别的原因
11.2.2 定义日志级别
尽管没有约定好的标准,但为JAVA应用程序编写的大多数日志系统仍然存在着一些常见的模式。通常他们为日志级别赋予的名字都具有高级的含义,大多数系统中都使用了6到10个不同的级别。
The levels in descending order are:
- SEVERE (highest value) 表示发生了严重的问题。
- WARNING 表示发生了一些可能是也可能不是问题的事件,并且可能需要进行检查。
- INFO 表示信息级别的日志。
- CONFIG 这个级别的事件通常包含了配置信息的细节。在应用程序或者组件启动时经常会看到这个级别的事件。
- FINE 表示调试信息。
- FINER 代表了应用程序追踪的不同级别。例如:在记录执行的SQL语句时使用FINER,在记录进入和退出方法调用时使用FINEST。
- FINEST (lowest value)
11.2.3 日志分类的工作方式
日志分类的概念比日志界别要稍微抽象一点。在几乎所有的Java用例中,日志分类都由命名记录器实例表示,并且每个记录器都可以分配一个不同的级别。通过这种模式,两个不同的类可以具有两个不同的记录器,可以讲一个设置为日志追踪数据,而另一个只记录警告。事实上,这正是大多数用例中分类的方法。在开发时,每个类都有自己的记录器,通常使用完全限定类名命名。通常建立记录器层次时,要使未定义级别的记录器继承某些父级记录器的级别,这取决于具体的日至系统。
11.2.4 筛选的工作方式
11.3 选择日志框架
11.3.1 API和实现
java.util.logging.LogManager类将负责创建和返回Logger实例,并代表默认实现进行操作。开发者可以扩展LogManager,并通过指定系统属性提供标准日志API的另一个实现。
11.3.2 性能
下面是日志系统能够正常运行的关键情况:
- 在调试级别日志被禁用时调用debug方法,该方法不应该花费毫秒级的时间 - 而是应该在纳秒级别甚至更少时间。
- 在选择一个性能良好的系统时,应该在代码中填满日志语句,然后关闭日志,并观察应用程序的响应时间是否有可感知的变化。这是在选择日志系统是应该注意的关键性能指标。
11.3.3 Apache Commons Logging和SLF4J
11.3.4 Apache Log4j 2
1.配置
2.级别
3.记录器
4.日志存储器
5.布局
6.过滤器
11.4 在应用程序中集成日志
private static final Logger log = LogManager.getLogger(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if(request.getParameter("action") == null) log.error("No action specified."); }
11.4.1 创建Log4j 2配置文件
Log4j 2将寻找两个不同的配置文件(log4j2和log4j2-test),并且它支持这两个配置文件的3中不同扩展(.xml、.json、.jsn)。
http://logging.apache.org/log4j/2.x/manual/configuration.html
示例:源代码中的Logging-Integration项目。
该项目非常简单,它包含了一个Servlet,其中包含了一些可以正确执行的方法,还有一些不可以正常执行的方法。首先必须要做的是创建正确的配置文件。当创建标准的配置文件时,它还将影响单元测试运行时的日志行为。
因为标准配置文件将指示Log4j 2把日志输出到文件,并且可能你不希望运行单元测试,所以你首先应该在项目的test文件夹的resources的目录中创建一个log4j2-test.xml文件。

<?xml version="1.0" encoding="UTF-8"?> <configuration status="WARN"> <appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> </appenders> <loggers> <root level="debug"> <appender-ref ref="Console"/> </root> </loggers> </configuration>
该配置文件几乎与隐式的默认配置一致。唯一的区别就是配置状态Logger的Level从OFF变成了WARN,root Logger的Level从ERROR变成了DEBUG,这两个改动都更适合于测试环境。
在创建了合适的测试配置之后,接下来在项目的source文件夹的resources目录中创建一个log4j2.xml文件。
<?xml version="1.0" encoding="UTF-8"?> <configuration status="WARN"> <appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> <RollingFile name="WroxFileAppender" fileName="../logs/application.log" filePattern="../logs/application-%d{MM-dd-yyyy}-%i.log"> <PatternLayout> <pattern>%d{HH:mm:ss.SSS} [%t] %X{id} %X{username} %-5level %c{36} %l: %msg%n</pattern> </PatternLayout> <Policies> <SizeBasedTriggeringPolicy size="10 MB" /> </Policies> <DefaultRolloverStrategy min="1" max="4" /> </RollingFile> </appenders> <loggers> <root level="warn"> <appender-ref ref="Console" /> </root> <logger name="com.wrox" level="info" additivity="false"> <appender-ref ref="WroxFileAppender" /> <appender-ref ref="Console"> <MarkerFilter marker="WROX_CONSOLE" onMatch="NEUTRAL" onMismatch="DENY" /> </appender-ref> </logger> <logger name="org.apache" level="info"> <appender-ref ref="WroxFileAppender" /> </logger> </loggers> </configuration>
这里有一些新鲜和有趣的内容。
第一,这里使用了滚动文件Appender将日志输出到Tomcat Logs目录中的application.log文件。在本例中,Appender被设置为每当文件大小达到10MB时就滚动日志文件,并保持不超过4个备份日志文件。
PatternLayout有许多默哀是可用于记录日志事件信息,我们可以在Log4j布局文档中了解所有的模式。http://logging.apache.org/log4j/2.x/manual/layouts.html。在本例中,文件Appender模式用%logger替换了%c并添加了%l,它用于输出类、方法、文件和日志消息发生的信号。它还添加了%X{id} %X{username},它们是ThreadContext中的属性。另外,文件Appender的模式与控制台Appender的模式相比,除了设置不同之外,它还使用了不同的XML模式。可以将Appender、Filter、Logger等的属性指定为标签特性或者嵌套标签。这两种方式是可以互相交换的。
最后,该文件的新的Logger配置表示com.wrox中的所有Logger和org.apache层次的级别都是INFO,并且com.wrox的所有Logger也都不是可添加的 - 它们不继承控制台Appender的属性,并且只记录到文件Appender中。不过要注意:com.wrox的控制台Appender的<appender-ref>元素包含一个嵌套的<MarkerFilter>元素,该过滤器表示com.wrox层次中的Logger可以将日志记录到控制台Appender,但只可以应用于包含名为WROX_CONSOLE的Marker的事件。http://logging.apache.org/log4j/2.x/manual/filters.html
11.4.2 在Web过滤器中使用鱼标签(Fish Tagging)
http://logging.apache.org/log4j/2.x/manual/thread-context.html
在使用任何日志框架时,都应该为请求添加鱼标签,这样就可以将属于相同请求的日志消息进行分组,然后进行分析。
org.apache.logging.log4j.ThreadContext中存储了当前线程的属性,知道ThreadContext被清空。同一线程中记录的所有事件,在属性被添加到ThreadContext之后,到该属性被移除之前都可以被关联到该属性。如果有许多并发Web请求正在执行,那么为每个请求分配唯一的鱼标签可以帮助你识别出特定请求的所有相关信息。
一个鱼标签通常是一些非常唯一的信息,例如UUID。ThreadContext可以存储任何用于区别日志事件的有用信息,例如登录用户的用户名。下面的LoggingFilter在请求开始时将标签(ID)和会话用户名(username)添加到了ThreadContext中,并在请求完成时清除ThreadContext。然后之前讨论过的模式将使用%X{id}和%X{username}打印这些属性。因为过滤器支持多个派发器类型,并且可以在单个请求中执行多次,所以它只在鱼标签和username属性尚未设置时设置它们,并且只在设置鱼标签和username属性的相同调用中清除ThreadContext。
@WebFilter(urlPatterns = "/*", dispatcherTypes = { DispatcherType.REQUEST, DispatcherType.ERROR, DispatcherType.FORWARD, DispatcherType.INCLUDE }) public class LoggingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { boolean clear = false; if(!ThreadContext.containsKey("id")) { clear = true; ThreadContext.put("id", UUID.randomUUID().toString()); HttpSession session = ((HttpServletRequest)request).getSession(false); if(session != null) ThreadContext.put("username", (String)session.getAttribute("username")); } try { chain.doFilter(request, response); } finally { if(clear) ThreadContext.clearAll(); } } }
11.4.3 在Java代码中编写日志语句
http://logging.apache.org/log4j/2.x/javadoc.html
@WebServlet(name = "actionServlet", urlPatterns = "/files") public class ActionServlet extends HttpServlet { private static final Logger log = LogManager.getLogger(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if(action != null) { log.info("Received request with action {}.", action); String contents = null; switch(action) { case "readFoo": contents = this.readFile("../foo.bar", true); break; case "readLicense": contents = this.readFile("../LICENSE", false); break; default: contents = "Bad action " + action + " specified."; log.warn("Action {} not supported.", action); } if(contents != null) response.getWriter().write(contents); } else { log.error("No action specified."); response.getWriter().write("No action specified."); } } protected String readFile(String fileName, boolean deleteWhenDone) { log.entry(fileName, deleteWhenDone); try { byte[] data = Files.readAllBytes(new File(fileName).toPath()); log.info("Successfully read file {}.", fileName); return log.exit(new String(data)); } catch(IOException e) { log.error(MarkerManager.getMarker("WROX_CONSOLE"), "Failed to read file {}.", fileName, e); return null; } } }
编译并启动应用程序,然后在浏览器中访问地址:http://localhost:8080/logging-integration/files。
日志文件application应该会出现在Tomcat的logs目录中,并且其中有一个错误。

在URL后面添加?action=badaction,再次加载日志文件,其中将出现一个新的信息消息和一个警告消息。

将action修改为,从浏览器中读取Tomcat的许可文件。这次就只有信息级别消息出现在了日志中。

最后,将action修改为readFoo,此时一个错误以及完整的异常堆栈追踪将出现在日志文件中。
该错误也会出现在控制台(调试器)输出中。因为它包含了名为WROX_CONSOLE的标记。


11.4.4 在JSP中使用日志标签库
Log4j2提供了一个标签库,用于在JSP中而不是使用脚本记录消息。
一般来说,很少有需要在表示层记录日志的。事实上,只要JSP中不包含任何业务逻辑,就不需要在日志中使用日志标签。
可以使用下面的taglib指令包含标签库:
<%@ taglib prefix="log" uri="http://logging.apache.org/log4j/tld/log" %>
Logging-Integration项目在Web根目录下有一个logging.jsp文件,它演示了使用这些日志标签的方式:
<log:entry /> <!DOCTYPE html> <html> <head> <title>Test Logging</title> </head> <body> <log:info message="JSP body displaying." /> Messages have been logged. <log:info>JSP body complete.</log:info> </body> </html> <log:exit />

Chapter 11
Updated on 9/4/14.
155.84 KB
摘录自:[美]Nicholas S.Williams著,王肖峰译 Java Web高级编程 [M]、清华大学出版社,2015、291、

浙公网安备 33010602011771号