最近做了一个日志埋点的功能,希望通过无侵入的方式,通过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了,如有不对,请指出,谢谢。