feat(web): API 访问日志支持 exclude-paths 过滤高频心跳刷屏

- 新增 viewsh.access-log.exclude-paths 配置(Ant Pattern),命中路径默认不打 INFO
- 异常或 HTTP 4xx/5xx 时仍打 WARN,保证"默认安静、出错必响"
- ApiAccessLogInterceptor 保留无参构造,兼容老用法
- gateway application.yaml 补注释:ZLM Hook 走 video-server 直连而非网关,理由说明

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-04-24 13:31:44 +08:00
parent ca575d6297
commit 04961ee614
3 changed files with 84 additions and 7 deletions

View File

@@ -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<String> 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));
}
}

View File

@@ -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 两条日志到日志文件(控制台)中。
*
* <p>支持通过 {@code viewsh.access-log.exclude-paths} 配置排除高频心跳/健康检查类请求的日志刷屏,
* 排除路径支持 Ant Pattern例如 {@code /index/hook/on_server_keepalive}、{@code /actuator/**})。
* 被排除的请求只有在 <b>发生异常</b> 或 <b>HTTP 状态码 ≥ 400</b> 时才会打印日志,
* 保证"默认安静、出错必响"。
*
* @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<String> excludePaths;
public ApiAccessLogInterceptor() {
this(Collections.emptyList());
}
public ApiAccessLogInterceptor(List<String> 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<String, String> 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());
}
}
}