diff --git a/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/apilog/config/ViewshApiLogAutoConfiguration.java b/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/apilog/config/ViewshApiLogAutoConfiguration.java index e0cfa569..aa27163a 100644 --- a/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/apilog/config/ViewshApiLogAutoConfiguration.java +++ b/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/apilog/config/ViewshApiLogAutoConfiguration.java @@ -10,14 +10,26 @@ 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.context.properties.bind.DefaultValue; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.Collections; +import java.util.List; + @AutoConfiguration(after = ViewshWebAutoConfiguration.class) public class ViewshApiLogAutoConfiguration implements WebMvcConfigurer { + /** + * 访问日志 Interceptor 的排除路径(Ant Pattern)。 + * 例如:{@code viewsh.access-log.exclude-paths=/index/hook/on_server_keepalive,/actuator/**} + * 命中的请求默认不打 INFO 访问日志,但出现异常或 HTTP 4xx/5xx 时仍会打印 WARN。 + */ + @Value("${viewsh.access-log.exclude-paths:}") + private List excludePaths; + /** * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 */ @@ -38,7 +50,8 @@ public class ViewshApiLogAutoConfiguration implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new ApiAccessLogInterceptor()); + registry.addInterceptor(new ApiAccessLogInterceptor( + excludePaths == null ? Collections.emptyList() : excludePaths)); } } diff --git a/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java b/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java index caefcdbd..bfa73058 100644 --- a/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java +++ b/viewsh-framework/viewsh-spring-boot-starter-web/src/main/java/com/viewsh/framework/apilog/core/interceptor/ApiAccessLogInterceptor.java @@ -9,11 +9,13 @@ import com.viewsh.framework.common.util.spring.SpringUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.AntPathMatcher; import org.springframework.util.StopWatch; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import java.lang.reflect.Method; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -24,6 +26,11 @@ import java.util.stream.IntStream; * * 目的:在非 prod 环境时,打印 request 和 response 两条日志到日志文件(控制台)中。 * + *

支持通过 {@code viewsh.access-log.exclude-paths} 配置排除高频心跳/健康检查类请求的日志刷屏, + * 排除路径支持 Ant Pattern(例如 {@code /index/hook/on_server_keepalive}、{@code /actuator/**})。 + * 被排除的请求只有在 发生异常HTTP 状态码 ≥ 400 时才会打印日志, + * 保证"默认安静、出错必响"。 + * * @author 芋道源码 */ @Slf4j @@ -33,6 +40,37 @@ public class ApiAccessLogInterceptor implements HandlerInterceptor { private static final String ATTRIBUTE_STOP_WATCH = "ApiAccessLogInterceptor.StopWatch"; + private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); + + /** + * 不打印常规访问日志的路径列表(Ant Pattern)。 + * 命中后:preHandle/afterCompletion 默认不打日志,但若发生异常或 4xx/5xx 仍会打印。 + */ + private final List excludePaths; + + public ApiAccessLogInterceptor() { + this(Collections.emptyList()); + } + + public ApiAccessLogInterceptor(List excludePaths) { + this.excludePaths = excludePaths == null ? Collections.emptyList() : excludePaths; + } + + private boolean isExcluded(String uri) { + if (excludePaths.isEmpty() || uri == null) { + return false; + } + for (String pattern : excludePaths) { + if (StrUtil.isBlank(pattern)) { + continue; + } + if (PATH_MATCHER.match(pattern, uri)) { + return true; + } + } + return false; + } + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 记录 HandlerMethod,提供给 ApiAccessLogFilter 使用 @@ -43,6 +81,16 @@ public class ApiAccessLogInterceptor implements HandlerInterceptor { // 打印 request 日志 if (!SpringUtils.isProd()) { + // 计时:无论是否排除都要记,afterCompletion 在异常分支还会用它 + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch); + + // 命中排除路径:保持安静,等 afterCompletion 里按异常/状态码兜底 + if (isExcluded(request.getRequestURI())) { + return true; + } + Map queryString = ServletUtils.getParamMap(request); String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) { @@ -51,10 +99,6 @@ public class ApiAccessLogInterceptor implements HandlerInterceptor { log.info("[preHandle][开始请求 URL({}) 参数({})]", request.getRequestURI(), StrUtil.blankToDefault(requestBody, queryString.toString())); } - // 计时 - StopWatch stopWatch = new StopWatch(); - stopWatch.start(); - request.setAttribute(ATTRIBUTE_STOP_WATCH, stopWatch); // 打印 Controller 路径 printHandlerMethodPosition(handlerMethod); } @@ -66,9 +110,26 @@ public class ApiAccessLogInterceptor implements HandlerInterceptor { // 打印 response 日志 if (!SpringUtils.isProd()) { StopWatch stopWatch = (StopWatch) request.getAttribute(ATTRIBUTE_STOP_WATCH); + if (stopWatch == null) { + return; + } stopWatch.stop(); - log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]", - request.getRequestURI(), stopWatch.getTotalTimeMillis()); + String uri = request.getRequestURI(); + boolean excluded = isExcluded(uri); + boolean hasError = ex != null || response.getStatus() >= 400; + // 命中排除 + 正常响应 → 不打日志,保持安静 + if (excluded && !hasError) { + return; + } + if (hasError) { + // 异常/非 2xx 请求显式打到 WARN,方便快速发现 keepalive/健康检查类的异常 + log.warn("[afterCompletion][完成请求 URL({}) 耗时({} ms) 状态({}) 异常({})]", + uri, stopWatch.getTotalTimeMillis(), response.getStatus(), + ex == null ? "-" : ex.getMessage()); + } else { + log.info("[afterCompletion][完成请求 URL({}) 耗时({} ms)]", + uri, stopWatch.getTotalTimeMillis()); + } } } diff --git a/viewsh-gateway/src/main/resources/application.yaml b/viewsh-gateway/src/main/resources/application.yaml index 77ca8df5..bcfd4475 100644 --- a/viewsh-gateway/src/main/resources/application.yaml +++ b/viewsh-gateway/src/main/resources/application.yaml @@ -225,6 +225,9 @@ spring: - Path=/app-api/video/** filters: - RewritePath=/app-api/video/v3/api-docs, /v3/api-docs + # 注意:ZLM Hook 回调 (/index/hook/**) 不走网关,由 ZLM 直连 video-server 的 Tomcat 端口 + # 原因:Hook 是东西向 M2M 流量(ZLM ↔ video-server),对延迟敏感,经网关会引入 + # 多余的单点 + 日志噪音;业界实践都是让流媒体 Hook 直连业务服务 x-forwarded: prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