最近做了一个日志埋点的功能,希望通过无侵入的方式,通过Logback发送日志数据到Kafka。

        熟悉slf4j的小伙伴都知道,在slf4j的上下文中只能有一个实现,Spring Starter已经帮我们默认引入了Logback,所以不需要考虑使用哪一种日志框架了。

        通过Logback打印日志,首先了就需要看它的xml配置文件,一般取名logback.xml或者logback-spring.xml,一般放置resource文件夹下,通常配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <springProperty scope="context" name="LOG_ROOT_LEVEL" source="logging.level.root" defaultValue="INFO"/>
    <springProperty scope="context" name="STDOUT" source="log.stdout" defaultValue="STDOUT"/>
    <springProperty scope="context" name="LOG_PREFIX" source="spring.application.name" defaultValue="app"/>
    <springProperty scope="context" name="LOG_HOME" source="logging.file.path" defaultValue="/log"/>
    <property name="LOG_CHARSET" value="UTF-8" />
    <property name="LOG_DIR" value="${LOG_HOME}/%d{yyyyMMdd}" />
    <property name="LOG_FORMAT" value="%d{HH:mm:ss.SSS} [%thread] %-5level logger_name:%logger{36} - [%tid] - message:%msg%n"/>
    <property name="MAX_FILE_SIZE" value="20MB" />
    <property name="MAX_HISTORY" value="7"/>

    <!--输出到控制台-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                <pattern>${LOG_FORMAT}</pattern>
            </layout>
        </encoder>
    </appender>

    <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${LOG_HOME}/${LOG_PREFIX}_all.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>${LOG_DIR}/${LOG_PREFIX}_all_%d{yyyy-MM-dd}_%i.log</FileNamePattern>
            <MaxHistory>${MAX_HISTORY}</MaxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                <pattern>${LOG_FORMAT}</pattern>
            </layout>
        </encoder>
    </appender>

    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <OnMismatch>DENY</OnMismatch>
            <OnMatch>ACCEPT</OnMatch>
        </filter>
        <File>${LOG_HOME}/${LOG_PREFIX}_err.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>${LOG_DIR}/${LOG_PREFIX}_err_%d{yyyy-MM-dd}_%i.log</FileNamePattern>
            <MaxHistory>${MAX_HISTORY}</MaxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                <pattern>${LOG_FORMAT}</pattern>
            </layout>
        </encoder>
    </appender>

    <appender name="KAFKA_LOG_APPEND" class="com.abc.KafkaLogAppender">
        <layout class="com.abc.LogLayout">
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level logger_name:%logger{36} - [%tid] - [%instance] - [%userid] - [message]:%msg%n</pattern>
        </layout>
    </appender>

    <springProfile name="dev">
        <root level="info">
            <appender-ref ref="FILE_ALL"/>
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="KAFKA_LOG_APPEND"/>
        </root>
    </springProfile>

</configuration>

        通过xml配置文件,能够发现,实际打日志的其实就是Appender了,那在Logback中常用的appender是ConsoleAppender和RollingFileAppender,感兴趣的小伙伴可以自行研究一下。

        在知道了打日志是通过Appender后,那么还需要一个在Appender打日志时,能够转换日志格式的工具,那么在Logback中就是Layout了。

        那如果想在打印的日志中增加一些自己的标签呢,如"%d{HH:mm:ss.SSS} [%thread] %-5level logger_name:%logger{36} - [%tid] - [message]:%msg%n",想在这个日志格式中增加自己的标签,那在Logback中就需要实现自己的ClassicConverter。在这个格式中,如%d{HH:mm:ss.SSS}等标签都是对应不同的ClassicConverter,有兴趣的小伙伴可以自行去研究一下。

        好了,理清思路后,要做3件事情:1.自定义Appender,2.自定义Layout,3.自定义ClassicConverter。ok,那就开始撸代码吧。

1.自定义Appender

@Data
@Slf4j
public class KafkaLogAppender<E> extends UnsynchronizedAppenderBase<ILoggingEvent> {

    private Layout<ILoggingEvent> layout;

    private KafkaProducer kafkaProducer;

    public void start() {
        if (layout == null) {
            addStatus(new ErrorStatus("No layout set for the appender named \"" + name + "\".", this));
        }
        Properties properties = new Properties();
        properties.setProperty("boostrap.server", "your kafka boostrap");
        kafkaProducer = new KafkaProducer(properties);
        super.start();
    }

    @Override
    public void stop() {
        super.stop();
    }

