Logback日志统一格式化为JSON

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"}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值