1 引言
一般项目中配置loback日志格式为如下,日志信息将会打印到一行:
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}][%X{spanId}] %logger{36}.%M - %msg%n</pattern>
</encoder>
如果需要将日志接入ES,并且将日志信息拆分成多个字段,那么需要转为json,可以以如下方式配置格式:
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>
{
"time": "%d{dd-MM-yyyy HH:mm:ss.SSS}",
"thread": "%thread",
"level": "%level",
"traceId": "%X{traceId}",
"spanId": "%X{spanId}",
"logger": "%logger{36}",
"method": "%M",
"msg": "%msg"
}%n
</pattern>
</encoder>
但是这种方式打印的key是固定的,并且需要将key-value中的value信息放到MDC中,MDC(基于ThreadLocal)是线程内共享的。也就是说需要一次请求处理中每次打印的value信息相同,否则会被后面的日志覆盖。然而当我们需要每次打印不同key或者相同key不同value时,这种方式就不能支持了。为了解决这个问题,就需要对logback做一些改造。
2 Logback改造
首先我们需要一个用于记录日志信息的实体对象类LogEntity,这里使用了jackson序列化工具,用到其中两个注解:
- @JsonInclude(JsonInclude.Include.NON_NULL) 表示在序列化时忽略null字段。
- @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 表示序列化时将驼峰字段名转为下划线
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
@NoArgsConstructor
@AllArgsConstructor
public class LogEntity {
private String traceId;
private String spanId;
private String time;
private String thread;
private String level;
private Long doctorId;
private Long patientId;
private Long unifiedAccountId;
private String clientIp;
private String url;
private String method;
private String businessKey;
private String businessValue;
private String msg;
@Override
public String toString() {
return getPreFix() + JsonUtil.toJson(this);
}
public byte[] toBytes() {
return (JsonUtil.toJson(this) + "\n").getBytes();
}
public static LogEntity toLogEntity(String str) {
if (StringUtils.isBlank(str) || !str.startsWith(getPreFix()) || str.length() <= getPreFix().length()) {
return null;
}
try {
String logStr = str.substring(getPreFix().length());
return JsonUtil.getMapper().readValue(logStr, LogEntity.class);
} catch (Exception e) {
//不处理异常
}
return null;
}
public static String getPreFix() {
return "##LogEntity##";
}
}
Logback主要有三个组件,分别为Logger、Appender和Layout,其中用于控制输出格式的是Layout,我们可以对Layout做下改造,即继承PatternLayoutEncoder并重写encode方法。
public class CustomJsonEncoder extends PatternLayoutEncoder {
@Override
public byte[] encode(ILoggingEvent event) {
Map<String, String> mdcPropertyMap = event.getMDCPropertyMap();
// 获取traceId和spanId
LogEntity.LogEntityBuilder logEntityBuilder = LogEntity.builder().level(event.getLevel().levelStr)
.time(DateUtil.toFormatDateTime(DateUtil.toLocalDateTimeWithMillisecond(event.getTimeStamp()), "dd-MM-yyyy HH:mm:ss.SSS"))
.thread(event.getThreadName()).traceId(mdcPropertyMap.get("traceId")).spanId(mdcPropertyMap.get("spanId"));
IThrowableProxy err = event.getThrowableProxy();
//错误信息格式化
StringBuilder errMsg = new StringBuilder();
if (err != null) {
errMsg.append("\n");
errMsg.append(err.getClassName());
errMsg.append(" : ");
errMsg.append(err.getMessage());
errMsg.append(" \n ");
StackTraceElementProxy[] errTrack = err.getStackTraceElementProxyArray();
for (StackTraceElementProxy stackTraceElementProxy : errTrack) {
errMsg.append(" at ");
errMsg.append(stackTraceElementProxy.getStackTraceElement());
errMsg.append(" \n ");
}
errMsg.delete(errMsg.length() - 3, errMsg.length());
}
//业务输出日志信息处理
String message = event.getFormattedMessage();
LogEntity businessMessage = LogEntity.toLogEntity(message);
//msg属性特殊处理,如果message不是logEntity格式,则将原始message信息放入msg中
StringBuilder msg = new StringBuilder();
if (businessMessage == null && StringUtils.isNotBlank(message)) {
msg.append(message);
} else if (businessMessage != null && StringUtils.isNotBlank(businessMessage.getMsg())) {
msg.append(businessMessage.getMsg());
}
//将错误信息也放入msg中
msg.append(errMsg);
logEntityBuilder.msg(msg.length() == 0 ? null : msg.toString());
LogEntity logEntity = logEntityBuilder.build();
//将其他信息也拷贝到日志实体对象,忽略msg属性
LogEntityMapper.INSTANCE.update(businessMessage, logEntity);
return logEntity.toBytes();
}
}
在logback.xml中相对应的日志文件Appender配置如下,只需要将原PatternLayoutEncoder改为CustomJsonEncoder
<encoder class="com.dajia.common.log.CustomJsonEncoder">
<pattern>%d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}][%X{spanId}] %logger{36}.%M - %msg%n</pattern>
</encoder>
3 效果
打日志方式如下:
Long originDoctorId = 12345L;
//方式1:reportAction doctorId=12345内容会放到日志的msg字段中
log.info("reportAction doctorId={}", originDoctorId);
//方式2:数据会映射到到对应日志字段
log.info(LogEntity.builder().doctorId(doctorId).msg("reportAction").build().toString());
方法一日志:
{"time":"27-07-2023 12:04:51.000","thread":"main","level":"INFO","msg":"reportAction doctorId=originDoctorId"}
方法二日志:
{"time":"27-07-2023 12:04:51.000","thread":"main","level":"INFO","doctorId": 12345, "msg":"reportAction"}