From 727850933f5f5cede55ebcc3177c70c6ae76c077 Mon Sep 17 00:00:00 2001 From: Charles7c Date: Sun, 25 Dec 2022 13:16:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=9B=91=E6=8E=A7=E6=A8=A1=E5=9D=97=EF=BC=88?= =?UTF-8?q?=E5=AD=98=E6=94=BE=E7=B3=BB=E7=BB=9F=E7=9B=91=E6=8E=A7=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BE=8B?= =?UTF-8?q?=E5=A6=82=EF=BC=9A=E6=97=A5=E5=BF=97=E7=AE=A1=E7=90=86=E3=80=81?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=9B=91=E6=8E=A7=E7=AD=89=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=93=8D=E4=BD=9C=E6=97=A5=E5=BF=97=E5=BC=95?= =?UTF-8?q?=E6=93=8E=EF=BC=8C=E8=AE=B0=E5=BD=95=20HTTP=20=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 43 ++- continew-admin-common/pom.xml | 6 + .../common/config/AsyncConfiguration.java | 73 +++++ .../properties/ContinewAdminProperties.java | 12 + .../threadpool/ThreadPoolConfiguration.java | 85 +++++ .../threadpool/ThreadPoolProperties.java | 44 +++ .../handler/GlobalExceptionHandler.java | 29 ++ .../common/model/dto/OperationLog.java | 47 +++ .../cnadmin/common/util/ExceptionUtils.java | 33 ++ .../cnadmin/common/util/IpUtils.java | 111 +++++++ .../cnadmin/common/util/ServletUtils.java | 95 ++++++ .../common/util/holder/LogContextHolder.java | 60 ++++ continew-admin-monitor/pom.xml | 41 +++ .../cnadmin/monitor/annotation/Log.java | 41 +++ .../config/WebMvcMonitorConfiguration.java | 45 +++ .../config/properties/LogProperties.java | 52 ++++ .../cnadmin/monitor/enums/LogLevelEnum.java | 40 +++ .../cnadmin/monitor/filter/LogFilter.java | 80 +++++ .../monitor/interceptor/LogInterceptor.java | 293 ++++++++++++++++++ .../cnadmin/monitor/mapper/LogMapper.java | 29 ++ .../cnadmin/monitor/model/entity/SysLog.java | 126 ++++++++ .../cnadmin/monitor/service/LogService.java | 27 ++ .../monitor/service/impl/LogServiceImpl.java | 48 +++ .../src/main/resources/mapper/LogMapper.xml | 4 + continew-admin-webapi/pom.xml | 6 + .../src/main/resources/application.yml | 24 +- .../changelog/v0.0.1/continew-admin_table.sql | 25 +- pom.xml | 16 + 28 files changed, 1523 insertions(+), 12 deletions(-) create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/AsyncConfiguration.java create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/threadpool/ThreadPoolConfiguration.java create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/threadpool/ThreadPoolProperties.java create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/dto/OperationLog.java create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/IpUtils.java create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/ServletUtils.java create mode 100644 continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/holder/LogContextHolder.java create mode 100644 continew-admin-monitor/pom.xml create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/annotation/Log.java create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/config/WebMvcMonitorConfiguration.java create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/config/properties/LogProperties.java create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/enums/LogLevelEnum.java create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/filter/LogFilter.java create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/interceptor/LogInterceptor.java create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/mapper/LogMapper.java create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/entity/SysLog.java create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/LogService.java create mode 100644 continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/impl/LogServiceImpl.java create mode 100644 continew-admin-monitor/src/main/resources/mapper/LogMapper.xml diff --git a/README.md b/README.md index 5993ea01..ce769733 100644 --- a/README.md +++ b/README.md @@ -77,22 +77,43 @@ continew-admin # 全局通用项目配置及依赖版本管理 │ │ └─ ContinewAdminApplication.java # 启动入口 │ └─ resources # 工程配置目录 │ └─ db.changelog.v0.0.1 # 数据库脚本文件 - ├─ continew-admin-system # 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等) + ├─ continew-admin-monitor # 系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等) │ └─ src │ └─ main │ ├─ java # 工程源文件代码目录 │ │ └─ top │ │ └─ charles7c │ │ └─ cnadmin - │ │ ├─ auth # 认证相关业务及配置 - │ │ │ ├─ config # 认证相关配置 + │ │ └─ monitor + │ │ ├─ annotation # 系统监控相关注解 + │ │ ├─ config # 系统监控相关配置 + │ │ │ └─ properties # 系统监控相关配置属性 + │ │ ├─ enums # 系统监控相关枚举 + │ │ ├─ filter # 系统监控相关过滤器 + │ │ ├─ interceptor # 系统监控相关拦截器 + │ │ ├─ mapper # 系统监控相关 Mapper + │ │ ├─ model # 系统监控相关模型 + │ │ │ └─ entity # 系统监控相关实体对象 + │ │ └─ service # 系统监控相关业务接口及实现类 + │ │ └─ impl # 系统监控相关业务实现类 + │ └─ resources # 工程配置目录 + │ └─ mapper # MyBatis Mapper XML 文件目录 + ├─ continew-admin-system # 系统管理模块(存放系统管理模块相关功能,例如:部门管理、角色管理、用户管理等) + │ └─ src + │ └─ main + │ ├─ java # 工程源文件代码目录 + │ │ └─ top + │ │ └─ charles7c + │ │ └─ cnadmin + │ │ ├─ auth # 系统认证相关业务及配置 + │ │ │ ├─ config # 系统认证相关配置 │ │ │ │ ├─ satoken # Sa-Token 配置 - │ │ │ │ └─ properties # 认证相关配置属性 - │ │ │ ├─ model # 认证相关模型 - │ │ │ │ ├─ request # 认证相关请求对象 - │ │ │ │ └─ vo # 认证相关 VO(View Object) - │ │ │ └─ service # 认证相关业务接口及实现类 - │ │ │ └─ impl # 认证相关业务实现类 + │ │ │ │ └─ properties # 系统认证相关配置属性 + │ │ │ ├─ model # 系统认证相关模型 + │ │ │ │ ├─ request # 系统认证相关请求对象 + │ │ │ │ └─ vo # 系统认证相关 VO(View Object) + │ │ │ └─ service # 系统认证相关业务接口及实现类 + │ │ │ └─ impl # 系统认证相关业务实现类 │ │ └─ system # 系统管理相关业务及配置 │ │ ├─ mapper # 系统管理相关 Mapper │ │ ├─ model # 系统管理相关模型 @@ -112,6 +133,7 @@ continew-admin # 全局通用项目配置及依赖版本管理 │ ├─ config # 公共配置 │ │ ├─ jackson # Jackson 配置 │ │ ├─ mybatis # MyBatis Plus 配置 + │ │ ├─ threadpool # 线程池配置 │ │ └─ properties # 公共配置属性 │ ├─ consts # 公共常量 │ ├─ exception # 公共异常 @@ -121,7 +143,8 @@ continew-admin # 全局通用项目配置及依赖版本管理 │ │ ├─ entity # 公共实体对象 │ │ └─ vo # 公共 VO(View Object) │ └─ util # 公共工具类 - │ └─ helper # 公共 Helper(助手) + │ ├─ helper # 公共 Helper(助手) + │ └─ holder # 公共 Holder(持有者) ``` ### License diff --git a/continew-admin-common/pom.xml b/continew-admin-common/pom.xml index 483a0c17..ceb54cef 100644 --- a/continew-admin-common/pom.xml +++ b/continew-admin-common/pom.xml @@ -110,6 +110,12 @@ limitations under the License. + + + net.dreamlu + mica-ip2region + + com.github.xiaoymin diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/AsyncConfiguration.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/AsyncConfiguration.java new file mode 100644 index 00000000..ca70111e --- /dev/null +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/AsyncConfiguration.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.common.config; + +import java.util.Arrays; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; + +import cn.hutool.core.util.ArrayUtil; + +import top.charles7c.cnadmin.common.exception.ServiceException; + +/** + * 异步任务执行配置 + * + * @author Charles7c + * @since 2022/12/23 22:33 + */ +@Slf4j +@Configuration +@RequiredArgsConstructor +@EnableAsync(proxyTargetClass = true) +public class AsyncConfiguration implements AsyncConfigurer { + + private final ScheduledExecutorService scheduledExecutorService; + + /** + * 异步任务执行时,使用 Java 内置线程池 + */ + @Override + public Executor getAsyncExecutor() { + return scheduledExecutorService; + } + + /** + * 异步任务执行时的异常处理 + */ + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return (throwable, method, objects) -> { + throwable.printStackTrace(); + StringBuilder sb = new StringBuilder(); + sb.append("Exception message - ").append(throwable.getMessage()).append(", Method name - ") + .append(method.getName()); + if (ArrayUtil.isNotEmpty(objects)) { + sb.append(", Parameter value - ").append(Arrays.toString(objects)); + } + throw new ServiceException(sb.toString()); + }; + } +} diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/properties/ContinewAdminProperties.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/properties/ContinewAdminProperties.java index 142a1472..b61ded9a 100644 --- a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/properties/ContinewAdminProperties.java +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/properties/ContinewAdminProperties.java @@ -25,6 +25,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.stereotype.Component; +import cn.hutool.core.convert.Convert; +import cn.hutool.extra.spring.SpringUtil; + /** * 项目配置属性 * @@ -72,4 +75,13 @@ public class ContinewAdminProperties { */ @NestedConfigurationProperty private License license; + + /** + * 是否本地解析 IP 归属地 + */ + public static final boolean IP_ADDR_LOCAL_PARSE_ENABLED; + + static { + IP_ADDR_LOCAL_PARSE_ENABLED = Convert.toBool(SpringUtil.getProperty("continew-admin.ipAddrLocalParseEnabled")); + } } diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/threadpool/ThreadPoolConfiguration.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/threadpool/ThreadPoolConfiguration.java new file mode 100644 index 00000000..d55e11a4 --- /dev/null +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/threadpool/ThreadPoolConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.common.config.threadpool; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import top.charles7c.cnadmin.common.util.ExceptionUtils; + +/** + * 线程池配置 + * + * @author Charles7c + * @since 2022/12/23 23:13 + */ +@Slf4j +@Configuration +@RequiredArgsConstructor +public class ThreadPoolConfiguration { + + private final ThreadPoolProperties threadPoolProperties; + /** 核心(最小)线程数 = CPU 核心数 + 1 */ + private final int corePoolSize = Runtime.getRuntime().availableProcessors() + 1; + + /** + * Spring 内置线程池:ThreadPoolTaskExecutor + */ + @Bean + @ConditionalOnProperty(prefix = "thread-pool", name = "enabled", havingValue = "true") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + // 核心(最小)线程数 + executor.setCorePoolSize(corePoolSize); + // 最大线程数 + executor.setMaxPoolSize(corePoolSize * 2); + // 队列容量 + executor.setQueueCapacity(threadPoolProperties.getQueueCapacity()); + // 活跃时间 + executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds()); + // 配置当池内线程数已达到上限的时候,该如何处理新任务:不在新线程中执行任务,而是由调用者所在的线程来执行 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + return executor; + } + + /** + * Java 内置线程池:ScheduledExecutorService(适用于执行周期性或定时任务) + */ + @Bean + public ScheduledExecutorService scheduledExecutorService() { + return new ScheduledThreadPoolExecutor(corePoolSize, + new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(), + new ThreadPoolExecutor.CallerRunsPolicy()) { + + @Override + protected void afterExecute(Runnable runnable, Throwable throwable) { + super.afterExecute(runnable, throwable); + ExceptionUtils.printException(runnable, throwable); + } + }; + } +} diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/threadpool/ThreadPoolProperties.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/threadpool/ThreadPoolProperties.java new file mode 100644 index 00000000..8e64a615 --- /dev/null +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/config/threadpool/ThreadPoolProperties.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.common.config.threadpool; + +import lombok.Data; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 线程池配置属性 + * + * @author Charles7c + * @since 2022/12/23 23:06 + */ +@Data +@Component +@ConfigurationProperties(prefix = "thread-pool") +public class ThreadPoolProperties { + + /** + * 队列容量 + */ + private int queueCapacity; + + /** + * 活跃时间 + */ + private int keepAliveSeconds; +} diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/handler/GlobalExceptionHandler.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/handler/GlobalExceptionHandler.java index 2cc96dd4..db218a87 100644 --- a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/handler/GlobalExceptionHandler.java +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/handler/GlobalExceptionHandler.java @@ -38,9 +38,12 @@ import cn.dev33.satoken.exception.NotLoginException; import cn.hutool.core.util.StrUtil; import top.charles7c.cnadmin.common.exception.BadRequestException; +import top.charles7c.cnadmin.common.exception.ServiceException; +import top.charles7c.cnadmin.common.model.dto.OperationLog; import top.charles7c.cnadmin.common.model.vo.R; import top.charles7c.cnadmin.common.util.ExceptionUtils; import top.charles7c.cnadmin.common.util.StreamUtils; +import top.charles7c.cnadmin.common.util.holder.LogContextHolder; /** * 全局异常处理器 @@ -58,6 +61,7 @@ public class GlobalExceptionHandler { @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Exception.class) public R handleException(Exception e, HttpServletRequest request) { + this.setException(e); log.error("请求地址'{}',发生未知异常", request.getRequestURI(), e); return R.fail(e.getMessage()); } @@ -68,10 +72,22 @@ public class GlobalExceptionHandler { @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(RuntimeException.class) public R handleRuntimeException(RuntimeException e, HttpServletRequest request) { + this.setException(e); log.error("请求地址'{}',发生系统异常", request.getRequestURI(), e); return R.fail(e.getMessage()); } + /** + * 拦截业务异常 + */ + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(ServiceException.class) + public R handleServiceException(ServiceException e, HttpServletRequest request) { + this.setException(e); + log.error("请求地址'{}',发生业务异常", request.getRequestURI(), e); + return R.fail(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage()); + } + /** * 拦截自定义验证异常-错误请求 */ @@ -147,4 +163,17 @@ public class GlobalExceptionHandler { log.error("请求地址'{}',认证失败'{}',无法访问系统资源", request.getRequestURI(), e.getMessage()); return R.fail(HttpStatus.UNAUTHORIZED.value(), "认证失败,无法访问系统资源"); } + + /** + * 操作日志保存异常信息 + * + * @param e + * 异常信息 + */ + private void setException(Exception e) { + OperationLog operationLog = LogContextHolder.get(); + if (operationLog != null) { + operationLog.setException(e); + } + } } \ No newline at end of file diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/dto/OperationLog.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/dto/OperationLog.java new file mode 100644 index 00000000..f0684bcd --- /dev/null +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/model/dto/OperationLog.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.common.model.dto; + +import java.util.Date; + +import lombok.Data; + +/** + * 操作日志 + * + * @author Charles7c + * @since 2022/12/25 8:59 + */ +@Data +public class OperationLog { + + /** + * 操作人 + */ + private Long createUser; + + /** + * 操作时间 + */ + private Date createTime; + + /** + * 异常 + */ + private Exception exception; + +} diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/ExceptionUtils.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/ExceptionUtils.java index 90f2e684..38dbe272 100644 --- a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/ExceptionUtils.java +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/ExceptionUtils.java @@ -16,10 +16,14 @@ package top.charles7c.cnadmin.common.util; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import java.util.function.Consumer; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; /** * 异常工具类 @@ -27,9 +31,38 @@ import lombok.NoArgsConstructor; * @author Charles7c * @since 2022/12/21 20:56 */ +@Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ExceptionUtils { + /** + * 打印线程异常信息 + * + * @param runnable + * 线程执行内容 + * @param throwable + * 异常 + */ + public static void printException(Runnable runnable, Throwable throwable) { + if (throwable == null && runnable instanceof Future) { + try { + Future future = (Future)runnable; + if (future.isDone()) { + future.get(); + } + } catch (CancellationException e) { + throwable = e; + } catch (ExecutionException e) { + throwable = e.getCause(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + if (throwable != null) { + log.error(throwable.getMessage(), throwable); + } + } + /** * 如果有异常,返回 null * diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/IpUtils.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/IpUtils.java new file mode 100644 index 00000000..deb06e0a --- /dev/null +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/IpUtils.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.common.util; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import cn.hutool.core.net.NetUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.http.HtmlUtil; +import cn.hutool.http.HttpUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; + +import top.charles7c.cnadmin.common.config.properties.ContinewAdminProperties; + +import net.dreamlu.mica.ip2region.core.Ip2regionSearcher; +import net.dreamlu.mica.ip2region.core.IpInfo; + +/** + * IP 工具类 + * + * @author Charles7c + * @since 2022/12/23 20:00 + */ +@Slf4j +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class IpUtils { + + /** + * 太平洋网开放 API,查询 IP 归属地 + */ + private static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp?ip=%s&json=true"; + + /** + * 根据IP获取详细地址 + * + * @param ip + * IP地址 + * @return 详细地址 + */ + public static String getCityInfo(String ip) { + if (ContinewAdminProperties.IP_ADDR_LOCAL_PARSE_ENABLED) { + return getLocalCityInfo(ip); + } else { + return getHttpCityInfo(ip); + } + } + + /** + * 根据 IP 获取详细地址(网络解析) + * + * @param ip + * IP地址 + * @return 详细地址 + */ + public static String getHttpCityInfo(String ip) { + if (isInnerIP(ip)) { + return "内网IP"; + } + String api = String.format(IP_URL, ip); + JSONObject object = JSONUtil.parseObj(HttpUtil.get(api)); + return object.get("addr", String.class); + } + + /** + * 根据 IP 获取详细地址(本地解析) + * + * @param ip + * IP 地址 + * @return 详细地址 + */ + public static String getLocalCityInfo(String ip) { + if (isInnerIP(ip)) { + return "内网IP"; + } + Ip2regionSearcher ip2regionSearcher = SpringUtil.getBean(Ip2regionSearcher.class); + IpInfo ipInfo = ip2regionSearcher.memorySearch(ip); + if (ipInfo != null) { + return ipInfo.getAddress(); + } + return null; + } + + /** + * 是否为内网IPv4 + * + * @param ip + * IP 地址 + * @return 是否为内网IP + */ + public static boolean isInnerIP(String ip) { + ip = "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip); + return NetUtil.isInnerIP(ip); + } +} diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/ServletUtils.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/ServletUtils.java new file mode 100644 index 00000000..06be5082 --- /dev/null +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/ServletUtils.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.common.util; + +import java.util.*; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import cn.hutool.http.useragent.UserAgent; +import cn.hutool.http.useragent.UserAgentUtil; + +/** + * Servlet 工具类 + * + * @author Charles7c + * @since 2022/12/23 20:00 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ServletUtils { + + /** + * 获取请求对象 + * + * @return / + */ + public static HttpServletRequest getRequest() { + return getServletRequestAttributes().getRequest(); + } + + /** + * 获取响应对象 + * + * @return / + */ + public static HttpServletResponse getResponse() { + return getServletRequestAttributes().getResponse(); + } + + private static ServletRequestAttributes getServletRequestAttributes() { + return (ServletRequestAttributes)Objects.requireNonNull(RequestContextHolder.getRequestAttributes()); + } + + /** + * 获取浏览器及其版本信息 + * + * @param request + * 请求信息 + * @return 浏览器及其版本信息 + */ + public static String getBrowser(HttpServletRequest request) { + if (request == null) { + return null; + } + UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent")); + return userAgent.getBrowser().getName() + " " + userAgent.getVersion(); + } + + /** + * 获取响应所有的头(header)信息 + * + * @param response + * 响应对象{@link HttpServletResponse} + * @return header值 + */ + public static Map> getHeaderMap(HttpServletResponse response) { + final Map> headerMap = new HashMap<>(); + + final Collection names = response.getHeaderNames(); + for (String name : names) { + headerMap.put(name, response.getHeaders(name)); + } + return headerMap; + } +} diff --git a/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/holder/LogContextHolder.java b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/holder/LogContextHolder.java new file mode 100644 index 00000000..78ae5960 --- /dev/null +++ b/continew-admin-common/src/main/java/top/charles7c/cnadmin/common/util/holder/LogContextHolder.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.common.util.holder; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import top.charles7c.cnadmin.common.model.dto.OperationLog; + +/** + * 操作日志上下文持有者 + * + * @author Charles7c + * @since 2022/12/25 8:55 + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class LogContextHolder { + + private static final ThreadLocal LOG_THREAD_LOCAL = new ThreadLocal<>(); + + /** + * 存储操作日志 + * + * @param operationLog + * 操作日志信息 + */ + public static void set(OperationLog operationLog) { + LOG_THREAD_LOCAL.set(operationLog); + } + + /** + * 获取操作日志 + * + * @return 操作日志信息 + */ + public static OperationLog get() { + return LOG_THREAD_LOCAL.get(); + } + + /** + * 移除操作日志 + */ + public static void remove() { + LOG_THREAD_LOCAL.remove(); + } +} diff --git a/continew-admin-monitor/pom.xml b/continew-admin-monitor/pom.xml new file mode 100644 index 00000000..59cadaf8 --- /dev/null +++ b/continew-admin-monitor/pom.xml @@ -0,0 +1,41 @@ + + + + 4.0.0 + + + continew-admin + top.charles7c + ${revision} + + + continew-admin-monitor + jar + + ${project.artifactId} + 系统监控模块(存放系统监控模块相关功能,例如:日志管理、服务监控等) + + + + + top.charles7c + continew-admin-common + + + \ No newline at end of file diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/annotation/Log.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/annotation/Log.java new file mode 100644 index 00000000..0da9e8de --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/annotation/Log.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.monitor.annotation; + +import java.lang.annotation.*; + +/** + * 操作日志注解(用于接口方法或类上) + * + * @author Charles7c + * @since 2022/12/23 20:00 + */ +@Documented +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Log { + + /** + * 操作日志描述 + */ + String value() default ""; + + /** + * 是否忽略日志记录 + */ + boolean ignore() default false; +} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/config/WebMvcMonitorConfiguration.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/config/WebMvcMonitorConfiguration.java new file mode 100644 index 00000000..37e63680 --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/config/WebMvcMonitorConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.monitor.config; + +import lombok.RequiredArgsConstructor; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import top.charles7c.cnadmin.monitor.interceptor.LogInterceptor; + +/** + * 监控模块 Web MVC 配置 + * + * @author Charles7c + * @since 2022/12/24 23:15 + */ +@EnableWebMvc +@Configuration +@RequiredArgsConstructor +public class WebMvcMonitorConfiguration implements WebMvcConfigurer { + + private final LogInterceptor logInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(logInterceptor); + } +} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/config/properties/LogProperties.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/config/properties/LogProperties.java new file mode 100644 index 00000000..c0221cc7 --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/config/properties/LogProperties.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.monitor.config.properties; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Data; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 操作日志配置属性 + * + * @author Charles7c + * @since 2022/12/24 23:04 + */ +@Data +@Component +@ConfigurationProperties(prefix = "logging.operation") +public class LogProperties { + + /** + * 是否启用操作日志 + */ + private Boolean enabled = false; + + /** + * 脱敏字段 + */ + private List desensitize = new ArrayList<>(); + + /** + * 不记录操作日志的请求方式 + */ + private List excludeMethods = new ArrayList<>(); +} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/enums/LogLevelEnum.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/enums/LogLevelEnum.java new file mode 100644 index 00000000..0efbdc86 --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/enums/LogLevelEnum.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.monitor.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 操作日志级别枚举 + * + * @author Charles7c + * @since 2022/12/25 9:09 + */ +@Getter +@RequiredArgsConstructor +public enum LogLevelEnum { + + /** 普通 */ + INFO("普通"), + + /** 错误 */ + ERROR("错误"),; + + /** 描述 */ + private final String description; +} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/filter/LogFilter.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/filter/LogFilter.java new file mode 100644 index 00000000..a0ee989c --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/filter/LogFilter.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.monitor.filter; + +import java.io.IOException; +import java.util.Objects; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; +import org.springframework.web.util.WebUtils; + +/** + * 操作日志过滤器(缓存请求和响应体过滤器) + * + *

