springboot logback日志+异常+阿里云日志 aliyun-log-logback-appender
前言
最近有个新项目用了,springboot3.0,以前项目日志保存得方式是阿里云云服务自动读取日志文件,最近项目部署得方式可能有变化,所以新项目用logback+aliyun-log-logback-appender得方式保存到阿里云日志服务。用logback得原因主要是懒,spring默认就是这个,其他还要各种配置和兼容。
重点
通过配置MDC控制保存到阿里云的数据,logback-spring.xml要配置对应的mdcFields
通过ContentCachingRequestWrapper和ContentCachingResponseWrapper取入参和返回数据,这两个不需要太多代码
RestControllerAdvice+ExceptionHandler全局捕获异常并处理
gradle
implementation 'com.google.protobuf:protobuf-java:2.5.0'
implementation 'com.aliyun.openservices:aliyun-log-logback-appender:0.1.18'
implementation 'org.slf4j:slf4j-nop:1.7.25'
阿里云日志应该只需要这么多得引入。
logback-spring.xml 日志配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- 项目名称 -->
<property name="PROJECT_NAME" value="apiv3"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %highlight([%-5level] [%thread] %logger{50} - %msg%n)</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!--为了防止进程退出时,内存中的数据丢失,请加上此选项-->
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/>
<appender name="apiv3Log" class="com.aliyun.openservices.log.logback.LoghubAppender">
<!--必选项-->
<!-- 账号及网络配置 -->
<endpoint>cn-beijing.log.aliyuncs.com</endpoint>
<accessKeyId>####</accessKeyId>
<accessKeySecret>###</accessKeySecret>
<!-- sls 项目配置 -->
<project>#####</project>
<!--开发环境区分 -->
<springProfile name="dev">
<logStore>####dev</logStore>
</springProfile>
<!--开发环境区分 -->
<springProfile name="test">
<logStore>###test</logStore>
</springProfile>
<!--必选项 (end)-->
<!-- 可选项 -->
<!-- <topic>your topic</topic>-->
<!-- <source>your source</source>-->
<!-- 可选项 详见 '参数说明'-->
<totalSizeInBytes>104857600</totalSizeInBytes>
<maxBlockMs>0</maxBlockMs>
<ioThreadCount>8</ioThreadCount>
<batchSizeThresholdInBytes>524288</batchSizeThresholdInBytes>
<batchCountThreshold>4096</batchCountThreshold>
<lingerMs>2000</lingerMs>
<retries>10</retries>
<baseRetryBackoffMs>100</baseRetryBackoffMs>
<maxRetryBackoffMs>50000</maxRetryBackoffMs>
<!-- 可选项 通过配置 encoder 的 pattern 自定义 log 的格式 -->
<!-- <encoder>-->
<!-- <pattern>%d %-5level [%thread] %X{traceId} %logger{0}: %msg</pattern>–>-->
<!-- <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] %highlight([%-5level] [%thread] %logger{50} - %msg%n)</pattern>-->
<!-- <charset>UTF-8</charset>-->
<!-- </encoder>-->
<!-- 可选项 设置 time 字段呈现的格式 -->
<timeFormat>yyyy-MM-dd'T'HH:mmZ</timeFormat>
<!-- 可选项 设置 time 字段呈现的时区 -->
<timeZone>UTC+8</timeZone>
<mdcFields>
TraceId,#####
</mdcFields>
</appender>
<!--TRACE < DEBUG < INFO < WARN < ERROR < FATAL-->
<!-- 开发环境下的日志配置 -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="apiv3Log"/>
</root>
</springProfile>
<springProfile name="test">
<root level="INFO">
<!-- <appender-ref ref="CONSOLE"/>-->
<appender-ref ref="apiv3Log"/>
</root>
</springProfile>
<!-- 生产环境下的日志配置 -->
<springProfile name="prod">
<root level="INFO">
。。。。。。
</root>
</springProfile>
</configuration>
这是logback-spring.xml 基本配置。放在resources目录中。
###基本都是阿里云中得一些参数信息,熟悉阿里云日志的,都明白。
mdcFields也是我最近才发现,它会把MDC中的中对应不为null的字段保存在阿里云中。
HttpFilter
因为在日志中要记录请求的参数和返回值,aop实现有点负责,所以用了HttpFilter
@Component
@WebFilter(filterName = "accessLogFilter", urlPatterns = "/*")
@Order(-9999) // 保证最先执行
public class AccessLogFilter extends HttpFilter {
private static final Logger logger = LoggerFactory.getLogger(AccessLogAspect.class);
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
ContentCachingRequestWrapper cachingRequestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper cachingResponseWrapper = new ContentCachingResponseWrapper(response);
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("user-agent"));
// 接收到请求,记录请求内容````
MDC.put("TraceId", TraceIDUtils.getTraceId());
//客户端类型
MDC.put("ClientType", userAgent.getOperatingSystem().getDeviceType().getName());
//客户端操作系统类型
MDC.put("OsType", userAgent.getOperatingSystem().getName());
MDC.put("ClientMethod", request.getMethod());
MDC.put("ClientIp", IpUtil.getIpAddress(request));
MDC.put("ClientParam", request.getQueryString());
MDC.put("ClientUrl", request.getRequestURL().toString());
MDC.put("ClientUri", request.getRequestURI());
//记录请求开始时间
long start = RequestTimeUtil.getStart();
MDC.put("RequestStart", String.valueOf(start));
//请求头参数
Map<Object, Object> headerMap = new HashMap<>();
Enumeration<String> enumeration = cachingRequestWrapper.getHeaderNames();
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String value = request.getHeader(name);
headerMap.put(name, value);
}
MDC.put("RequestHeader", JSON.toJSONString(headerMap));
MDC.put("LogType", LogTypEnum.requestStart.name());
MDC.put("LogTime", String.valueOf(System.currentTimeMillis()));
//日志输出
logger.info("--------------request start--------------");
//这里清理掉MDC 防止变成下一个log输出的垃圾数据
MDC.remove("LogTime");
MDC.remove("ClientType");
MDC.remove("OsType");
MDC.remove("ClientMethod");
MDC.remove("ClientIp");
MDC.remove("ClientParam");
MDC.remove("ClientUrl");
MDC.remove("ClientUri");
MDC.remove("RequestStart");
MDC.remove("LogType");
MDC.remove("RequestHeader");
super.doFilter(cachingRequestWrapper, cachingResponseWrapper, chain);
String responseBody = new String(cachingResponseWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
MDC.put("LogType", LogTypEnum.requestEnd.name());
MDC.put("RequestParamMap", JSON.toJSONString(cachingRequestWrapper.getParameterMap()));
String requestBodyParam = new String(cachingRequestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
MDC.put("RequestBodyParam", requestBodyParam);
MDC.put("ReturnData", responseBody);
long end = RequestTimeUtil.getEnd();
MDC.put("RequestEnd", String.valueOf(end));
MDC.put("RequestProcessTime", String.valueOf(end - start));
MDC.put("LogTime", String.valueOf(System.currentTimeMillis()));
logger.info("--------------request end--------------");
// 这一步很重要,把缓存的响应内容,输出到客户端
cachingResponseWrapper.copyBodyToResponse();
MDC.clear();
RequestTimeUtil.remove();
TraceIDUtils.remove();
}
找了很久才找到ContentCachingRequestWrapper和ContentCachingResponseWrapper这两个可以取入参和返回数据的类,不用动其他代码就可以实现了。
其实到这里最基础的请求日志,已经实现了。
大概就是这样子的。
其他aop的日志也是差不多的方式去处理。
异常处理
RestControllerAdvice+ExceptionHandler全局捕获异常,ResultError是自定义异常,主要处理一些assert抛出的错误。
@RestControllerAdvice
public class ExceptionControllerAdvice {
private static final Logger logger = LoggerFactory.getLogger(AccessLogAspect.class);
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 从异常对象中拿到ObjectError对象
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
// 然后提取错误提示信息进行返回
return Result.fail(GlobalResultEnum.fail.getCode(), objectError.getDefaultMessage());
}
@ExceptionHandler(ResultError.class)
public Result<Object> APIExceptionHandler(ResultError e) {
MDC.put("ResponseStatus", String.valueOf(e.getResultEnum().getCode()));
MDC.put("ResponseGlobalStatus", String.valueOf(e.getResultEnum().getGlobalStatus()));
MDC.put("ResponseMsg", e.getResultEnum().getMsg());
MDC.put("LogType", LogTypEnum.resultError.name());
MDC.put("LogTime", String.valueOf(System.currentTimeMillis()));
logger.info(JSON.toJSONString(e.getErrorMessage()));
MDC.remove("LogTime");
MDC.remove("ResponseStatus");
MDC.remove("ResponseGlobalStatus");
MDC.remove("ResponseMsg");
MDC.remove("LogType");
return Result.of(e);
}
}
public class ResultError extends RuntimeException {
@Getter
private ResultEnum resultEnum;
@Getter
private Object errorMessage;
public ResultError(ResultEnum resultEnum) {
super(resultEnum.getMsg());
this.resultEnum = resultEnum;
}
public ResultError(ResultEnum resultEnum, Object errorMessage) {
super(resultEnum.getMsg());
this.resultEnum = resultEnum;
this.errorMessage = errorMessage;
}
}
public interface ResultEnum {
/**
* 状态码
* @return int
*/
int getCode();
/**
* 全局状态码
* @return int
*/
int getGlobalStatus();
/**
* 描述信息
* @return String
*/
String getMsg();
}
ResultEnum 是全局状态接口,所有的状态都可以继承这个类,实现类似这样。
public enum GlobalResultEnum implements ResultEnum {
success(100, "success"),
fail(99, "fail");
private final int code;
private final int globalStatus = 0;
private final String msg;
GlobalResultEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
/**
* 状态码
*
* @return int
*/
@Override
public int getCode() {
return code;
}
/**
* 全局状态码
*
* @return int
*/
@Override
public int getGlobalStatus() {
return globalStatus + code;
}
/**
* 描述信息
*
* @return String
*/
@Override
public String getMsg() {
return msg;
}
}
全局定义唯一状态码(globalStatus),定义全局状态接口(ResultEnum)
也挺麻烦,还需要优化。
写的不是很清晰,欢迎讨论。