mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-10-31 10:18:42 +08:00 
			
		
		
		
	【优化】增强访问日志,支持是否记录、脱敏、操作信息等功能
This commit is contained in:
		| @@ -1,6 +1,7 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.config; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.apilog.core.filter.ApiAccessLogFilter; | ||||
| import cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; | ||||
| @@ -10,23 +11,26 @@ import cn.iocoder.yudao.framework.web.config.WebProperties; | ||||
| import cn.iocoder.yudao.framework.web.config.YudaoWebAutoConfiguration; | ||||
| import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi; | ||||
| import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi; | ||||
| import jakarta.servlet.Filter; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.boot.autoconfigure.AutoConfiguration; | ||||
| import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | ||||
| import org.springframework.boot.web.servlet.FilterRegistrationBean; | ||||
| import org.springframework.context.annotation.Bean; | ||||
|  | ||||
| import jakarta.servlet.Filter; | ||||
| import org.springframework.web.servlet.config.annotation.InterceptorRegistry; | ||||
| import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||||
|  | ||||
| @AutoConfiguration(after = YudaoWebAutoConfiguration.class) | ||||
| public class YudaoApiLogAutoConfiguration { | ||||
| public class YudaoApiLogAutoConfiguration implements WebMvcConfigurer { | ||||
|  | ||||
|     @Bean | ||||
|     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") | ||||
|     public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) { | ||||
|         return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi); | ||||
|     } | ||||
|  | ||||
|     @Bean | ||||
|     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") | ||||
|     public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) { | ||||
|         return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi); | ||||
|     } | ||||
| @@ -49,4 +53,9 @@ public class YudaoApiLogAutoConfiguration { | ||||
|         return bean; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void addInterceptors(InterceptorRegistry registry) { | ||||
|         registry.addInterceptor(new ApiAccessLogInterceptor()); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,65 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.annotations; | ||||
|  | ||||
| import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; | ||||
|  | ||||
| import java.lang.annotation.ElementType; | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| import java.lang.annotation.Target; | ||||
|  | ||||
| /** | ||||
|  * 访问日志注解 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @Target({ElementType.METHOD}) | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| public @interface ApiAccessLog { | ||||
|  | ||||
|     // ========== 开关字段 ========== | ||||
|  | ||||
|     /** | ||||
|      * 是否记录访问日志 | ||||
|      */ | ||||
|     boolean enable() default true; | ||||
|     /** | ||||
|      * 是否记录请求参数 | ||||
|      * | ||||
|      * 默认记录,主要考虑请求数据一般不大。可手动设置为 false 进行关闭 | ||||
|      */ | ||||
|     boolean requestEnable() default true; | ||||
|     /** | ||||
|      * 是否记录响应结果 | ||||
|      * | ||||
|      * 默认不记录,主要考虑响应数据可能比较大。可手动设置为 true 进行打开 | ||||
|      */ | ||||
|     boolean responseEnable() default false; | ||||
|     /** | ||||
|      * 敏感参数数组 | ||||
|      * | ||||
|      * 添加后,请求参数、响应结果不会记录该参数 | ||||
|      */ | ||||
|     String[] sanitizeKeys() default {}; | ||||
|  | ||||
|     // ========== 模块字段 ========== | ||||
|  | ||||
|     /** | ||||
|      * 操作模块 | ||||
|      * | ||||
|      * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.tags.Tag#name()} 属性 | ||||
|      */ | ||||
|     String operateModule() default ""; | ||||
|     /** | ||||
|      * 操作名 | ||||
|      * | ||||
|      * 为空时,会尝试读取 {@link io.swagger.v3.oas.annotations.Operation#summary()} 属性 | ||||
|      */ | ||||
|     String operateName() default ""; | ||||
|     /** | ||||
|      * 操作分类 | ||||
|      * | ||||
|      * 实际并不是数组,因为枚举不能设置 null 作为默认值 | ||||
|      */ | ||||
|     OperateTypeEnum[] operateType() default {}; | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,51 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.enums; | ||||
|  | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Getter; | ||||
|  | ||||
| /** | ||||
|  * 操作日志的操作类型 | ||||
|  * | ||||
|  * @author ruoyi | ||||
|  */ | ||||
| @Getter | ||||
| @AllArgsConstructor | ||||
| public enum OperateTypeEnum { | ||||
|  | ||||
|     /** | ||||
|      * 查询 | ||||
|      */ | ||||
|     GET(1), | ||||
|     /** | ||||
|      * 新增 | ||||
|      */ | ||||
|     CREATE(2), | ||||
|     /** | ||||
|      * 修改 | ||||
|      */ | ||||
|     UPDATE(3), | ||||
|     /** | ||||
|      * 删除 | ||||
|      */ | ||||
|     DELETE(4), | ||||
|     /** | ||||
|      * 导出 | ||||
|      */ | ||||
|     EXPORT(5), | ||||
|     /** | ||||
|      * 导入 | ||||
|      */ | ||||
|     IMPORT(6), | ||||
|     /** | ||||
|      * 其它 | ||||
|      * | ||||
|      * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 | ||||
|      */ | ||||
|     OTHER(0); | ||||
|  | ||||
|     /** | ||||
|      * 类型 | ||||
|      */ | ||||
|     private final Integer type; | ||||
|  | ||||
| } | ||||
| @@ -1,38 +1,56 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.filter; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.date.LocalDateTimeUtil; | ||||
| import cn.hutool.core.exceptions.ExceptionUtil; | ||||
| import cn.hutool.core.map.MapUtil; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLog; | ||||
| import cn.hutool.core.util.ArrayUtil; | ||||
| import cn.hutool.core.util.BooleanUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.iocoder.yudao.framework.apilog.core.annotations.ApiAccessLog; | ||||
| import cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiAccessLogFrameworkService; | ||||
| import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | ||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||
| import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||||
| import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; | ||||
| import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | ||||
| import cn.iocoder.yudao.framework.web.config.WebProperties; | ||||
| import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter; | ||||
| import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
|  | ||||
| import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; | ||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import jakarta.servlet.FilterChain; | ||||
| import jakarta.servlet.ServletException; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import jakarta.servlet.http.HttpServletResponse; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.web.bind.annotation.RequestMethod; | ||||
| import org.springframework.web.method.HandlerMethod; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.time.LocalDateTime; | ||||
| import java.time.temporal.ChronoUnit; | ||||
| import java.util.Iterator; | ||||
| import java.util.Map; | ||||
|  | ||||
| import static cn.iocoder.yudao.framework.apilog.core.interceptor.ApiAccessLogInterceptor.*; | ||||
| import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; | ||||
|  | ||||
| /** | ||||
|  * API 访问日志 Filter | ||||
|  * | ||||
|  * 目的:记录 API 访问日志到数据库中 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @Slf4j | ||||
| public class ApiAccessLogFilter extends ApiRequestFilter { | ||||
|  | ||||
|     private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"}; | ||||
|  | ||||
|     private final String applicationName; | ||||
|  | ||||
|     private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; | ||||
| @@ -44,6 +62,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter { | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     @SuppressWarnings("NullableProblems") | ||||
|     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||||
|             throws ServletException, IOException { | ||||
|         // 获得开始时间 | ||||
| @@ -66,45 +85,166 @@ public class ApiAccessLogFilter extends ApiRequestFilter { | ||||
|  | ||||
|     private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime, | ||||
|                                     Map<String, String> queryString, String requestBody, Exception ex) { | ||||
|         ApiAccessLog accessLog = new ApiAccessLog(); | ||||
|         ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO(); | ||||
|         try { | ||||
|             this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex); | ||||
|             boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex); | ||||
|             if (!enable) { | ||||
|                 return; | ||||
|             } | ||||
|             apiAccessLogFrameworkService.createApiAccessLog(accessLog); | ||||
|         } catch (Throwable th) { | ||||
|             log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void buildApiAccessLogDTO(ApiAccessLog accessLog, HttpServletRequest request, LocalDateTime beginTime, | ||||
|     private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime, | ||||
|                                       Map<String, String> queryString, String requestBody, Exception ex) { | ||||
|         // 判断:是否要记录操作日志 | ||||
|         HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ATTRIBUTE_HANDLER_METHOD); | ||||
|         ApiAccessLog accessLogAnnotation = null; | ||||
|         if (handlerMethod != null) { | ||||
|             accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class); | ||||
|             if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // 处理用户信息 | ||||
|         accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); | ||||
|         accessLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); | ||||
|         accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)) | ||||
|                 .setUserType(WebFrameworkUtils.getLoginUserType(request)); | ||||
|         // 设置访问结果 | ||||
|         CommonResult<?> result = WebFrameworkUtils.getCommonResult(request); | ||||
|         if (result != null) { | ||||
|             accessLog.setResultCode(result.getCode()); | ||||
|             accessLog.setResultMsg(result.getMsg()); | ||||
|             accessLog.setResultCode(result.getCode()).setResultMsg(result.getMsg()); | ||||
|         } else if (ex != null) { | ||||
|             accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()); | ||||
|             accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); | ||||
|             accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()) | ||||
|                     .setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); | ||||
|         } else { | ||||
|             accessLog.setResultCode(0); | ||||
|             accessLog.setResultMsg(""); | ||||
|             accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()).setResultMsg(""); | ||||
|         } | ||||
|         // 设置请求字段 | ||||
|         accessLog.setTraceId(TracerUtils.getTraceId()).setApplicationName(applicationName) | ||||
|                 .setRequestUrl(request.getRequestURI()).setRequestMethod(request.getMethod()) | ||||
|                 .setUserAgent(ServletUtils.getUserAgent(request)).setUserIp(ServletUtils.getClientIP(request)); | ||||
|         String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null; | ||||
|         Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE; | ||||
|         if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false | ||||
|             Map<String, Object> requestParams = MapUtil.<String, Object>builder() | ||||
|                     .put("query", sanitizeMap(queryString, sanitizeKeys)) | ||||
|                     .put("body", sanitizeJson(requestBody, sanitizeKeys)).build(); | ||||
|             accessLog.setRequestParams(toJsonString(requestParams)); | ||||
|         } | ||||
|         Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE; | ||||
|         if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true | ||||
|             accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys)); | ||||
|         } | ||||
|         // 设置其它字段 | ||||
|         accessLog.setTraceId(TracerUtils.getTraceId()); | ||||
|         accessLog.setApplicationName(applicationName); | ||||
|         accessLog.setRequestUrl(request.getRequestURI()); | ||||
|         Map<String, Object> requestParams = MapUtil.<String, Object>builder().put("query", queryString).put("body", requestBody).build(); | ||||
|         accessLog.setRequestParams(toJsonString(requestParams)); | ||||
|         accessLog.setRequestMethod(request.getMethod()); | ||||
|         accessLog.setUserAgent(ServletUtils.getUserAgent(request)); | ||||
|         accessLog.setUserIp(ServletUtils.getClientIP(request)); | ||||
|         // 持续时间 | ||||
|         accessLog.setBeginTime(beginTime); | ||||
|         accessLog.setEndTime(LocalDateTime.now()); | ||||
|         accessLog.setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS)); | ||||
|         accessLog.setBeginTime(beginTime).setEndTime(LocalDateTime.now()) | ||||
|                 .setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS)); | ||||
|  | ||||
|         // 操作模块 | ||||
|         if (handlerMethod != null) { | ||||
|             Tag tagAnnotation = handlerMethod.getBeanType().getAnnotation(Tag.class); | ||||
|             Operation operationAnnotation = handlerMethod.getMethodAnnotation(Operation.class); | ||||
|             String operateModule = accessLogAnnotation != null ? accessLogAnnotation.operateModule() : | ||||
|                     tagAnnotation != null ? StrUtil.nullToDefault(tagAnnotation.name(), tagAnnotation.description()) : null; | ||||
|             String operateName = accessLogAnnotation != null ? accessLogAnnotation.operateName() : | ||||
|                     operationAnnotation != null ? operationAnnotation.summary() : null; | ||||
|             OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ? | ||||
|                     accessLogAnnotation.operateType()[0] : parseOperateLogType(request); | ||||
|             accessLog.setOperateModule(operateModule).setOperateName(operateName).setOperateType(operateType.getType()); | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     // ========== 解析 @ApiAccessLog、@Swagger 注解  ========== | ||||
|  | ||||
|     private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) { | ||||
|         RequestMethod requestMethod = RequestMethod.resolve(request.getMethod()); | ||||
|         if (requestMethod == null) { | ||||
|             return OperateTypeEnum.OTHER; | ||||
|         } | ||||
|         switch (requestMethod) { | ||||
|             case GET: | ||||
|                 return OperateTypeEnum.GET; | ||||
|             case POST: | ||||
|                 return OperateTypeEnum.CREATE; | ||||
|             case PUT: | ||||
|                 return OperateTypeEnum.UPDATE; | ||||
|             case DELETE: | ||||
|                 return OperateTypeEnum.DELETE; | ||||
|             default: | ||||
|                 return OperateTypeEnum.OTHER; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ========== | ||||
|  | ||||
|     private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) { | ||||
|         if (CollUtil.isNotEmpty(map)) { | ||||
|             return null; | ||||
|         } | ||||
|         if (sanitizeKeys != null) { | ||||
|             MapUtil.removeAny(map, sanitizeKeys); | ||||
|         } | ||||
|         MapUtil.removeAny(map, SANITIZE_KEYS); | ||||
|         return JsonUtils.toJsonString(map); | ||||
|     } | ||||
|  | ||||
|     private static String sanitizeJson(String jsonString, String[] sanitizeKeys) { | ||||
|         if (StrUtil.isEmpty(jsonString)) { | ||||
|             return null; | ||||
|         } | ||||
|         try { | ||||
|             JsonNode rootNode = JsonUtils.parseTree(jsonString); | ||||
|             sanitizeJson(rootNode, sanitizeKeys); | ||||
|             return JsonUtils.toJsonString(rootNode); | ||||
|         } catch (Exception e) { | ||||
|             // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 | ||||
|             log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); | ||||
|             return jsonString; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static String sanitizeJson(CommonResult<?> commonResult, String[] sanitizeKeys) { | ||||
|         if (commonResult == null) { | ||||
|             return null; | ||||
|         } | ||||
|         String jsonString = toJsonString(commonResult); | ||||
|         try { | ||||
|             JsonNode rootNode = JsonUtils.parseTree(jsonString); | ||||
|             sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉 | ||||
|             return JsonUtils.toJsonString(rootNode); | ||||
|         } catch (Exception e) { | ||||
|             // 脱敏失败的情况下,直接忽略异常,避免影响用户请求 | ||||
|             log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e); | ||||
|             return jsonString; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) { | ||||
|         // 情况一:数组,遍历处理 | ||||
|         if (node.isArray()) { | ||||
|             for (JsonNode childNode : node) { | ||||
|                 sanitizeJson(childNode, sanitizeKeys); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|         // 情况二:非 Object,只是某个值,直接返回 | ||||
|         if (!node.isObject()) { | ||||
|             return; | ||||
|         } | ||||
|         //  情况三:Object,遍历处理 | ||||
|         Iterator<Map.Entry<String, JsonNode>> iterator = node.properties().iterator(); | ||||
|         while (iterator.hasNext()) { | ||||
|             Map.Entry<String, JsonNode> entry = iterator.next(); | ||||
|             if (ArrayUtil.contains(sanitizeKeys, entry.getKey()) | ||||
|                 || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) { | ||||
|                 iterator.remove(); | ||||
|                 continue; | ||||
|             } | ||||
|             sanitizeJson(entry.getValue(), sanitizeKeys); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,67 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.interceptor; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | ||||
| import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import jakarta.servlet.http.HttpServletResponse; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.util.StopWatch; | ||||
| import org.springframework.web.method.HandlerMethod; | ||||
| import org.springframework.web.servlet.HandlerInterceptor; | ||||
|  | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * API 访问日志 Interceptor | ||||
|  * | ||||
|  * 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @Slf4j | ||||
| public class ApiAccessLogInterceptor implements HandlerInterceptor { | ||||
|  | ||||
|     public static String ATTRIBUTE_HANDLER_METHOD = "HANDLER_METHOD"; | ||||
|  | ||||
|     private static String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch"; | ||||
|  | ||||
|     @Override | ||||
|     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { | ||||
|         // 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用 | ||||
|         HandlerMethod handlerMethod = handler instanceof HandlerMethod ? (HandlerMethod) handler : null; | ||||
|         if (handlerMethod != null) { | ||||
|             request.setAttribute(ATTRIBUTE_HANDLER_METHOD, handlerMethod); | ||||
|         } | ||||
|  | ||||
|         // 打印 request 日志 | ||||
|         if (!SpringUtils.isProd()) { | ||||
|             Map<String, String> queryString = ServletUtils.getParamMap(request); | ||||
|             String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; | ||||
|             if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) { | ||||
|                 log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI()); | ||||
|             } else { | ||||
|                 log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(), | ||||
|                         StrUtil.nullToDefault(requestBody, queryString.toString())); | ||||
|             } | ||||
|             // 计时 | ||||
|             StopWatch stopWatch = new StopWatch(); | ||||
|             stopWatch.start(); | ||||
|             request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch); | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { | ||||
|         // 打印 response 日志 | ||||
|         if (!SpringUtils.isProd()) { | ||||
|             StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH); | ||||
|             stopWatch.stop(); | ||||
|             log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]", | ||||
|                     request.getRequestURI(), stopWatch.getTotalTimeMillis()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -1,85 +0,0 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.service; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import jakarta.validation.constraints.NotNull; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| /** | ||||
|  * API 访问日志 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @Data | ||||
| public class ApiAccessLog { | ||||
|  | ||||
|     /** | ||||
|      * 链路追踪编号 | ||||
|      */ | ||||
|     private String traceId; | ||||
|     /** | ||||
|      * 用户编号 | ||||
|      */ | ||||
|     private Long userId; | ||||
|     /** | ||||
|      * 用户类型 | ||||
|      */ | ||||
|     private Integer userType; | ||||
|     /** | ||||
|      * 应用名 | ||||
|      */ | ||||
|     @NotNull(message = "应用名不能为空") | ||||
|     private String applicationName; | ||||
|  | ||||
|     /** | ||||
|      * 请求方法名 | ||||
|      */ | ||||
|     @NotNull(message = "http 请求方法不能为空") | ||||
|     private String requestMethod; | ||||
|     /** | ||||
|      * 访问地址 | ||||
|      */ | ||||
|     @NotNull(message = "访问地址不能为空") | ||||
|     private String requestUrl; | ||||
|     /** | ||||
|      * 请求参数 | ||||
|      */ | ||||
|     @NotNull(message = "请求参数不能为空") | ||||
|     private String requestParams; | ||||
|     /** | ||||
|      * 用户 IP | ||||
|      */ | ||||
|     @NotNull(message = "ip 不能为空") | ||||
|     private String userIp; | ||||
|     /** | ||||
|      * 浏览器 UA | ||||
|      */ | ||||
|     @NotNull(message = "User-Agent 不能为空") | ||||
|     private String userAgent; | ||||
|  | ||||
|     /** | ||||
|      * 开始请求时间 | ||||
|      */ | ||||
|     @NotNull(message = "开始请求时间不能为空") | ||||
|     private LocalDateTime beginTime; | ||||
|     /** | ||||
|      * 结束请求时间 | ||||
|      */ | ||||
|     @NotNull(message = "结束请求时间不能为空") | ||||
|     private LocalDateTime endTime; | ||||
|     /** | ||||
|      * 执行时长,单位:毫秒 | ||||
|      */ | ||||
|     @NotNull(message = "执行时长不能为空") | ||||
|     private Integer duration; | ||||
|     /** | ||||
|      * 结果码 | ||||
|      */ | ||||
|     @NotNull(message = "错误码不能为空") | ||||
|     private Integer resultCode; | ||||
|     /** | ||||
|      * 结果提示 | ||||
|      */ | ||||
|     private String resultMsg; | ||||
|  | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.service; | ||||
|  | ||||
| import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; | ||||
|  | ||||
| /** | ||||
|  * API 访问日志 Framework Service 接口 | ||||
|  * | ||||
| @@ -10,7 +12,8 @@ public interface ApiAccessLogFrameworkService { | ||||
|     /** | ||||
|      * 创建 API 访问日志 | ||||
|      * | ||||
|      * @param apiAccessLog API 访问日志 | ||||
|      * @param reqDTO API 访问日志 | ||||
|      */ | ||||
|     void createApiAccessLog(ApiAccessLog apiAccessLog); | ||||
|     void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.service; | ||||
|  | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.iocoder.yudao.module.infra.api.logger.ApiAccessLogApi; | ||||
| import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| @@ -20,8 +19,7 @@ public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkSe | ||||
|  | ||||
|     @Override | ||||
|     @Async | ||||
|     public void createApiAccessLog(ApiAccessLog apiAccessLog) { | ||||
|         ApiAccessLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiAccessLog, ApiAccessLogCreateReqDTO.class); | ||||
|     public void createApiAccessLog(ApiAccessLogCreateReqDTO reqDTO) { | ||||
|         apiAccessLogApi.createApiAccessLog(reqDTO); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,107 +0,0 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.service; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import jakarta.validation.constraints.NotNull; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| /** | ||||
|  * API 错误日志 | ||||
|  * | ||||
|  * @author 芋道源码 | ||||
|  */ | ||||
| @Data | ||||
| public class ApiErrorLog { | ||||
|  | ||||
|     /** | ||||
|      * 链路编号 | ||||
|      */ | ||||
|     private String traceId; | ||||
|     /** | ||||
|      * 账号编号 | ||||
|      */ | ||||
|     private Long userId; | ||||
|     /** | ||||
|      * 用户类型 | ||||
|      */ | ||||
|     private Integer userType; | ||||
|     /** | ||||
|      * 应用名 | ||||
|      */ | ||||
|     @NotNull(message = "应用名不能为空") | ||||
|     private String applicationName; | ||||
|  | ||||
|     /** | ||||
|      * 请求方法名 | ||||
|      */ | ||||
|     @NotNull(message = "http 请求方法不能为空") | ||||
|     private String requestMethod; | ||||
|     /** | ||||
|      * 访问地址 | ||||
|      */ | ||||
|     @NotNull(message = "访问地址不能为空") | ||||
|     private String requestUrl; | ||||
|     /** | ||||
|      * 请求参数 | ||||
|      */ | ||||
|     @NotNull(message = "请求参数不能为空") | ||||
|     private String requestParams; | ||||
|     /** | ||||
|      * 用户 IP | ||||
|      */ | ||||
|     @NotNull(message = "ip 不能为空") | ||||
|     private String userIp; | ||||
|     /** | ||||
|      * 浏览器 UA | ||||
|      */ | ||||
|     @NotNull(message = "User-Agent 不能为空") | ||||
|     private String userAgent; | ||||
|  | ||||
|     /** | ||||
|      * 异常时间 | ||||
|      */ | ||||
|     @NotNull(message = "异常时间不能为空") | ||||
|     private LocalDateTime exceptionTime; | ||||
|     /** | ||||
|      * 异常名 | ||||
|      */ | ||||
|     @NotNull(message = "异常名不能为空") | ||||
|     private String exceptionName; | ||||
|     /** | ||||
|      * 异常发生的类全名 | ||||
|      */ | ||||
|     @NotNull(message = "异常发生的类全名不能为空") | ||||
|     private String exceptionClassName; | ||||
|     /** | ||||
|      * 异常发生的类文件 | ||||
|      */ | ||||
|     @NotNull(message = "异常发生的类文件不能为空") | ||||
|     private String exceptionFileName; | ||||
|     /** | ||||
|      * 异常发生的方法名 | ||||
|      */ | ||||
|     @NotNull(message = "异常发生的方法名不能为空") | ||||
|     private String exceptionMethodName; | ||||
|     /** | ||||
|      * 异常发生的方法所在行 | ||||
|      */ | ||||
|     @NotNull(message = "异常发生的方法所在行不能为空") | ||||
|     private Integer exceptionLineNumber; | ||||
|     /** | ||||
|      * 异常的栈轨迹异常的栈轨迹 | ||||
|      */ | ||||
|     @NotNull(message = "异常的栈轨迹不能为空") | ||||
|     private String exceptionStackTrace; | ||||
|     /** | ||||
|      * 异常导致的根消息 | ||||
|      */ | ||||
|     @NotNull(message = "异常导致的根消息不能为空") | ||||
|     private String exceptionRootCauseMessage; | ||||
|     /** | ||||
|      * 异常导致的消息 | ||||
|      */ | ||||
|     @NotNull(message = "异常导致的消息不能为空") | ||||
|     private String exceptionMessage; | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -1,5 +1,7 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.service; | ||||
|  | ||||
| import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; | ||||
|  | ||||
| /** | ||||
|  * API 错误日志 Framework Service 接口 | ||||
|  * | ||||
| @@ -10,7 +12,8 @@ public interface ApiErrorLogFrameworkService { | ||||
|     /** | ||||
|      * 创建 API 错误日志 | ||||
|      * | ||||
|      * @param apiErrorLog API 错误日志 | ||||
|      * @param reqDTO API 错误日志 | ||||
|      */ | ||||
|     void createApiErrorLog(ApiErrorLog apiErrorLog); | ||||
|     void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package cn.iocoder.yudao.framework.apilog.core.service; | ||||
|  | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.iocoder.yudao.module.infra.api.logger.ApiErrorLogApi; | ||||
| import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| @@ -20,8 +19,7 @@ public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkServ | ||||
|  | ||||
|     @Override | ||||
|     @Async | ||||
|     public void createApiErrorLog(ApiErrorLog apiErrorLog) { | ||||
|         ApiErrorLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiErrorLog, ApiErrorLogCreateReqDTO.class); | ||||
|     public void createApiErrorLog(ApiErrorLogCreateReqDTO reqDTO) { | ||||
|         apiErrorLogApi.createApiErrorLog(reqDTO); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.web.core.handler; | ||||
| import cn.hutool.core.exceptions.ExceptionUtil; | ||||
| import cn.hutool.core.map.MapUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLog; | ||||
| import cn.iocoder.yudao.framework.apilog.core.service.ApiErrorLogFrameworkService; | ||||
| import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||||
| import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||||
| @@ -11,6 +10,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||||
| import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; | ||||
| import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | ||||
| import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; | ||||
| import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.apache.commons.lang3.exception.ExceptionUtils; | ||||
| @@ -46,6 +46,7 @@ import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeC | ||||
| @Slf4j | ||||
| public class GlobalExceptionHandler { | ||||
|  | ||||
|     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") | ||||
|     private final String applicationName; | ||||
|  | ||||
|     private final ApiErrorLogFrameworkService apiErrorLogFrameworkService; | ||||
| @@ -237,10 +238,10 @@ public class GlobalExceptionHandler { | ||||
|  | ||||
|     private void createExceptionLog(HttpServletRequest req, Throwable e) { | ||||
|         // 插入错误日志 | ||||
|         ApiErrorLog errorLog = new ApiErrorLog(); | ||||
|         ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO(); | ||||
|         try { | ||||
|             // 初始化 errorLog | ||||
|             initExceptionLog(errorLog, req, e); | ||||
|             buildExceptionLog(errorLog, req, e); | ||||
|             // 执行插入 errorLog | ||||
|             apiErrorLogFrameworkService.createApiErrorLog(errorLog); | ||||
|         } catch (Throwable th) { | ||||
| @@ -248,7 +249,7 @@ public class GlobalExceptionHandler { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void initExceptionLog(ApiErrorLog errorLog, HttpServletRequest request, Throwable e) { | ||||
|     private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) { | ||||
|         // 处理用户信息 | ||||
|         errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); | ||||
|         errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 YunaiV
					YunaiV