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,17 @@
<?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-common</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>ContiNew Starter 日志模块 - 公共模块</description>
</project>

View File

@@ -0,0 +1,53 @@
/*
* 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.common.annotation;
import java.lang.annotation.*;
/**
* 日志注解
* <p>用于接口方法或类上,辅助 Spring Doc 使用效果最佳</p>
*
* @author Charles7c
* @since 1.1.0
*/
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
/**
* 日志描述(仅用于接口方法上)
* <p>
* 优先级:@Log("描述") > @Operation(summary="描述")
* </p>
*/
String value() default "";
/**
* 所属模块(用于接口方法或类上)
* <p>
* 优先级: 接口方法上的 @Log(module = "模块") > 接口类上的 @Log(module = "模块") > @Tag(name = "模块") 内容
* </p>
*/
String module() default "";
/**
* 是否忽略日志记录(用于接口方法或类上)
*/
boolean ignore() default false;
}

View File

@@ -0,0 +1,47 @@
/*
* 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.common.dao;
import top.charles7c.continew.starter.log.common.model.LogRecord;
import java.util.Collections;
import java.util.List;
/**
* 日志持久层接口
*
* @author Charles7c
* @since 1.1.0
*/
public interface LogDao {
/**
* 查询日志列表
*
* @return 日志列表
*/
default List<LogRecord> list() {
return Collections.emptyList();
}
/**
* 记录日志
*
* @param logRecord 日志信息
*/
void add(LogRecord logRecord);
}

View File

