缘起

随着系统微服务化的开展,越来越多的服务被建立起来,而且服务都布署到公司私有云平台上。这个进程中,日志查看的问题逐渐暴露出来。

首先,分布式系统,程序部署在不同的机器上,查询日志需要登录到各台机器上,很不方便;其次,云服务使用 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