+ * 由于 requestBody 和 responseBody 分别对应的是 InputStream 和 OutputStream,由于流的特性,读取完之后就无法再被使用了。 所以,需要额外缓存一次流信息。 + *

+ * + * @author Charles7c + * @since 2022/12/24 21:16 + */ +@Component +public class LogFilter extends OncePerRequestFilter implements Ordered { + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // 包装流,可重复读取 + if (!(request instanceof ContentCachingRequestWrapper)) { + request = new ContentCachingRequestWrapper(request); + } + if (!(response instanceof ContentCachingResponseWrapper)) { + response = new ContentCachingResponseWrapper(response); + } + + filterChain.doFilter(request, response); + updateResponse(response); + } + + /** + * 更新响应(不操作这一步,会导致接口响应空白) + * + * @param response + * 响应对象 + * @throws IOException + * / + */ + private void updateResponse(HttpServletResponse response) throws IOException { + ContentCachingResponseWrapper responseWrapper = + WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class); + Objects.requireNonNull(responseWrapper).copyBodyToResponse(); + } +} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/interceptor/LogInterceptor.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/interceptor/LogInterceptor.java new file mode 100644 index 00000000..1c24af50 --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/interceptor/LogInterceptor.java @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.monitor.interceptor; + +import java.util.Date; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import io.swagger.v3.oas.annotations.Operation; + +import org.jetbrains.annotations.NotNull; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; +import org.springframework.web.util.WebUtils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONUtil; + +import top.charles7c.cnadmin.common.model.dto.OperationLog; +import top.charles7c.cnadmin.common.util.IpUtils; +import top.charles7c.cnadmin.common.util.ServletUtils; +import top.charles7c.cnadmin.common.util.helper.LoginHelper; +import top.charles7c.cnadmin.common.util.holder.LogContextHolder; +import top.charles7c.cnadmin.monitor.annotation.Log; +import top.charles7c.cnadmin.monitor.config.properties.LogProperties; +import top.charles7c.cnadmin.monitor.enums.LogLevelEnum; +import top.charles7c.cnadmin.monitor.model.entity.SysLog; + +/** + * 操作日志拦截器 + * + * @author Charles7c + * @since 2022/12/24 21:14 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class LogInterceptor implements HandlerInterceptor { + + private final LogProperties operationLogProperties; + + @Override + public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, + @NotNull Object handler) { + if (!checkIsNeedRecord(handler, request)) { + return true; + } + + // 记录操作时间 + this.logCreateTime(); + return true; + } + + @Override + public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, + @NotNull Object handler, Exception e) { + // 记录请求耗时及异常信息 + SysLog sysLog = this.logElapsedTimeAndException(); + if (sysLog == null) { + return; + } + + // 记录描述 + this.logDescription(sysLog, handler); + // 记录请求信息 + this.logRequest(sysLog, request); + // 记录响应信息 + this.logResponse(sysLog, response); + + // 保存操作日志 + SpringUtil.getApplicationContext().publishEvent(sysLog); + } + + /** + * 记录操作时间 + */ + private void logCreateTime() { + OperationLog operationLog = new OperationLog(); + operationLog.setCreateUser(LoginHelper.getUserId()); + operationLog.setCreateTime(new Date()); + LogContextHolder.set(operationLog); + } + + /** + * 记录请求耗时及异常信息 + * + * @return 日志信息 + */ + private SysLog logElapsedTimeAndException() { + OperationLog operationLog = LogContextHolder.get(); + if (operationLog != null) { + LogContextHolder.remove(); + SysLog sysLog = new SysLog(); + sysLog.setCreateTime(operationLog.getCreateTime()); + sysLog.setElapsedTime(System.currentTimeMillis() - sysLog.getCreateTime().getTime()); + sysLog.setLogLevel(LogLevelEnum.INFO); + + // 记录异常信息 + Exception exception = operationLog.getException(); + if (exception != null) { + sysLog.setLogLevel(LogLevelEnum.ERROR); + sysLog.setException(ExceptionUtil.stacktraceToString(operationLog.getException(), -1)); + } + return sysLog; + } + return null; + } + + /** + * 记录日志描述 + * + * @param sysLog + * 日志信息 + * @param handler + * 处理器 + */ + private void logDescription(@NotNull SysLog sysLog, Object handler) { + HandlerMethod handlerMethod = (HandlerMethod)handler; + Operation methodOperation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Operation.class); + Log methodLog = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Log.class); + + if (methodOperation != null) { + sysLog.setDescription( + StrUtil.isNotBlank(methodOperation.summary()) ? methodOperation.summary() : "请在该接口方法上指定操作日志描述"); + } + // 例如:@Log("获取验证码") -> 获取验证码 + if (methodLog != null && StrUtil.isNotBlank(methodLog.value())) { + sysLog.setDescription(methodLog.value()); + } + } + + /** + * 记录请求信息 + * + * @param sysLog + * 日志信息 + * @param request + * 请求对象 + */ + private void logRequest(@NotNull SysLog sysLog, @NotNull HttpServletRequest request) { + sysLog.setRequestUrl(StrUtil.isBlank(request.getQueryString()) ? request.getRequestURL().toString() + : request.getRequestURL().append("?").append(request.getQueryString()).toString()); + sysLog.setRequestMethod(request.getMethod()); + sysLog.setRequestHeader(this.desensitize(ServletUtil.getHeaderMap(request))); + String requestBody = this.getRequestBody(request); + if (StrUtil.isNotBlank(requestBody)) { + sysLog.setRequestBody(this.desensitize( + JSONUtil.isTypeJSON(requestBody) ? JSONUtil.parseObj(requestBody) : ServletUtil.getParamMap(request))); + } + sysLog.setRequestIp(ServletUtil.getClientIP(request)); + sysLog.setLocation(IpUtils.getCityInfo(sysLog.getRequestIp())); + sysLog.setBrowser(ServletUtils.getBrowser(request)); + sysLog.setCreateUser(sysLog.getCreateUser() == null ? LoginHelper.getUserId() : sysLog.getCreateUser()); + } + + /** + * 记录响应信息 + * + * @param sysLog + * 日志信息 + * @param response + * 响应对象 + */ + private void logResponse(SysLog sysLog, HttpServletResponse response) { + sysLog.setStatusCode(response.getStatus()); + sysLog.setResponseHeader(this.desensitize(ServletUtils.getHeaderMap(response))); + // 响应体(不记录非 JSON 响应数据) + String responseBody = this.getResponseBody(response); + if (StrUtil.isNotBlank(responseBody) && JSONUtil.isTypeJSON(responseBody)) { + sysLog.setResponseBody(responseBody); + } + } + + /** + * 数据脱敏 + * + * @param waitDesensitizeData + * 待脱敏数据 + * @return 脱敏后的 JSON 字符串数据 + */ + private String desensitize(Map waitDesensitizeData) { + String desensitizeDataStr = JSONUtil.toJsonStr(waitDesensitizeData); + try { + if (CollUtil.isEmpty(waitDesensitizeData)) { + return desensitizeDataStr; + } + + for (String desensitizeProperty : operationLogProperties.getDesensitize()) { + waitDesensitizeData.computeIfPresent(desensitizeProperty, (k, v) -> "****************"); + waitDesensitizeData.computeIfPresent(desensitizeProperty.toLowerCase(), (k, v) -> "****************"); + waitDesensitizeData.computeIfPresent(desensitizeProperty.toUpperCase(), (k, v) -> "****************"); + } + return JSONUtil.toJsonStr(waitDesensitizeData); + } catch (Exception ignored) { + } + return desensitizeDataStr; + } + + /** + * 获取请求体 + * + * @param request + * 请求对象 + * @return 请求体 + */ + private String getRequestBody(HttpServletRequest request) { + String requestBody = ""; + ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class); + if (wrapper != null) { + requestBody = StrUtil.utf8Str(wrapper.getContentAsByteArray()); + } + return requestBody; + } + + /** + * 获取响应体 + * + * @param response + * 响应对象 + * @return 响应体 + */ + private String getResponseBody(HttpServletResponse response) { + String responseBody = ""; + ContentCachingResponseWrapper wrapper = + WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class); + if (wrapper != null) { + responseBody = StrUtil.utf8Str(wrapper.getContentAsByteArray()); + } + return responseBody; + } + + /** + * 检查是否要记录操作日志 + * + * @param handler + * / + * @param request + * / + * @return true 需要记录,false 不需要记录 + */ + private boolean checkIsNeedRecord(Object handler, HttpServletRequest request) { + // 1、未启用时,不需要记录操作日志 + if (!(handler instanceof HandlerMethod) || Boolean.FALSE.equals(operationLogProperties.getEnabled())) { + return false; + } + + // 2、排除不需要记录日志的接口 + HandlerMethod handlerMethod = (HandlerMethod)handler; + Log methodLog = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Log.class); + // 2.1 请求方式不要求记录且请求上没有 @Log 注解,则不记录操作日志 + if (operationLogProperties.getExcludeMethods().contains(request.getMethod()) && methodLog == null) { + return false; + } + // 2.2 如果接口上既没有 @Log 注解,也没有 @Operation 注解,则不记录操作日志 + Operation methodOperation = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Operation.class); + if (methodLog == null && methodOperation == null) { + return false; + } + // 2.3 如果接口被隐藏,不记录操作日志 + if (methodOperation != null && methodOperation.hidden()) { + return false; + } + // 2.4 如果接口上有 @Log 注解,但是要求忽略该接口,则不记录操作日志 + return methodLog == null || !methodLog.ignore(); + } +} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/mapper/LogMapper.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/mapper/LogMapper.java new file mode 100644 index 00000000..da1db18a --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/mapper/LogMapper.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.monitor.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import top.charles7c.cnadmin.monitor.model.entity.SysLog; + +/** + * 操作日志 Mapper + * + * @author Charles7c + * @since 2022/12/22 21:47 + */ +public interface LogMapper extends BaseMapper {} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/entity/SysLog.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/entity/SysLog.java new file mode 100644 index 00000000..aa675efd --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/model/entity/SysLog.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.monitor.model.entity; + +import java.io.Serializable; +import java.util.Date; + +import lombok.Data; + +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import top.charles7c.cnadmin.monitor.enums.LogLevelEnum; + +/** + * 操作日志实体 + * + * @author Charles7c + * @since 2022/12/25 9:11 + */ +@Data +@TableName("sys_log") +public class SysLog implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 日志ID + */ + @TableId + private Long logId; + + /** + * 日志级别 + */ + private LogLevelEnum logLevel; + + /** + * 日志描述 + */ + private String description; + + /** + * 请求 URL + */ + private String requestUrl; + + /** + * 请求方式 + */ + private String requestMethod; + + /** + * 请求头 + */ + private String requestHeader; + + /** + * 请求体 + */ + private String requestBody; + + /** + * 状态码 + */ + private Integer statusCode; + + /** + * 响应头 + */ + private String responseHeader; + + /** + * 响应体 + */ + private String responseBody; + + /** + * 请求耗时(ms) + */ + private Long elapsedTime; + + /** + * 请求IP + */ + private String requestIp; + + /** + * 操作地址 + */ + private String location; + + /** + * 浏览器 + */ + private String browser; + + /** + * 异常 + */ + private String exception; + + /** + * 操作人 + */ + private Long createUser; + + /** + * 操作时间 + */ + private Date createTime; +} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/LogService.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/LogService.java new file mode 100644 index 00000000..5c522c8d --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/LogService.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.monitor.service; + +/** + * 操作日志业务接口 + * + * @author Charles7c + * @since 2022/12/23 20:12 + */ +public interface LogService { + +} diff --git a/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/impl/LogServiceImpl.java b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/impl/LogServiceImpl.java new file mode 100644 index 00000000..fabb8220 --- /dev/null +++ b/continew-admin-monitor/src/main/java/top/charles7c/cnadmin/monitor/service/impl/LogServiceImpl.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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.cnadmin.monitor.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import top.charles7c.cnadmin.monitor.mapper.LogMapper; +import top.charles7c.cnadmin.monitor.model.entity.SysLog; +import top.charles7c.cnadmin.monitor.service.LogService; + +/** + * 操作日志业务实现类 + * + * @author Charles7c + * @since 2022/12/23 20:12 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LogServiceImpl implements LogService { + + private final LogMapper logMapper; + + @Async + @EventListener + public void save(SysLog sysLog) { + logMapper.insert(sysLog); + } +} diff --git a/continew-admin-monitor/src/main/resources/mapper/LogMapper.xml b/continew-admin-monitor/src/main/resources/mapper/LogMapper.xml new file mode 100644 index 00000000..2c4a88b9 --- /dev/null +++ b/continew-admin-monitor/src/main/resources/mapper/LogMapper.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/continew-admin-webapi/pom.xml b/continew-admin-webapi/pom.xml index 8cb0cdde..8e9810dd 100644 --- a/continew-admin-webapi/pom.xml +++ b/continew-admin-webapi/pom.xml @@ -44,6 +44,12 @@ limitations under the License. test
+ + + top.charles7c + continew-admin-monitor + + top.charles7c diff --git a/continew-admin-webapi/src/main/resources/application.yml b/continew-admin-webapi/src/main/resources/application.yml index 672b0fa3..a80ef09c 100644 --- a/continew-admin-webapi/src/main/resources/application.yml +++ b/continew-admin-webapi/src/main/resources/application.yml @@ -19,6 +19,8 @@ continew-admin: license: name: Apache-2.0 url: https://github.com/Charles7c/continew-admin/blob/dev/LICENSE + # 是否本地解析 IP 归属地 + ipAddrLocalParseEnabled: false --- ### 日志配置(重叠部分,优先级高于 logback-spring.xml 中的配置) logging: @@ -27,6 +29,17 @@ logging: file: path: @logging.file.path@ config: classpath:logback-spring.xml + ## 操作日志配置 + operation: + # 是否启用操作日志 + enabled: true + # 不记录操作日志的请求方式 + #excludeMethods: + # - GET + # 脱敏字段 + desensitize: + - password + - Authorization --- ### 接口文档配置 springdoc: @@ -169,4 +182,13 @@ spring: # 反序列化配置(JSON -> Bean) deserialization: # 允许反序列化不存在的属性 - fail_on_unknown_properties: false \ No newline at end of file + fail_on_unknown_properties: false + +--- ### 线程池配置 +thread-pool: + # 是否启用线程池 + enabled: true + # 队列容量 + queueCapacity: 128 + # 活跃时间 + keepAliveSeconds: 300 diff --git a/continew-admin-webapi/src/main/resources/db/changelog/v0.0.1/continew-admin_table.sql b/continew-admin-webapi/src/main/resources/db/changelog/v0.0.1/continew-admin_table.sql index 2a8c70e7..0c6853d4 100644 --- a/continew-admin-webapi/src/main/resources/db/changelog/v0.0.1/continew-admin_table.sql +++ b/continew-admin-webapi/src/main/resources/db/changelog/v0.0.1/continew-admin_table.sql @@ -22,4 +22,27 @@ CREATE TABLE IF NOT EXISTS `sys_user` ( UNIQUE INDEX `uk_email`(`email`) USING BTREE, INDEX `idx_createUser`(`create_user`) USING BTREE, INDEX `idx_updateUser`(`update_user`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; \ No newline at end of file +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; + +-- changeset Charles7c:2 +CREATE TABLE IF NOT EXISTS `sys_log` ( + `log_id` bigint(20) unsigned AUTO_INCREMENT COMMENT '日志ID', + `log_level` varchar(255) DEFAULT NULL COMMENT '日志级别', + `description` varchar(255) DEFAULT NULL COMMENT '日志描述', + `request_url` varchar(512) NOT NULL DEFAULT '' COMMENT '请求URL', + `request_method` varchar(10) DEFAULT NULL COMMENT '请求方式', + `request_header` text COMMENT '请求头', + `request_body` text DEFAULT NULL COMMENT '请求体', + `status_code` int(11) unsigned DEFAULT NULL COMMENT '状态码', + `response_header` text DEFAULT NULL COMMENT '响应头', + `response_body` text DEFAULT NULL COMMENT '响应体', + `elapsed_time` bigint(20) unsigned DEFAULT NULL COMMENT '请求耗时(ms)', + `request_ip` varchar(255) DEFAULT NULL COMMENT '请求IP', + `location` varchar(512) DEFAULT NULL COMMENT '操作地址', + `browser` varchar(255) DEFAULT NULL COMMENT '浏览器', + `exception` text DEFAULT NULL COMMENT '异常', + `create_user` bigint(20) unsigned DEFAULT NULL COMMENT '操作人', + `create_time` datetime NOT NULL COMMENT '操作时间', + PRIMARY KEY (`log_id`) USING BTREE, + INDEX `idx_createUser`(`create_user`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表'; diff --git a/pom.xml b/pom.xml index 5ab9f3cb..09650ecc 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ limitations under the License. continew-admin-webapi + continew-admin-monitor continew-admin-system continew-admin-common @@ -50,6 +51,7 @@ limitations under the License. 3.9.1 + 2.7.6 4.0.0 3.19.0 1.6.2 @@ -118,6 +120,13 @@ limitations under the License. + + + net.dreamlu + mica-ip2region + ${ip2region.version} + + com.github.xiaoymin @@ -156,6 +165,13 @@ limitations under the License. ${project.version} + + + top.charles7c + continew-admin-monitor + ${project.version} + + top.charles7c