Log Builder
Log Builder是在2.13.0版本新加入的功能,在logback的2.0版本中也加入了这种组装日志的形式。
过去:
logger.error("Unable to process request due to {}", code, exception);
现在:
logger.atError().withThrowable(exception).log("Unable to process request due to {}", code);
过去的用法中我们如果没有具体点进去方法,可能会疑惑这是一个参数还是一个异常,那么现在我们有了船新的版本,可以通过新版的Builder清楚地区分这两个情况!
现在,当调用任何atTrace,atDebug,atInfo,atWarn,atError,atFatal,always或atLevel(Level)方法时,Logger类都会返回LogBuilder。 另外,logBuilder允许在记录事件之前将Marker,Throwable和/或位置添加到事件中。 调用log方法总是导致log事件被完成并发送。
在官方文档中提到Log Builder还加入一个withLocation()
方法,用来标识异常的位置信息,我们在logback也知道,想要知道这个异常位置是非常消耗性能的,官方针对这个功能的性能做了一个性能测试,结果表明虽然withLocation()
还是会比不记录Location速度慢,但是相对于非Builder版本还是快了不少。
Flow Tracing
Logger类提供了一些方法,方便你跟踪应用程序的执行路径。这些方法会产生从其它调试日志中单独过滤掉的日志事件。你可以使用这些方法来:
- 在开发阶段辅助问题诊断,而不需要单步调试
- 在运维阶段辅助问题诊断,如果单步调试不可用
- 帮助新用户理解应用程序的行为
流程跟踪的主要方法是 entry()【方法过期,不建议使用,请使用traceEntry()】、 traceEntry()和 exit()【方法过期,不建议使用,请使用traceExit()】、 traceExit()。它们都产生TRACE级别的日志。前两个方法通常放置在被跟踪方法的开始处,它们使用ENTER标记,此标记是FLOW的子标记。后两个方法通常放置在被跟踪方法的return语句之前,它们使用EXIT标记,此标记是FLOW的子标记。
throwing()方法可以在一个不太可能被处理的异常(例如RuntimeException)抛出时使用,确保必要的诊断信息可用。该方法产生ERROR级别的日志,使用THROWING标记,此标记是EXCEPTION的子标记。 catching()方法可以在捕获一个异常,并不准备重新抛出的情况下使用。该方法产生ERROR级别的日志,使用CATCHING标记,此标记是EXCEPTION的子标记。
简单示例:
public void exampleException() {
logger.traceEntry();
try {
String msg = messages[messages.length];
logger.error("An exception should have been thrown");
} catch (Exception ex) {
logger.catching(ex);
}
logger.traceExit();
}
我把示例也放到github上:studyLog4j ,路径:/src/main/java/flowtracing/
Marker s(标记)
日志记录框架的主要目的之一是提供一种仅在需要时生成调试和诊断信息的方法,并允许对该信息进行过滤,以使信息不会淹没系统或需要使用该信息的个人。例如,应用程序希望与执行的SQL语句分开记录其进入,退出和其他操作,并希望能够记录与更新分开的查询。按如下代码再在配置文件中添加MarkerFilters来根据Marker进行过滤,实现仅仅对SQL更新操作记录日志,或者记录所有SQL操作日志,或者记录全部应用日志。
private static final Marker SQL_MARKER = MarkerManager.getMarker("SQL");
private static final Marker UPDATE_MARKER = MarkerManager.getMarker("SQL_UPDATE").setParents(SQL_MARKER);
private static final Marker QUERY_MARKER = MarkerManager.getMarker("SQL_QUERY").setParents(SQL_MARKER);
public String doQuery(String table) {
logger.traceEntry();
logger.debug(QUERY_MARKER, "SELECT * FROM {}", table);
String result = ...
return logger.traceExit(result);
}
public String doUpdate(String table, Map<String, String> params) {
logger.traceEntry();
if (logger.isDebugEnabled()) {
logger.debug(UPDATE_MARKER, "UPDATE {} SET {}", table, formatCols());
}
String result = ...
return logger.traceExit(result);
}
使用Marker时,要注意以下规则:
- Marker必须是唯一的。它们是通过名称永久注册的,所以每个标记需要唯一。当然,你特地要使用同样名字也是可以的;
- 父Marker可以被动态的添加、移除。但是这种操作的成本比较昂贵;
- 具有多级祖先的Marker解析起来比较消耗资源 ;
Event Logging
EventLogger类提供了一种用于记录应用程序中发生的事件的简单机制。 尽管EventLogger可用作启动应由审核日志记录系统处理的事件的方式,但EventLogger本身并未实现审核日志记录系统所需的任何功能。
在web 项目中使用 EventLogger 的推荐方法是使用ThreadContext Map,我们可以通过自定义一个 servlet 过滤器,拦截请求,在此之前将相关的信息(例如用户的ID,用户的IP地址,产品名称等)存储在里面 ,然后在请求执行结束时清除。当需要记录的 event 发生时,创建并借助ThreadContext Map填充 StructuredDataMessage,然后调用 EventLogger.logEvent(msg)
,其中 msg 是 StructuredDataMessage 的 引用。
EventLogger类使用名为“ EventLogger”的Logger。 EventLogger使用默认的OFF日志记录级别来表示它不能被过滤。 可以使用StructuredDataLayout格式化这些事件以进行打印。
Messages
虽然Log4j 2提供了接受字符串和对象的Logger方法,但是这些最终都会在Message对象中体现,然后和日志事件相关联。应用程序可以自由构造自己的消息并将其传递给Logger。 尽管看起来比直接将消息格式和参数传递给事件的成本要昂贵,但是测试表明,使用现代JVM,创建和销毁事件的成本很小,尤其是当复杂的任务封装在Message中而不是应用程序中时。 另外,当使用接受字符串和参数的方法时,仅当任何已配置的全局过滤器或Logger的日志级别允许处理消息时,才会创建基础Message对象。
public interface Message extends Serializable {
//返回Message的格式化字符串,你可以实现这个接口自定义格式化方式
String getFormattedMessage();
//返回格式化的方式,例如ParameterizedMessage类,我们熟悉的pattern,打印格式
String getFormat();
//参数
Object[] getParameters();
//异常
Throwable getThrowable();
}
有哪些Message
FormattedMessage,LocalizedMessage,LoggerNameAwareMessage,MapMessage,MessageFormatMessage,MultiformatMessage,StringFormattedMessage,ParameterizedMessage.......
后续我们整个Log4j 2日志打印流程的代码分析再具体讨论这些常见的Message,这里我们大概先简单地有个概念。
Thread Context
在Log4j 1.X中引入了一个叫 Nested Diagnostic Context(NDC)的上下文,log4j 2引入Mapped Diagnostic Context (MDC)【关于MDC可以看logback的文档,介绍的很详细】,并将他们合并成Thread Context。Thread Context Map等效于MDC, Thread Context Stack等效于NDC,由于这些缩写词已广为人知,因此在Log4j 2中仍经常将它们称为MDC和NDC。
Fish Tagging
大多数的系统都是对外向多个客户端提供服务的,我们使用多线程,不同的线程处理不同的客户端发送过来的请求,那么日志对于事件的跟踪和调试是必不可缺的。区分一个客户端的日志输出与另一个客户端的日志记录输出的通用方法是为每个客户端实例化一个新的单独的Logger。 但是这会导致Logger数量的激增,以及增加日志管理的开销。
那么我们可以通过标记来自同一个客户端的日志请求,正如可以标记鱼并跟踪其运动一样,使用通用标签或数据元素集对日志事件进行标记可以跟踪交易或请求的完整流程。 我们称之为Fish Tagging。
Log4j提供了2种方式, Thread Context Map 和Thread Context Stack。Thread Context Map允许使用键/值对添加和标识任意数量的item。Thread Context Stack允许将一个或多个item推入堆栈,然后通过它们在堆栈中的顺序或数据本身进行标识。
由于键/值对更加灵活,因此当在请求的处理过程中可能添加数据项或一个或两个以上项时,建议使用 Thread Context Map
Thread Context Stack:用户将上下文信息压入堆栈
ThreadContext.push(UUID.randomUUID().toString()); // Add the fishtag;
logger.debug("Message 1");
.
.
.
logger.debug("Message 2");
.
.
ThreadContext.pop();
Thread Context Map:与当前请求相关的属性在开头添加,并在结尾删除
ThreadContext.put("id", UUID.randomUUID().toString()); // Add the fishtag;
ThreadContext.put("ipAddress", request.getRemoteAddr());
ThreadContext.put("loginId", session.getAttribute("loginId"));
ThreadContext.put("hostName", request.getServerName());
.
logger.debug("Message 1");
.
.
logger.debug("Message 2");
.
.
ThreadContext.clear();
CloseableThreadContext
将项目放在stack或者map上时,有必要在适当时再次将其删除。 为解决此问题,CloseableThreadContext实现了AutoCloseable接口。 这允许将项目推入stack或放入Map中,并在调用close()方法时将其删除-或自动作为try-with-resources的一部分。 例如,要将某些东西暂时推入stack,然后将其删除:
Thread Context Stack
// Add to the ThreadContext stack for this try block only;
try (final CloseableThreadContext.Instance ctc = CloseableThreadContext.push(UUID.randomUUID().toString())) {
logger.debug("Message 1");
.
.
logger.debug("Message 2");
.
.
}
Thread Context Map:
// Add to the ThreadContext map for this try block only;
try (final CloseableThreadContext.Instance ctc = CloseableThreadContext.put("id", UUID.randomUUID().toString())
.put("loginId", session.getAttribute("loginId"))) {
logger.debug("Message 1");
.
.
logger.debug("Message 2");
.
.
}
实现
Stack 和 Map是按线程管理的,默认情况下基于ThreadLocal。Map 可以通过配置system property的 log4j2.isThreadContextMapInheritable 为 true使用 InheritableThreadLocal,如果使用这种方式,那么Map的内容可以传递给子线程,但是,如在Executors)类中讨论的那样,在使用线程池的其他情况下,ThreadContext可能不会始终自动传递给工作线程。 在这些情况下,池化机制应提供这样做的方法, getContext()和cloneStack()方法可分别用于获取Map和Stack的副本。
注意:ThreadContext类的所有方法都是静态的。