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:
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user