feat(log): 新增日志模块 - HttpTracePro(Spring Boot Actuator HttpTrace 定制增强版)

This commit is contained in:
2023-12-17 14:07:07 +08:00
parent ad1d001973
commit 3e9a59df5a
24 changed files with 1459 additions and 13 deletions

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-log</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-log-httptrace-pro</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 日志模块 - HttpTraceProSpring Boot Actuator HttpTrace 定制增强版)</description>
<dependencies>
<!-- Swagger 注解 -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
</dependency>
<!-- TTL线程间传递 ThreadLocal异步执行时上下文传递的解决方案 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
<!-- 日志模块 - 公共模块 -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-log-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.autoconfigure;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import java.lang.annotation.*;
/**
* 是否启用日志记录注解
*
* @author Charles7c
* @since 1.1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@ConditionalOnProperty(prefix = "continew-starter.log", name = "enabled", havingValue = "true")
public @interface ConditionalOnEnabledLog {}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.autoconfigure;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.charles7c.continew.starter.log.common.dao.LogDao;
import top.charles7c.continew.starter.log.common.dao.impl.LogDaoDefaultImpl;
import top.charles7c.continew.starter.log.httptracepro.handler.LogFilter;
import top.charles7c.continew.starter.log.httptracepro.handler.LogInterceptor;
/**
* 日志自动配置
*
* @author Charles7c
* @since 1.1.0
*/
@Slf4j
@Configuration
@ConditionalOnEnabledLog
@RequiredArgsConstructor
@EnableConfigurationProperties(LogProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class LogAutoConfiguration implements WebMvcConfigurer {
private final LogProperties properties;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor(logDao(), properties));
}
/**
* 日志过滤器
*/
@Bean
@ConditionalOnMissingBean
public LogFilter logFilter() {
return new LogFilter();
}
/**
* 日志持久层接口
*/
@Bean
@ConditionalOnMissingBean
public LogDao logDao() {
return new LogDaoDefaultImpl();
}
@PostConstruct
public void postConstruct() {
log.info("[ContiNew Starter] - Auto Configuration 'Log-HttpTracePro' completed initialization.");
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.autoconfigure;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import top.charles7c.continew.starter.log.common.enums.Include;
import java.util.HashSet;
import java.util.Set;
/**
* 日志配置属性
*
* @author Charles7c
* @since 1.1.0
*/
@Data
@ConfigurationProperties(prefix = "continew-starter.log")
public class LogProperties {
/**
* 是否启用日志
*/
private boolean enabled = false;
/**
* 包含信息
*/
private Set<Include> include = new HashSet<>(Include.defaultIncludes());
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.handler;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.core.Ordered;
import org.springframework.lang.NonNull;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Objects;
/**
* 日志过滤器
*
* @author Dave SyerSpring Boot Actuator
* @author Wallace WadgeSpring Boot Actuator
* @author Andy WilkinsonSpring Boot Actuator
* @author Venil NoronhaSpring Boot Actuator
* @author Madhura BhaveSpring Boot Actuator
* @author Charles7c
* @since 1.1.0
*/
@RequiredArgsConstructor
public class LogFilter extends OncePerRequestFilter implements Ordered {
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 10;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
if (!isRequestValid(request)) {
filterChain.doFilter(request, response);
return;
}
// 包装输入、输出流,可重复读取
if (!(request instanceof ContentCachingRequestWrapper)) {
request = new ContentCachingRequestWrapper(request);
}
if (!(response instanceof ContentCachingResponseWrapper)) {
response = new ContentCachingResponseWrapper(response);
}
filterChain.doFilter(request, response);
// 更新响应(不操作这一步,会导致接口响应空白)
updateResponse(response);
}
private boolean isRequestValid(HttpServletRequest request) {
try {
new URI(request.getRequestURL().toString());
return true;
} catch (URISyntaxException e) {
return false;
}
}
private void updateResponse(HttpServletResponse response) throws IOException {
ContentCachingResponseWrapper responseWrapper =
WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
Objects.requireNonNull(responseWrapper).copyBodyToResponse();
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.handler;
import cn.hutool.core.util.StrUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import top.charles7c.continew.starter.log.common.annotation.Log;
import top.charles7c.continew.starter.log.common.dao.LogDao;
import top.charles7c.continew.starter.log.common.enums.Include;
import top.charles7c.continew.starter.log.common.model.LogRecord;
import top.charles7c.continew.starter.log.httptracepro.autoconfigure.LogProperties;
import java.time.Clock;
import java.util.Set;
/**
* 日志拦截器
*
* @author Charles7c
* @since 1.1.0
*/
@Slf4j
@RequiredArgsConstructor
public class LogInterceptor implements HandlerInterceptor {
private final LogDao dao;
private final LogProperties properties;
private final TransmittableThreadLocal<Clock> timestampTtl = new TransmittableThreadLocal<>();
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler) {
if (this.isRequestRecord(handler)) {
timestampTtl.set(Clock.systemUTC());
}
return true;
}
@Override
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
@NonNull Object handler, Exception e) {
Clock timestamp = timestampTtl.get();
if (null == timestamp) {
return;
}
timestampTtl.remove();
Set<Include> includeSet = properties.getInclude();
RecordableServletHttpRequest sourceRequest = new RecordableServletHttpRequest(request);
LogRecord.Started startedLogRecord = LogRecord.start(timestamp, sourceRequest);
RecordableServletHttpResponse sourceResponse = new RecordableServletHttpResponse(response, response.getStatus());
LogRecord finishedLogRecord = startedLogRecord.finish(sourceResponse, includeSet);
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (includeSet.contains(Include.DESCRIPTION)) {
// 记录日志描述
this.logDescription(finishedLogRecord, handlerMethod);
}
if (includeSet.contains(Include.MODULE)) {
// 记录所属模块
this.logModule(finishedLogRecord, handlerMethod);
}
dao.add(finishedLogRecord);
}
/**
* 记录描述
*
* @param logRecord 日志信息
* @param handlerMethod 处理器方法
*/
private void logDescription(LogRecord logRecord, HandlerMethod handlerMethod) {
// 例如:@Operation(summary="新增部门") -> 新增部门
Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
if (null != methodOperation) {
logRecord.setDescription(StrUtil.blankToDefault(methodOperation.summary(), "请在该接口方法上指定日志描述"));
}
// 例如:@Log("新增部门") -> 新增部门
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
if (null != methodLog && StrUtil.isNotBlank(methodLog.value())) {
logRecord.setDescription(methodLog.value());
}
}
/**
* 记录模块
*
* @param logRecord 日志信息
* @param handlerMethod 处理器方法
*/
private void logModule(LogRecord logRecord, HandlerMethod handlerMethod) {
// 例如:@Tag(name = "部门管理") -> 部门管理
Tag classTag = handlerMethod.getBeanType().getDeclaredAnnotation(Tag.class);
if (null != classTag) {
String name = classTag.name();
logRecord.setModule(StrUtil.blankToDefault(name, "请在该接口类上指定所属模块"));
}
// 例如:@Log(module = "部门管理") -> 部门管理
Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
if (null != classLog && StrUtil.isNotBlank(classLog.module())) {
logRecord.setModule(classLog.module());
}
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
if (null != methodLog && StrUtil.isNotBlank(methodLog.module())) {
logRecord.setModule(methodLog.module());
}
}
/**
* 是否要记录日志
*
* @param handler 处理器
* @return true需要记录false不需要记录
*/
private boolean isRequestRecord(Object handler) {
if (!(handler instanceof HandlerMethod handlerMethod)) {
return false;
}
// 如果接口被隐藏,不记录日志
Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
if (null != methodOperation && methodOperation.hidden()) {
return false;
}
// 如果接口方法或类上有 @Log 注解,且要求忽略该接口,则不记录日志
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
if (null != methodLog && methodLog.ignore()) {
return false;
}
Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
return null == classLog || !classLog.ignore();
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.handler;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.WebUtils;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.log.common.model.RecordableHttpRequest;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* 可记录的 HTTP 请求信息适配器
*
* @author Andy WilkinsonSpring Boot Actuator
* @author Charles7c
*/
public final class RecordableServletHttpRequest implements RecordableHttpRequest {
private final HttpServletRequest request;
public RecordableServletHttpRequest(HttpServletRequest request) {
this.request = request;
}
@Override
public String getMethod() {
return request.getMethod();
}
@Override
public URI getUri() {
String queryString = request.getQueryString();
if (StrUtil.isBlank(queryString)) {
return URI.create(request.getRequestURL().toString());
}
try {
StringBuffer urlBuffer = this.appendQueryString(queryString);
return new URI(urlBuffer.toString());
} catch (URISyntaxException e) {
String encoded = UriUtils.encodeQuery(queryString, StandardCharsets.UTF_8);
StringBuffer urlBuffer = this.appendQueryString(encoded);
return URI.create(urlBuffer.toString());
}
}
@Override
public String getIp() {
return JakartaServletUtil.getClientIP(request);
}
@Override
public Map<String, List<String>> getHeaders() {
return JakartaServletUtil.getHeadersMap(request);
}
@Override
public String getBody() {
ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
if (null != wrapper) {
return StrUtil.utf8Str(wrapper.getContentAsByteArray());
}
return StringConstants.EMPTY;
}
@Override
public Map<String, Object> getParam() {
String body = this.getBody();
return StrUtil.isNotBlank(body) && JSONUtil.isTypeJSON(body)
? JSONUtil.toBean(body, Map.class)
: Collections.unmodifiableMap(request.getParameterMap());
}
private StringBuffer appendQueryString(String queryString) {
return request.getRequestURL().append(StringConstants.QUESTION_MARK).append(queryString);
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.log.httptracepro.handler;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.log.common.model.RecordableHttpResponse;
import java.util.*;
/**
* 可记录的 HTTP 响应信息适配器
*
* @author Andy WilkinsonSpring Boot Actuator
* @author Charles7c
*/
public final class RecordableServletHttpResponse implements RecordableHttpResponse {
private final HttpServletResponse response;
private final int status;
public RecordableServletHttpResponse(HttpServletResponse response, int status) {
this.response = response;
this.status = status;
}
@Override
public int getStatus() {
return this.status;
}
@Override
public Map<String, List<String>> getHeaders() {
Map<String, List<String>> headers = new LinkedHashMap<>();
for (String name : response.getHeaderNames()) {
headers.put(name, new ArrayList<>(response.getHeaders(name)));
}
return headers;
}
@Override
public String getBody() {
ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
if (null != wrapper) {
return StrUtil.utf8Str(wrapper.getContentAsByteArray());
}
return StringConstants.EMPTY;
}
@Override
public Map<String, Object> getParam() {
String body = this.getBody();
return StrUtil.isNotBlank(body) && JSONUtil.isTypeJSON(body)
? JSONUtil.toBean(body, Map.class)
: null;
}
}

View File

@@ -0,0 +1 @@
top.charles7c.continew.starter.log.httptracepro.autoconfigure.LogAutoConfiguration