    @Override
    protected void append(ILoggingEvent event) {
        String msg = layout.doLayout(event);
        ProducerRecord producerRecord = new ProducerRecord("your topic", msg);
        kafkaProducer.send(producerRecord);
    }
}

        这里实现了UnsynchronizedAppenderBase接口,当然也可以实现其他的如AppenderBase。这里的ILoggingEvent,所有的日志打印信息都是在ILoggingEvent中,小伙伴们到时候可以自己Debug一下。这里的layout就是慢点我们要实现的自定义layout。这里start()方法,会在日志启动的时候加载,所有kafkaproducer就在这个时候实例化掉。append方法会在实际打日志的时候调用。

2.自定义layout

@Data
public class LogLayout extends TraceIdPatternLogbackLayout {

    private static final String QUOTE = "\"";
    private static final String COLON = ":";
    private static final String COMMA = ",";

    private String appName;

    private CusPatternLogbackUserConverter cusPatternLogbackUserConverter;
    private CusPatternLogbackInstanceConverter cusPatternLogbackInstanceConverter;
    private DateConverter dateConverter;

    public LogLayout() {
    }

    static {
        defaultConverterMap.put("userid", CusPatternLogbackUserConverter.class.getName());
        defaultConverterMap.put("instance", CusPatternLogbackInstanceConverter.class.getName());
    }

    @Override
    public void start() {
        super.start();
        cusPatternLogbackUserConverter = new CusPatternLogbackUserConverter();
        cusPatternLogbackInstanceConverter = new CusPatternLogbackInstanceConverter();
        dateConverter = new DateConverter();
        dateConverter.start();
    }

    @Override
    public String doLayout(ILoggingEvent event) {
        // 原始打印的信息
        String origin = super.doLayout(event);
        // 获取上下文的用户信息
        String userInfo = onConvertersUser(event);
        // 获取上下文TraceId
        String traceId = onConvertersTraceId();
        // Instance信息
        String instance = onConvertersInstance(event);

        // 通过format组装
        StringBuilder sb = new StringBuilder();
        sb.append("{");

        fieldName("level", sb);
        quoto(event.getLevel().levelStr, sb);
        sb.append(COMMA);

        fieldName("logger", sb);
        quoto(event.getLoggerName(), sb);
        sb.append(COMMA);

        fieldName("timestamp", sb);
        quoto(dateConverter.convert(event),sb);
        sb.append(COMMA);

        fieldName("appName", sb);
        quoto(appName, sb);
        sb.append(COMMA);

        fieldName("message", sb);
        quoto(event.getFormattedMessage(), sb);
        sb.append(COMMA);

        fieldName("userInfo", sb);
        sb.append(userInfo);
        sb.append(COMMA);

        fieldName("originLog", sb);
        quoto(origin, sb);
        sb.append(COMMA);

        fieldName("instance", sb);
        quoto(instance, sb);
        sb.append(COMMA);

        fieldName("traceId", sb);
        quoto(traceId, sb);
        sb.append("}");

        return sb.toString();
    }

    protected String onConvertersUser(ILoggingEvent event) {
        String userInfo = cusPatternLogbackUserConverter.convert(event);
        return userInfo;
    }

    protected String onConvertersTraceId() {
        String traceId = TraceContext.traceId();
        return traceId;
    }

    protected String onConvertersInstance(ILoggingEvent event) {
        String instance = cusPatternLogbackInstanceConverter.convert(event);
        return instance;
    }

    private static void fieldName(String name, StringBuilder sb) {
        quoto(name, sb);
        sb.append(COLON);
    }

    private static void quoto(String value, StringBuilder sb) {
        sb.append(QUOTE);
        sb.append(value);
        sb.append(QUOTE);
    }
}

        layout的作用就是进行一个ILoggingEvent到实际打印的一个格式化。这里我继承了Skywalking的一个Layout,当然有个基本的PatternLayoutBase可以去实现。这里能看到defaultConverterMap,增加了userid、instance,这个就是前面说到的自定义标签,如果使用了自定义的layout,就可以在日志格式中加入[%userid]和[%instance]。

3.自定义ClassicConverter

public class CusPatternLogbackUserConverter extends ClassicConverter {

    @Override
    public String convert(ILoggingEvent event) {
        String userInfo = MDC.get("user");
        return StringUtils.isBlank(userInfo) ? "" : userInfo;
    }
}

        MDC是一个TheadLocal实现的全局的一个类似于Map的一个工具类,可以传递一些变量。

        到这里,在依照前面logback.xml的配置就可以实现Logback发送kafka了,当然Logback已经帮我们实现了一些,如DBAppender,RabittMQAppender等。

4.提示

        如果使用Spring Boot的话,切记,日志和Spring是两个上下文,就是两个Context,所以如果想使用Spring里面的一些功能的话,就要自己通过Spring的Context来获取Bean或者其他一些信息了。

以上就是Logback发送Kafka了,如有不对,请指出,谢谢。