Log4J、Log4J2和LogBack
Log4J、Log4J2(log4j的2.0+版本)、Logback都是log日志的实现框架,作者都是Ceki Gülcü或者说是他的团队,最早开发的log4j,并基于slf4j和log4j,优化开发了logback,再后来又用全新的技术disruptor框架重构log4j底层并将logback取其精华重新搭建了一个全新的框架log4j2也就是log4j的2.0+版本,可以这么理解,logback是log4j的升级版,但并没有用log4j那套接口log4j2推翻了log4j和logbak所有底层实现,但是复用了log4j的几大模块和接口。
SLF4J与Log4J、Log4J2和LogBack的关系
SLF4J的全称是Simple Logging Facade for Java,slf4j是门面模式的典型应用。
下面是门面模式的一个典型调用过程,其核心为外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。 下图中客户端不需要直接调用几个子系统,只需要与统一的门面进行通信即可。
Log4J、Log4J2和LogBack使用代码:
// 使用log4j,需要log4j.jar
import org.apache.log4j.Logger;
Logger logger_log4j = Logger.getLogger(Test.class);
logger_log4j.info("Hello World!");
// 使用log4j2,需要log4j-api.jar、log4j-core.jar
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Logger logger_log4j2 = LogManager.getLogger(Test.class);
logger_log4j2.info("Hello World!");
// logback,需要logback-classic.jar、logback-core.jar
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
Logger logger_logback = new LoggerContext().getLogger(Test.class);
logger_logback.info("Hello World!");
从上面不难看出,使用不同的日志框架,就要引入不同的jar包,使用不同的代码获取Logger。如果项目升级需要更换不同的框架,那么就需要修改所有的地方来获取新的Logger,这将会产生巨大的工作量。
基于此,我们需要一种接口来将不同的日志框架的使用统一起来,这也是为什么要使用slf4j的原因。
SLF4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志系统。
注意:类似的日志门面还有Jakarta Common logging(JCL),主要区别在于,SLF4J是一个比较新的日志框架,它更加灵活,性能更好,支持更多的日志实现,而且JCL基于classLoader在运行时动态加载日志框架,可能会产生很多意想不到的安全问题。
通过上面的介绍,我们可以知道JCL和SLF4J都是日志门面(Facade),而Log4J、Log4J2和LogBack都是子系统角色(SunSystem),也就是具体的日志实现框架。他们的关系如下,JUL是JDK本身提供的一种实现。
使用slf4j后,当我们在打印日志时,就可以使用下面的方式:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Logger logger = LoggerFactory.getLogger(Test.class);
logger.info("Hello World!")
日志级别
log4j定义了8个级别的log(除去OFF和ALL,可以说分为6个级别),优先级从高到低依次为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。
ALL:最低等级的,用于打开所有日志记录。
TRACE: designates finer-grained informational events than the DEBUG.Since:1.2.12,很低的日志级别,一般不会使用。
DEBUG: 指出细粒度信息事件对调试应用程序是非常有帮助的,主要用于开发过程中打印一些运行信息。
INFO: 消息在粗粒度级别上突出强调应用程序的运行过程。打印一些你感兴趣的或者重要的信息,这个可以用于生产环境中输出程序运行的一些重要信息,但是不能滥用,避免打印 过多的日志。
WARN: 表明会出现潜在错误的情形,有些信息不是错误信息,但是也要给程序员的一些提示。
ERROR: 指出虽然发生错误事件,但仍然不影响系统的继续运行。打印错误和异常信息,如果不想输出太多的日志,可以使用这个级别。
FATAL: 指出每个严重的错误事件将会导致应用程序的退出。这个级别比较高了。重大错误,这种级别你可以直接停止程序了。
OFF: 最高等级的,用于关闭所有日志记录。
如果将log level设置在某一个级别上,那么比此级别优先级高的log都能打印出来。例如,如果设置优先级为WARN,那么OFF、FATAL、ERROR、WARN 4个级别的log能正常 输出,而INFO、DEBUG、TRACE、 ALL级别的log则会被忽略。Log4j建议只使用四个级别,优先级从高到低分别是ERROR、WARN、INFO、DEBUG。
Log4j与Logback之间的关键区别
Log4j与Logback之间的一些主要区别:
更好的版本:当我们比较log4j和logback的版本时,log4j优于小于1.2.1的logback版本。随着logback的改进,版本log4j和版本log4j2与logback在性能或任何特性方面都没有区别。因此,log4j是logback新版本发明之前使用最多的日志工具。
实现:由于log4j是一个较旧的版本,现在新的logback内部结构得到了改进,在关键执行上的工作速度比log4j快十倍,而log4j中也不支持较小的内存占用。
自动重新加载配置文件:在logback中,通过使用带有修改的logback-classic支持重新加载配置文件。在log4j中,这是通过configureAndWatch方法使用DOMconfigurator PropertyConfigurator完成的。尽管在log4j中通过configureAndWatch方法创建了一个单独的线程,并且该线程无法停止,因此对JEE来说是不安全的,但在logback中,该logback经典在JEE环境中运行良好,因为在扫描过程中不会创建任何线程。
根日志记录器:在logback中,根日志记录器通过元素配置,该元素可以支持级别属性,如跟踪、信息、调试、错误、警告、全部或关闭。而根记录器是由log4j中的调试级别定义的。在logback中,无法继承根记录器的级别,在log4j中,根记录器始终是配置的记录器,并且可以通过子记录器继承。请注意,定义日志记录级别时区分大小写。
过滤器:在log4j中,提供的过滤功能选项比logback少得多。因为在log4j中,日志记录要降低到调试级别,这将创建更多日志记录数据,导致分析日志记录数据的复杂性,降低性能。但这不是回写的情况,因为我们可以在警告级别停止;客户可以在警告级别等待,而不是在调试级别登录并创建更多日志信息。
Appender
在logback.xml中添加
<appender name="JMQ" class="com.jd.interact.luban.service.handle.log.JMQAppender">
自定义appender,实现日志文件发mq,然后mq消费端可以将日志写数据库或ES。
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
/**
* log日志mq消息推送appender
* 批量推送方案对比:
* 1、ThreadLocal存放多条日志,但是由于入口太多,无法进行统一clear缓存信息,而且若脚本New Thread的话,日志条数不满足配置数量导致无法推送
* 2、定时推送策略,若定时推动可能导致时间段内存放的数据太大发生内存泄露等问题
* 3、目前策略:通过现成安全队列推动日志消息,oncurrentLinkedQueue是一种非阻塞的线程安全队列,用CAS来实现线程安全
*/
@Slf4j
public class JMQAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
private MessageProducer messageProducer = JmqConfiguration.getMessageProducer();
// 线程安全的LRU
private static final ConcurrentLinkedQueue<LogEntity> LOG_QUEUE = new ConcurrentLinkedQueue();
private static final AtomicLong COUNT_NUM = new AtomicLong();
@Override
protected void append(ILoggingEvent iLoggingEvent) {
try {
Integer randomValue = ThreadLocalRandom.current().nextInt(100);
String logLevel = iLoggingEvent.getLevel().levelStr;
String loggerName = iLoggingEvent.getLoggerName();
// 校验日志过滤百分比 0为不推送 ,100为全部推送
if (randomValue >= DuccConfig.LOG_SWITCH) {
return;
}
// 校验日志级别
if (!DuccConfig.LOG_LEVEL.contains(logLevel)) {
return;
}
// 日志黑白名单校验,白名单优先级高,日志非完全匹配,
// 如配置白名单 com.jd.test 则所有以com.jd.test开头的日志都会识别为白名单日志,黑名单同理
if (!whiteLog(loggerName) && blackLog(loggerName)) {
return;
}
LogEntity logEntity = buildLogEntity(iLoggingEvent);
ExecutorConfiguration.getLogPushExecutor().execute(() -> {
LOG_QUEUE.offer(logEntity);
Long num = COUNT_NUM.incrementAndGet();
if (num % DuccConfig.LOG_BATCH_NUM == 0) {
pushLogInfo();
}
});
} catch (Throwable ex) {
log.error("append", ex);
}
}
/**
* 组装log日志对象,后续推送mq消息实体
*
* @param iLoggingEvent
* @return
*/
private LogEntity buildLogEntity(ILoggingEvent iLoggingEvent) {
// 日志内容长度限制,防止日志内容过长,默认5000字符
String message = StringUtils.substring(getMessage(iLoggingEvent), 0, DuccConfig.LOG_MESSAGE_LENGTH);
LogEntity logEntity = new LogEntity();
logEntity.setAppName(EnvironmentUtils.getAppName());
logEntity.setEnv(EnvironmentUtils.getEnv());
logEntity.setClusterId(ClusterInfoUtils.getLocalClusterId());
logEntity.setIp(SystemUtil.getHostInfo().getAddress());
logEntity.setLoggerName(iLoggingEvent.getLoggerName());
logEntity.setLogThreadName(Thread.currentThread().getName());
logEntity.setThreadName(iLoggingEvent.getThreadName());
logEntity.setProjectId(WorkflowContext.getProjectId());
logEntity.setLevel(iLoggingEvent.getLevel().levelStr);
logEntity.setTimeStamp(System.currentTimeMillis());
logEntity.setTraceId(WorkflowContext.getTraceId());
logEntity.setWorkflowId(WorkflowContext.getWorkflowId());
logEntity.setMessage(message);
logEntity.setPin(WorkflowContext.getPin());
logEntity.setDataCenter(EnvironmentUtils.getDatacenter());
// projectId为空则记为系统日志,否则为业务日志
String logType = StringUtils.isBlank(logEntity.getProjectId()) ? "系统日志" : "业务日志";
logEntity.setLogType(logType);
return logEntity;
}
private String getMessage(ILoggingEvent iLoggingEvent) {
// 若为异常日志,则打印异常栈信息
if (iLoggingEvent.getThrowableProxy() != null) {
ExtendedThrowableProxyConverter throwableConverter = new ExtendedThrowableProxyConverter();
throwableConverter.start();
return iLoggingEvent.getFormattedMessage() + "\n" + throwableConverter.convert(iLoggingEvent);
}
return iLoggingEvent.getFormattedMessage();
}
private boolean whiteLog(String loogerName) {
for (String logName : DuccConfig.WHITE_LOG_LIST) {
if (loogerName.startsWith(logName)) {
return true;
}
}
return false;
}
private boolean blackLog(String loogerName) {
for (String logName : DuccConfig.BLACK_LOG_LIST) {
if (loogerName.startsWith(logName)) {
return true;
}
}
return false;
}
private void pushLogInfo() {
String umpKey = WorkflowUmpKeyConstants.LUBAN_UMP_PREFIX + UmpEnvUtils.env + "_pushLogInfo";
CallerInfo taskMonitor = UmpUtil.methodReg(umpKey);
try {
List<LogEntity> logEntities = Lists.newArrayList();
// 循环达到批次数量或者队列中日志已读取完成则进行推送
while (logEntities.size() < DuccConfig.LOG_BATCH_NUM) {
LogEntity logEntity = LOG_QUEUE.poll();
if (logEntity != null) {
logEntities.add(logEntity);
} else {
break;
}
}
if (CollectionUtils.isNotEmpty(logEntities)) {
// TODO 发log mq
Message message = new Message(MainConstants.LOG_TOPIC, JsonUtils.toJsonString(logEntities), WorkflowContext.getWorkflowId());
messageProducer.send(message, new AsyncSendCallback() {
@Override
public void success(PutMessage putMessage, Command command) {
}
@Override
public void execption(PutMessage putMessage, Throwable throwable) {
log.error("mqCallBackException", throwable);
}
});
}
} catch (Exception ex) {
log.error("pushLogInfo", ex);
UmpUtil.funcError(taskMonitor);
} finally {
UmpUtil.methodRegEnd(taskMonitor);
}
}
public static Integer getLogQueueLength() {
return LOG_QUEUE.size();
}
}