@@ -0,0 +1,92 @@
/*
* 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.common.dao.impl;
import top.charles7c.continew.starter.log.common.dao.LogDao;
import top.charles7c.continew.starter.log.common.model.LogRecord;
import java.util.LinkedList;
import java.util.List;
/**
* 日志持久层接口默认实现类(基于内存)
*
* @author Dave SyerSpring Boot Actuator
* @author Olivier BourgainSpring Boot Actuator
* @author Charles7c
* @since 1.1.0
*/
public class LogDaoDefaultImpl implements LogDao {
/**
* 容量
*/
private int capacity = 100;
/**
* 是否降序
*/
private boolean reverse = true;
/**
* 日志列表
*/
private final List<LogRecord> logRecords = new LinkedList<>();
@Override
public List<LogRecord> list() {
synchronized (this.logRecords) {
return List.copyOf(this.logRecords);
}
}
@Override
public void add(LogRecord logRecord) {
synchronized (this.logRecords) {
while (this.logRecords.size() >= this.capacity) {
this.logRecords.remove(this.reverse ? this.capacity - 1 : 0);
}
if (this.reverse) {
this.logRecords.add(0, logRecord);
} else {
this.logRecords.add(logRecord);
}
}
}
/**
* 设置内存中存储的最大日志容量
*
* @param capacity 容量
*/
public void setCapacity(int capacity) {
synchronized (this.logRecords) {
this.capacity = capacity;
}
}
/**
* 设置是否降序
*
* @param reverse 是否降序默认true
*/
public void setReverse(boolean reverse) {
synchronized (this.logRecords) {
this.reverse = reverse;
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.common.enums;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 日志包含信息
*
* @author Wallace WadgeSpring Boot Actuator
* @author Emily TsanovaSpring Boot Actuator
* @author Joseph BeetonSpring Boot Actuator
* @author Charles7c
* @since 1.1.0
*/
public enum Include {
/**
* 描述
*/
DESCRIPTION,
/**
* 模块
*/
MODULE,
/**
* 请求头
*/
REQUEST_HEADERS,
/**
* 请求体
*/
REQUEST_BODY,
/**
* 请求参数
*/
REQUEST_PARAM,
/**
* IP 归属地
*/
IP_ADDRESS,
/**
* 浏览器
*/
BROWSER,
/**
* 操作系统
*/
OS,
/**
* 响应头
*/
RESPONSE_HEADERS,
/**
* 响应体
*/
RESPONSE_BODY,
/**
* 响应参数
*/
RESPONSE_PARAM,
/**
* 耗时
*/
TIME_TAKEN;
private static final Set<Include> DEFAULT_INCLUDES;
static {
Set<Include> defaultIncludes = new LinkedHashSet<>();
defaultIncludes.add(Include.TIME_TAKEN);
defaultIncludes.add(Include.REQUEST_HEADERS);
defaultIncludes.add(Include.RESPONSE_HEADERS);
DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes);
}
/**
* 获取默认包含信息
*
* @return 默认包含信息
*/
public static Set<Include> defaultIncludes() {
return DEFAULT_INCLUDES;
}
}

View File

@@ -0,0 +1,138 @@
/*
* 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.common.model;
import lombok.Data;
import top.charles7c.continew.starter.log.common.enums.Include;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Set;
/**
* 日志信息
*
* @author Dave SyerSpring Boot Actuator
* @author Andy WilkinsonSpring Boot Actuator
* @author Phillip WebbSpring Boot Actuator
* @author Charles7c
* @since 1.1.0
*/
@Data
public class LogRecord {
/**
* 描述
*/
private String description;
/**
* 模块
*/
private String module;
/**
* 请求信息
*/
private LogRequest request;
/**
* 响应信息
*/
private LogResponse response;
/**
* 耗时
*/
private Duration timeTaken;
/**
* 时间戳
*/
private final Instant timestamp;
public LogRecord(Instant timestamp, LogRequest request, LogResponse response, Duration timeTaken) {
this.timestamp = timestamp;
this.request = request;
this.response = response;
this.timeTaken = timeTaken;
}
/**
* 开始记录日志
*
* @param request 请求信息
* @return 日志记录器
*/
public static Started start(RecordableHttpRequest request) {
return start(Clock.systemUTC(), request);
}
/**
* 开始记录日志
*
* @param timestamp 开始时间
* @param request 请求信息
* @return 日志记录器
*/
public static Started start(Clock timestamp, RecordableHttpRequest request) {
return new Started(timestamp, request);
}
/**
* 日志记录器
*/
public static final class Started {
private final Instant timestamp;
private final RecordableHttpRequest request;
private Started(Clock clock, RecordableHttpRequest request) {
this.timestamp = Instant.now(clock);
this.request = request;
}
/**
* 结束日志记录
*
* @param response 响应信息
* @param includes 包含信息
* @return 日志记录
*/
public LogRecord finish(RecordableHttpResponse response, Set<Include> includes) {
return finish(Clock.systemUTC(), response, includes);
}
/**
* 结束日志记录
*
* @param clock 时间
* @param response 响应信息
* @param includes 包含信息
* @return 日志记录
*/
public LogRecord finish(Clock clock, RecordableHttpResponse response, Set<Include> includes) {
LogRequest logRequest = new LogRequest(this.request, includes);
LogResponse logResponse = new LogResponse(response, includes);
Duration duration = (includes.contains(Include.TIME_TAKEN)) ? Duration.between(this.timestamp, Instant.now(clock)) : null;
return new LogRecord(this.timestamp, logRequest, logResponse, duration);
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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.common.model;
import lombok.Data;
import org.springframework.http.HttpHeaders;
import top.charles7c.continew.starter.core.util.ExceptionUtils;
import top.charles7c.continew.starter.core.util.IpUtils;
import top.charles7c.continew.starter.core.util.ServletUtils;
import top.charles7c.continew.starter.log.common.enums.Include;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 请求信息
*
* @author Charles7c
* @since 1.1.0
*/
@Data
public class LogRequest {
/**
* 请求方式
*/
private String method;
/**
* 请求 URI
*/
private URI uri;
/**
* IP
*/
private String ip;
/**
* 请求头
*/
private Map<String, List<String>> headers;
/**
* 请求体JSON 字符串)
*/
private String body;
/**
* 请求参数
*/
private Map<String, Object> param;
/**
* IP 归属地
*/
private String address;
/**
* 浏览器
*/
private String browser;
/**
* 操作系统
*/
private String os;
public LogRequest(RecordableHttpRequest request, Set<Include> includes) {
this.method = request.getMethod();
this.uri = request.getUri();
this.ip = request.getIp();
this.headers = (includes.contains(Include.REQUEST_HEADERS)) ? request.getHeaders() : null;
this.body = (includes.contains(Include.REQUEST_BODY)) ? request.getBody() : null;
this.param = (includes.contains(Include.RESPONSE_PARAM)) ? request.getParam() : null;
this.address = (includes.contains(Include.IP_ADDRESS)) ? IpUtils.getAddress(this.ip) : null;
String userAgentString = ExceptionUtils.exToNull(() -> this.headers.get(HttpHeaders.USER_AGENT).get(0));
this.browser = (includes.contains(Include.BROWSER)) ? ServletUtils.getBrowser(userAgentString) : null;
this.os = (includes.contains(Include.OS)) ? ServletUtils.getOs(userAgentString) : null;
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.common.model;
import lombok.Data;
import top.charles7c.continew.starter.log.common.enums.Include;
import java.util.*;
/**
* 响应信息
*
* @author Charles7c
* @since 1.1.0
*/
@Data
public class LogResponse {
/**
* 状态码
*/
private Integer status;
/**
* 响应头
*/
private Map<String, List<String>> headers;
/**
* 响应体JSON 字符串)
*/
private String body;
/**
* 响应参数
*/
private Map<String, Object> param;
public LogResponse(RecordableHttpResponse response, Set<Include> includes) {
this.status = response.getStatus();
this.headers = (includes.contains(Include.REQUEST_HEADERS)) ? response.getHeaders() : null;
this.body = (includes.contains(Include.REQUEST_BODY)) ? response.getBody() : null;
this.param = (includes.contains(Include.RESPONSE_PARAM)) ? response.getParam() : null;
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.common.model;
import java.net.URI;
import java.util.List;
import java.util.Map;
/**
* 可记录的 HTTP 请求信息
*
* @author Andy WilkinsonSpring Boot Actuator
* @author Phillip WebbSpring Boot Actuator
* @author Charles7c
* @see RecordableHttpResponse
* @since 1.1.0
*/
public interface RecordableHttpRequest {
/**
* 获取请求方式
*
* @return 请求方式
*/
String getMethod();
/**
* 获取 URI
*
* @return URI
*/
URI getUri();
/**
* 获取 IP
*
* @return IP
*/
String getIp();
/**
* 获取请求头
*
* @return 请求头
*/
Map<String, List<String>> getHeaders();
/**
* 获取请求体
*
* @return 请求体
*/
String getBody();
/**
* 获取请求参数
*
* @return 请求参数
*/
Map<String, Object> getParam();
}

View File

@@ -0,0 +1,59 @@
/*
* 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.common.model;
import java.util.List;
import java.util.Map;
/**
* 可记录的 HTTP 响应信息
*
* @author Andy WilkinsonSpring Boot Actuator
* @author Charles7c
* @see RecordableHttpRequest
* @since 1.1.0
*/
public interface RecordableHttpResponse {
/**
* 获取状态码
*
* @return 状态码
*/
int getStatus();
/**
* 获取响应头
*
* @return 响应头
*/
Map<String, List<String>> getHeaders();
/**
* 获取响应体
*
* @return 响应体
*/
String getBody();
/**
* 获取响应参数
*
* @return 响应参数
*/
Map<String, Object> getParam();
}