缘起
随着系统微服务化的开展,越来越多的服务被建立起来,而且服务都布署到公司私有云平台上。这个进程中,日志查看的问题逐渐暴露出来。
首先,分布式系统,程序部署在不同的机器上,查询日志需要登录到各台机器上,很不方便;其次,云服务使用 Docker 虚拟技术,当服务重新部署时,机器上的文件会全部抹除,因此日志文件也会因为服务重启或升级导致丢失。
所以,目前我们团队非常需要一个分布式日志的解决方案。查了一些资料,ELK 是一个比较靠谱的解决方案。但是通过收集机器上的日志文件的方式需要在机器上安装软件,不是很方便。因此最终选择使用日志框架提供的自定义 appender 功能,通过 kafka 上报日志的方式来实现。
然而,我们团队的项目中,各种日志框架可谓百花齐放。老项目使用 log4j,SpringBoot 项目默认是使用 logback,有些项目则采用 log4j2,虽然都有使用 slf4j 来避免直接使用具体日志实现,但是 slf4j 并没有提供 appender 的标准接口,而且这几种日志框架并存的现状难以改变。因此需要兼容这几种日志框架。
经过一番摸索终于搞定了。网上有很多的博文也介绍了怎么使用,但是首先,这些文章都没有提供完整的实例,看代码片段调试起来总会遇到这样那样的问题;其次,都没有说明在配置 appender 时如何配置自定义参数。所以,在解决了这些问题之后,我将日志上报解决方案中的自定义 appender 这部分总结并抽取出来,希望可以帮助到大家。
解决方案
这里解释一下,各个自定义 appender 是如何实现的。
一、logback
maven 依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
代码实现
package com.github.dadiyang.appender;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.classic.spi.ThrowableProxy;
import ch.qos.logback.core.AppenderBase;
/**
* 自定义实现logback的输出源
* @author dadiyang
* @since 2019/4/30
*/
public class LogbackAppender extends AppenderBase<ILoggingEvent> {
private String appName;
@Override
protected void append(ILoggingEvent event) {
String level = event.getLevel().toString();
String loggerName = event.getLoggerName();
String msg = event.getFormattedMessage();
String threadName = event.getThreadName();
Throwable throwable = event.getThrowableProxy() != null ? ((ThrowableProxy) event.getThrowableProxy()).getThrowable() : null;
// todo 这里实现自定义的日志处理逻辑
}
/**
* 定义 setter 方法,这样配置项会被注入到这个 appender 中
*/
public void setAppName(String appName) {
this.appName = appName;
}
}
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" debug="false">
<appender name="CustomAppender" class="com.github.dadiyang.appender.LogbackAppender">
<!-- 这里填写自定义配置项 -->
<appName>test_logback_appender_app</appName>
</appender>
<root level="INFO">
<appender-ref ref="CustomAppender"/>
</root>
</configuration>
二、log4j2
maven 依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>1.7.21</version>
</dependency>
代码实现
package com.github.dadiyang.appender;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.Layout;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
import org.apache.logging.log4j.core.config.plugins.PluginElement;
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
import org.apache.logging.log4j.core.layout.PatternLayout;
import java.io.Serializable;
/**
* 自定义实现log4j2的输出源
* @author dadiyang
* @since 2019/4/30
*/
@Plugin(name = "Log4j2Appender", category = "Core", elementType = "appender", printObject = true)
public final class Log4j2Appender extends AbstractAppender {
private String appName;
protected Log4j2Appender(String name, String appName, Filter filter, Layout<? extends Serializable> layout,
final boolean ignoreExceptions) {
super(name, filter, layout, ignoreExceptions);
this.appName = appName;
}
@Override
public void append(LogEvent event) {
// 此处自定义实现输出
String level = event.getLevel().toString();
String loggerName = event.getLoggerName();
String msg = event.getMessage().getFormattedMessage();
String threadName = event.getThreadName();
Throwable throwable = event.getThrown();
// todo 这里实现自定义的日志处理逻辑
}
/**
* log4j2 使用 appender 插件工厂,因此传参可以直接通过 PluginAttribute 注解注入
*/
@PluginFactory
public static Log4j2Appender createAppender(
@PluginAttribute("name") String name,
@PluginAttribute("appName") String appName,
@PluginElement("Layout") Layout<? extends Serializable> layout,
@PluginElement("Filter") final Filter filter) {
if (layout == null) {
layout = PatternLayout.createDefaultLayout();
}
return new Log4j2Appender(name, appName, filter, layout, true);
}
}
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 这里 packages 要添加我们自定义 appender 所在的包 -->
<configuration status="WARN" packages="com.github.dadiyang.appender">
<appenders>
<!-- 这个就是自定义的Appender -->
<Log4j2Appender name="CustomAppender" appName="test_logback_appender_app"/>
</appenders>
<loggers>
<root level="INFO">
<!-- 注册到全局 appender -->
<appender-ref ref="CustomAppender"/>
</root>
</loggers>
</configuration>
这里配置的 package 需要添加我们的 appender 所在的包,否则可能会出现
Error processing element XXX ([Appenders: null]): CLASS_NOT_FOUND
三、Log4j
maven 依赖
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.2</version>
</dependency>
代码实现
package com.github.dadiyang.appender;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.spi.LoggingEvent;
/**
* @author dadiyang
* @since 2019/4/30
*/
public class Log4jAppender extends AppenderSkeleton {
private String appName;
@Override
protected void append(LoggingEvent event) {
String level = event.getLevel().toString();
String loggerName = event.getLoggerName();
String msg = event.getRenderedMessage();
String threadName = event.getThreadName();
Throwable throwable = event.getThrowableInformation() != null ? event.getThrowableInformation().getThrowable() : null;
// todo 这里实现自定义的日志处理逻辑
}
/**
* 定义 setter 方法,这样在配置文件中添加类似 log4j.appender.CustomAppender.appName=test_app_name 的配置项时,配置会被注入到这个 appender 中
*/
public void setAppName(String appName) {
this.appName = appName;
}
@Override
public void close() {
}
@Override
public boolean requiresLayout() {
return false;
}
}
log4j.properties
# 全局使用
log4j.rootLogger=INFO,CustomAppender
# 注册 Log4jAppender
log4j.appender.CustomAppender=com.github.dadiyang.appender.Log4jAppender
# 这里填写集群名称
log4j.appender.CustomAppender.appName=test_logback_appender_app