mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-21 18:57:10 +08:00
refactor(log): 新增 LogHandler 提升日志模块的复用性
This commit is contained in:
@@ -19,7 +19,9 @@ package top.continew.starter.log.aspect;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
import org.aspectj.lang.annotation.*;
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
@@ -47,12 +49,47 @@ public class AccessLogAspect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切点 - 匹配所有控制器层的方法
|
* 切点 - 匹配所有控制器层的 GET 请求方法
|
||||||
*/
|
*/
|
||||||
@Pointcut("execution(* *..controller.*.*(..)) || execution(* *..*Controller.*(..))")
|
@Pointcut("within(@org.springframework.web.bind.annotation.RequestMapping *)")
|
||||||
public void pointcut() {
|
public void pointcut() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切点 - 匹配所有控制器层的 GET 请求方法
|
||||||
|
*/
|
||||||
|
@Pointcut("within(@org.springframework.web.bind.annotation.GetMapping *)")
|
||||||
|
public void pointcutGet() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切点 - 匹配所有控制器层的 POST 请求方法
|
||||||
|
*/
|
||||||
|
@Pointcut("within(@org.springframework.web.bind.annotation.PostMapping *)")
|
||||||
|
public void pointcutPost() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切点 - 匹配所有控制器层的 PUT 请求方法
|
||||||
|
*/
|
||||||
|
@Pointcut("within(@org.springframework.web.bind.annotation.PutMapping *)")
|
||||||
|
public void pointcutPut() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切点 - 匹配所有控制器层的 DELETE 请求方法
|
||||||
|
*/
|
||||||
|
@Pointcut("within(@org.springframework.web.bind.annotation.DeleteMapping *)")
|
||||||
|
public void pointcutDelete() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切点 - 匹配所有控制器层的 PATCH 请求方法
|
||||||
|
*/
|
||||||
|
@Pointcut("within(@org.springframework.web.bind.annotation.PatchMapping *)")
|
||||||
|
public void pointcutPatch() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打印访问日志
|
* 打印访问日志
|
||||||
*
|
*
|
||||||
@@ -60,7 +97,7 @@ public class AccessLogAspect {
|
|||||||
* @return 返回结果
|
* @return 返回结果
|
||||||
* @throws Throwable 异常
|
* @throws Throwable 异常
|
||||||
*/
|
*/
|
||||||
@Around("pointcut()")
|
@Around("pointcut() || pointcutGet() || pointcutPost() || pointcutPut() || pointcutDelete() || pointcutPatch()")
|
||||||
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||||
Instant startTime = Instant.now();
|
Instant startTime = Instant.now();
|
||||||
// 非 Web 环境不记录
|
// 非 Web 环境不记录
|
||||||
|
@@ -32,15 +32,11 @@ import org.springframework.web.context.request.ServletRequestAttributes;
|
|||||||
import top.continew.starter.log.annotation.Log;
|
import top.continew.starter.log.annotation.Log;
|
||||||
import top.continew.starter.log.autoconfigure.LogProperties;
|
import top.continew.starter.log.autoconfigure.LogProperties;
|
||||||
import top.continew.starter.log.dao.LogDao;
|
import top.continew.starter.log.dao.LogDao;
|
||||||
import top.continew.starter.log.enums.Include;
|
import top.continew.starter.log.handler.LogHandler;
|
||||||
import top.continew.starter.log.http.recordable.impl.RecordableServletHttpRequest;
|
|
||||||
import top.continew.starter.log.http.recordable.impl.RecordableServletHttpResponse;
|
|
||||||
import top.continew.starter.log.model.LogRecord;
|
import top.continew.starter.log.model.LogRecord;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志切面
|
* 日志切面
|
||||||
@@ -53,12 +49,14 @@ import java.util.Set;
|
|||||||
public class LogAspect {
|
public class LogAspect {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
|
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
|
||||||
private final LogDao logDao;
|
|
||||||
private final LogProperties logProperties;
|
private final LogProperties logProperties;
|
||||||
|
private final LogHandler logHandler;
|
||||||
|
private final LogDao logDao;
|
||||||
|
|
||||||
public LogAspect(LogDao logDao, LogProperties logProperties) {
|
public LogAspect(LogProperties logProperties, LogHandler logHandler, LogDao logDao) {
|
||||||
this.logDao = logDao;
|
|
||||||
this.logProperties = logProperties;
|
this.logProperties = logProperties;
|
||||||
|
this.logHandler = logHandler;
|
||||||
|
this.logDao = logDao;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,7 +85,7 @@ public class LogAspect {
|
|||||||
HttpServletResponse response = attributes.getResponse();
|
HttpServletResponse response = attributes.getResponse();
|
||||||
String errorMsg = null;
|
String errorMsg = null;
|
||||||
// 开始记录
|
// 开始记录
|
||||||
LogRecord.Started startedLogRecord = LogRecord.start(startTime, new RecordableServletHttpRequest(request));
|
LogRecord.Started startedLogRecord = logHandler.start(startTime, request);
|
||||||
try {
|
try {
|
||||||
// 执行目标方法
|
// 执行目标方法
|
||||||
return joinPoint.proceed();
|
return joinPoint.proceed();
|
||||||
@@ -95,50 +93,23 @@ public class LogAspect {
|
|||||||
errorMsg = CharSequenceUtil.sub(e.getMessage(), 0, 2000);
|
errorMsg = CharSequenceUtil.sub(e.getMessage(), 0, 2000);
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
// 结束记录
|
|
||||||
this.logFinish(startedLogRecord, errorMsg, response, joinPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 结束记录日志
|
|
||||||
*
|
|
||||||
* @param startedLogRecord 日志记录器
|
|
||||||
* @param errorMsg 异常信息
|
|
||||||
* @param response 响应对象
|
|
||||||
* @param joinPoint 切点
|
|
||||||
*/
|
|
||||||
private void logFinish(LogRecord.Started startedLogRecord,
|
|
||||||
String errorMsg,
|
|
||||||
HttpServletResponse response,
|
|
||||||
ProceedingJoinPoint joinPoint) {
|
|
||||||
try {
|
try {
|
||||||
Instant endTime = Instant.now();
|
Instant endTime = Instant.now();
|
||||||
Method method = this.getMethod(joinPoint);
|
Method targetMethod = this.getMethod(joinPoint);
|
||||||
Class<?> targetClass = joinPoint.getTarget().getClass();
|
Class<?> targetClass = joinPoint.getTarget().getClass();
|
||||||
Log methodLog = method.getAnnotation(Log.class);
|
LogRecord logRecord = logHandler.finish(startedLogRecord, endTime, response, logProperties
|
||||||
Log classLog = targetClass.getAnnotation(Log.class);
|
.getIncludes(), targetMethod, targetClass);
|
||||||
Set<Include> includeSet = this.getIncludes(methodLog, classLog);
|
|
||||||
LogRecord finishedLogRecord = startedLogRecord
|
|
||||||
.finish(endTime, new RecordableServletHttpResponse(response, response.getStatus()), includeSet);
|
|
||||||
// 记录异常信息
|
// 记录异常信息
|
||||||
if (errorMsg != null) {
|
if (errorMsg != null) {
|
||||||
finishedLogRecord.setErrorMsg(errorMsg);
|
logRecord.setErrorMsg(errorMsg);
|
||||||
}
|
}
|
||||||
// 记录日志描述
|
logDao.add(logRecord);
|
||||||
if (includeSet.contains(Include.DESCRIPTION)) {
|
|
||||||
this.logDescription(finishedLogRecord, methodLog);
|
|
||||||
}
|
|
||||||
// 记录所属模块
|
|
||||||
if (includeSet.contains(Include.MODULE)) {
|
|
||||||
this.logModule(finishedLogRecord, methodLog, classLog);
|
|
||||||
}
|
|
||||||
logDao.add(finishedLogRecord);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Logging http log occurred an error: {}.", e.getMessage(), e);
|
log.error("Logging http log occurred an error: {}.", e.getMessage(), e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取方法
|
* 获取方法
|
||||||
@@ -150,74 +121,4 @@ public class LogAspect {
|
|||||||
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
|
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
|
||||||
return signature.getMethod();
|
return signature.getMethod();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取日志包含信息
|
|
||||||
*
|
|
||||||
* @param methodLog 方法级 Log 注解
|
|
||||||
* @param classLog 类级 Log 注解
|
|
||||||
* @return 日志包含信息
|
|
||||||
*/
|
|
||||||
private Set<Include> getIncludes(Log methodLog, Log classLog) {
|
|
||||||
Set<Include> includeSet = new HashSet<>(logProperties.getIncludes());
|
|
||||||
if (null != classLog) {
|
|
||||||
processInclude(includeSet, classLog);
|
|
||||||
}
|
|
||||||
if (null != methodLog) {
|
|
||||||
processInclude(includeSet, methodLog);
|
|
||||||
}
|
|
||||||
return includeSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理日志包含信息
|
|
||||||
*
|
|
||||||
* @param includes 日志包含信息
|
|
||||||
* @param logAnnotation Log 注解
|
|
||||||
*/
|
|
||||||
private void processInclude(Set<Include> includes, Log logAnnotation) {
|
|
||||||
Include[] includeArr = logAnnotation.includes();
|
|
||||||
if (includeArr.length > 0) {
|
|
||||||
includes.addAll(Set.of(includeArr));
|
|
||||||
}
|
|
||||||
Include[] excludeArr = logAnnotation.excludes();
|
|
||||||
if (excludeArr.length > 0) {
|
|
||||||
includes.removeAll(Set.of(excludeArr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录描述
|
|
||||||
*
|
|
||||||
* @param logRecord 日志信息
|
|
||||||
* @param methodLog 方法级 Log 注解
|
|
||||||
*/
|
|
||||||
private void logDescription(LogRecord logRecord, Log methodLog) {
|
|
||||||
// 例如:@Log("新增部门") -> 新增部门
|
|
||||||
if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.value())) {
|
|
||||||
logRecord.setDescription(methodLog.value());
|
|
||||||
} else {
|
|
||||||
logRecord.setDescription("请在该接口方法上指定日志描述");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录模块
|
|
||||||
*
|
|
||||||
* @param logRecord 日志信息
|
|
||||||
* @param methodLog 方法级 Log 注解
|
|
||||||
* @param classLog 类级 Log 注解
|
|
||||||
*/
|
|
||||||
private void logModule(LogRecord logRecord, Log methodLog, Log classLog) {
|
|
||||||
// 例如:@Log(module = "部门管理") -> 部门管理
|
|
||||||
// 优先使用方法注解的模块
|
|
||||||
if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.module())) {
|
|
||||||
logRecord.setModule(methodLog.module());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 其次使用类注解的模块
|
|
||||||
if (null != classLog) {
|
|
||||||
logRecord.setModule(CharSequenceUtil.blankToDefault(classLog.module(), "请在该接口类上指定所属模块"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,8 @@ import top.continew.starter.log.aspect.AccessLogAspect;
|
|||||||
import top.continew.starter.log.aspect.LogAspect;
|
import top.continew.starter.log.aspect.LogAspect;
|
||||||
import top.continew.starter.log.dao.LogDao;
|
import top.continew.starter.log.dao.LogDao;
|
||||||
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
|
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
|
||||||
|
import top.continew.starter.log.handler.AopLogHandler;
|
||||||
|
import top.continew.starter.log.handler.LogHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志自动配置
|
* 日志自动配置
|
||||||
@@ -53,12 +55,14 @@ public class LogAutoConfiguration {
|
|||||||
/**
|
/**
|
||||||
* 日志切面
|
* 日志切面
|
||||||
*
|
*
|
||||||
|
* @param logHandler 日志处理器
|
||||||
|
* @param logDao 日志持久层接口
|
||||||
* @return {@link LogAspect }
|
* @return {@link LogAspect }
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public LogAspect logAspect(LogDao logDao) {
|
public LogAspect logAspect(LogHandler logHandler, LogDao logDao) {
|
||||||
return new LogAspect(logDao, logProperties);
|
return new LogAspect(logProperties, logHandler, logDao);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,6 +76,15 @@ public class LogAutoConfiguration {
|
|||||||
return new AccessLogAspect(logProperties);
|
return new AccessLogAspect(logProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志处理器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public LogHandler logHandler() {
|
||||||
|
return new AopLogHandler();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志持久层接口
|
* 日志持久层接口
|
||||||
*/
|
*/
|
||||||
|
@@ -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.continew.starter.log.handler;
|
||||||
|
|
||||||
|
import cn.hutool.core.text.CharSequenceUtil;
|
||||||
|
import top.continew.starter.log.model.LogRecord;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志处理器-AOP 版实现
|
||||||
|
*
|
||||||
|
* @author Charles7c
|
||||||
|
* @since 2.8.0
|
||||||
|
*/
|
||||||
|
public class AopLogHandler extends AbstractLogHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logDescription(LogRecord logRecord, Method targetMethod) {
|
||||||
|
super.logDescription(logRecord, targetMethod);
|
||||||
|
if (CharSequenceUtil.isBlank(logRecord.getDescription())) {
|
||||||
|
logRecord.setDescription("请在该接口方法上指定日志描述");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logModule(LogRecord logRecord, Method targetMethod, Class<?> targetClass) {
|
||||||
|
super.logModule(logRecord, targetMethod, targetClass);
|
||||||
|
if (CharSequenceUtil.isBlank(logRecord.getModule())) {
|
||||||
|
logRecord.setModule("请在该接口类上指定所属模块");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* 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.continew.starter.log.handler;
|
||||||
|
|
||||||
|
import cn.hutool.core.annotation.AnnotationUtil;
|
||||||
|
import cn.hutool.core.text.CharSequenceUtil;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import top.continew.starter.log.annotation.Log;
|
||||||
|
import top.continew.starter.log.enums.Include;
|
||||||
|
import top.continew.starter.log.http.recordable.impl.RecordableServletHttpRequest;
|
||||||
|
import top.continew.starter.log.http.recordable.impl.RecordableServletHttpResponse;
|
||||||
|
import top.continew.starter.log.model.LogRecord;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志处理器基类
|
||||||
|
*
|
||||||
|
* @author Charles7c
|
||||||
|
* @since 2.8.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractLogHandler implements LogHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LogRecord.Started start(Instant startTime, HttpServletRequest request) {
|
||||||
|
return LogRecord.start(startTime, new RecordableServletHttpRequest(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LogRecord finish(LogRecord.Started started,
|
||||||
|
Instant endTime,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Set<Include> includes,
|
||||||
|
Method targetMethod,
|
||||||
|
Class<?> targetClass) {
|
||||||
|
Set<Include> includeSet = this.getIncludes(includes, targetMethod, targetClass);
|
||||||
|
LogRecord logRecord = this.finish(started, endTime, response, includeSet);
|
||||||
|
// 记录日志描述
|
||||||
|
if (includeSet.contains(Include.DESCRIPTION)) {
|
||||||
|
this.logDescription(logRecord, targetMethod);
|
||||||
|
}
|
||||||
|
// 记录所属模块
|
||||||
|
if (includeSet.contains(Include.MODULE)) {
|
||||||
|
this.logModule(logRecord, targetMethod, targetClass);
|
||||||
|
}
|
||||||
|
return logRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LogRecord finish(LogRecord.Started started,
|
||||||
|
Instant endTime,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Set<Include> includes) {
|
||||||
|
return started.finish(endTime, new RecordableServletHttpResponse(response, response.getStatus()), includes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录日志描述
|
||||||
|
*
|
||||||
|
* @param logRecord 日志记录
|
||||||
|
* @param targetMethod 目标方法
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void logDescription(LogRecord logRecord, Method targetMethod) {
|
||||||
|
Log methodLog = AnnotationUtil.getAnnotation(targetMethod, Log.class);
|
||||||
|
// 例如:@Log("新增部门") -> 新增部门
|
||||||
|
if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.value())) {
|
||||||
|
logRecord.setDescription(methodLog.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录所属模块
|
||||||
|
*
|
||||||
|
* @param logRecord 日志记录
|
||||||
|
* @param targetMethod 目标方法
|
||||||
|
* @param targetClass 目标类
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void logModule(LogRecord logRecord, Method targetMethod, Class<?> targetClass) {
|
||||||
|
Log methodLog = AnnotationUtil.getAnnotation(targetMethod, Log.class);
|
||||||
|
// 例如:@Log(module = "部门管理") -> 部门管理
|
||||||
|
// 方法级注解优先级高于类级注解
|
||||||
|
if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.module())) {
|
||||||
|
logRecord.setModule(methodLog.module());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log classLog = AnnotationUtil.getAnnotation(targetClass, Log.class);
|
||||||
|
if (null != classLog && CharSequenceUtil.isNotBlank(classLog.module())) {
|
||||||
|
logRecord.setModule(classLog.module());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Include> getIncludes(Set<Include> includes, Method targetMethod, Class<?> targetClass) {
|
||||||
|
Log classLog = AnnotationUtil.getAnnotation(targetClass, Log.class);
|
||||||
|
Set<Include> includeSet = new HashSet<>(includes);
|
||||||
|
if (null != classLog) {
|
||||||
|
this.processInclude(includeSet, classLog);
|
||||||
|
}
|
||||||
|
// 方法级注解优先级高于类级注解
|
||||||
|
Log methodLog = AnnotationUtil.getAnnotation(targetMethod, Log.class);
|
||||||
|
if (null != methodLog) {
|
||||||
|
this.processInclude(includeSet, methodLog);
|
||||||
|
}
|
||||||
|
return includeSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理日志包含信息
|
||||||
|
*
|
||||||
|
* @param includes 日志包含信息
|
||||||
|
* @param logAnnotation Log 注解
|
||||||
|
*/
|
||||||
|
private void processInclude(Set<Include> includes, Log logAnnotation) {
|
||||||
|
Include[] includeArr = logAnnotation.includes();
|
||||||
|
if (includeArr.length > 0) {
|
||||||
|
includes.addAll(Set.of(includeArr));
|
||||||
|
}
|
||||||
|
Include[] excludeArr = logAnnotation.excludes();
|
||||||
|
if (excludeArr.length > 0) {
|
||||||
|
includes.removeAll(Set.of(excludeArr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
* 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.continew.starter.log.handler;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import top.continew.starter.log.enums.Include;
|
||||||
|
import top.continew.starter.log.model.LogRecord;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志处理器
|
||||||
|
*
|
||||||
|
* @author Charles7c
|
||||||
|
* @since 2.8.0
|
||||||
|
*/
|
||||||
|
public interface LogHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始日志记录
|
||||||
|
*
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param request 请求对象
|
||||||
|
* @return 日志记录器
|
||||||
|
*/
|
||||||
|
LogRecord.Started start(Instant startTime, HttpServletRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束日志记录
|
||||||
|
*
|
||||||
|
* @param started 开始日志记录器
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @param response 响应对象
|
||||||
|
* @param includes 包含信息
|
||||||
|
* @return 日志记录
|
||||||
|
*/
|
||||||
|
LogRecord finish(LogRecord.Started started, Instant endTime, HttpServletResponse response, Set<Include> includes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束日志记录
|
||||||
|
*
|
||||||
|
* @param started 开始日志记录器-
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @param response 响应对象
|
||||||
|
* @param includes 包含信息
|
||||||
|
* @param targetMethod 目标方法
|
||||||
|
* @param targetClass 目标类
|
||||||
|
* @return 日志记录
|
||||||
|
*/
|
||||||
|
LogRecord finish(LogRecord.Started started,
|
||||||
|
Instant endTime,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Set<Include> includes,
|
||||||
|
Method targetMethod,
|
||||||
|
Class<?> targetClass);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录日志描述
|
||||||
|
*
|
||||||
|
* @param logRecord 日志记录
|
||||||
|
* @param targetMethod 目标方法
|
||||||
|
*/
|
||||||
|
void logDescription(LogRecord logRecord, Method targetMethod);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录所属模块
|
||||||
|
*
|
||||||
|
* @param logRecord 日志记录
|
||||||
|
* @param targetMethod 目标方法
|
||||||
|
* @param targetClass 目标类
|
||||||
|
*/
|
||||||
|
void logModule(LogRecord logRecord, Method targetMethod, Class<?> targetClass);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志包含信息
|
||||||
|
*
|
||||||
|
* @param includes 默认包含信息
|
||||||
|
* @param targetMethod 目标方法
|
||||||
|
* @param targetClass 目标类
|
||||||
|
* @return 日志包含信息
|
||||||
|
*/
|
||||||
|
Set<Include> getIncludes(Set<Include> includes, Method targetMethod, Class<?> targetClass);
|
||||||
|
}
|
@@ -26,11 +26,13 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import top.continew.starter.log.annotation.ConditionalOnEnabledLog;
|
||||||
import top.continew.starter.log.dao.LogDao;
|
import top.continew.starter.log.dao.LogDao;
|
||||||
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
|
import top.continew.starter.log.dao.impl.DefaultLogDaoImpl;
|
||||||
import top.continew.starter.log.annotation.ConditionalOnEnabledLog;
|
import top.continew.starter.log.handler.InterceptorLogHandler;
|
||||||
import top.continew.starter.log.handler.LogFilter;
|
import top.continew.starter.log.handler.LogFilter;
|
||||||
import top.continew.starter.log.handler.LogInterceptor;
|
import top.continew.starter.log.handler.LogHandler;
|
||||||
|
import top.continew.starter.log.interceptor.LogInterceptor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志自动配置
|
* 日志自动配置
|
||||||
@@ -53,7 +55,7 @@ public class LogAutoConfiguration implements WebMvcConfigurer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
registry.addInterceptor(new LogInterceptor(logDao(), logProperties));
|
registry.addInterceptor(new LogInterceptor(logProperties, logHandler(), logDao()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,6 +67,15 @@ public class LogAutoConfiguration implements WebMvcConfigurer {
|
|||||||
return new LogFilter(logProperties);
|
return new LogFilter(logProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志处理器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public LogHandler logHandler() {
|
||||||
|
return new InterceptorLogHandler();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志持久层接口
|
* 日志持久层接口
|
||||||
*/
|
*/
|
||||||
|
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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.continew.starter.log.handler;
|
||||||
|
|
||||||
|
import cn.hutool.core.annotation.AnnotationUtil;
|
||||||
|
import cn.hutool.core.text.CharSequenceUtil;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import top.continew.starter.log.model.LogRecord;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志处理器-拦截器版实现
|
||||||
|
*
|
||||||
|
* @author Charles7c
|
||||||
|
* @since 2.8.0
|
||||||
|
*/
|
||||||
|
public class InterceptorLogHandler extends AbstractLogHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logDescription(LogRecord logRecord, Method targetMethod) {
|
||||||
|
super.logDescription(logRecord, targetMethod);
|
||||||
|
if (CharSequenceUtil.isNotBlank(logRecord.getDescription())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 例如:@Operation(summary="新增部门") -> 新增部门
|
||||||
|
Operation methodOperation = AnnotationUtil.getAnnotation(targetMethod, Operation.class);
|
||||||
|
if (null != methodOperation) {
|
||||||
|
logRecord.setDescription(CharSequenceUtil.blankToDefault(methodOperation.summary(), "请在该接口方法上指定日志描述"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void logModule(LogRecord logRecord, Method targetMethod, Class<?> targetClass) {
|
||||||
|
super.logModule(logRecord, targetMethod, targetClass);
|
||||||
|
if (CharSequenceUtil.isNotBlank(logRecord.getModule())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 例如:@Tag(name = "部门管理") -> 部门管理
|
||||||
|
Tag classTag = AnnotationUtil.getAnnotation(targetClass, Tag.class);
|
||||||
|
if (null != classTag) {
|
||||||
|
String name = classTag.name();
|
||||||
|
logRecord.setModule(CharSequenceUtil.blankToDefault(name, "请在该接口类上指定所属模块"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -14,13 +14,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package top.continew.starter.log.handler;
|
package top.continew.starter.log.interceptor;
|
||||||
|
|
||||||
import cn.hutool.core.text.CharSequenceUtil;
|
|
||||||
import com.alibaba.ttl.TransmittableThreadLocal;
|
import com.alibaba.ttl.TransmittableThreadLocal;
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -28,18 +26,15 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.web.method.HandlerMethod;
|
import org.springframework.web.method.HandlerMethod;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
import top.continew.starter.log.http.recordable.impl.RecordableServletHttpRequest;
|
|
||||||
import top.continew.starter.log.http.recordable.impl.RecordableServletHttpResponse;
|
|
||||||
import top.continew.starter.log.annotation.Log;
|
import top.continew.starter.log.annotation.Log;
|
||||||
import top.continew.starter.log.dao.LogDao;
|
|
||||||
import top.continew.starter.log.enums.Include;
|
|
||||||
import top.continew.starter.log.model.LogRecord;
|
|
||||||
import top.continew.starter.log.autoconfigure.LogProperties;
|
import top.continew.starter.log.autoconfigure.LogProperties;
|
||||||
|
import top.continew.starter.log.dao.LogDao;
|
||||||
|
import top.continew.starter.log.handler.LogHandler;
|
||||||
|
import top.continew.starter.log.model.LogRecord;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 日志拦截器
|
* 日志拦截器
|
||||||
@@ -50,14 +45,16 @@ import java.util.Set;
|
|||||||
public class LogInterceptor implements HandlerInterceptor {
|
public class LogInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(LogInterceptor.class);
|
private static final Logger log = LoggerFactory.getLogger(LogInterceptor.class);
|
||||||
private final LogDao logDao;
|
|
||||||
private final LogProperties logProperties;
|
private final LogProperties logProperties;
|
||||||
|
private final LogHandler logHandler;
|
||||||
|
private final LogDao logDao;
|
||||||
private final TransmittableThreadLocal<Instant> timeTtl = new TransmittableThreadLocal<>();
|
private final TransmittableThreadLocal<Instant> timeTtl = new TransmittableThreadLocal<>();
|
||||||
private final TransmittableThreadLocal<LogRecord.Started> logTtl = new TransmittableThreadLocal<>();
|
private final TransmittableThreadLocal<LogRecord.Started> logTtl = new TransmittableThreadLocal<>();
|
||||||
|
|
||||||
public LogInterceptor(LogDao logDao, LogProperties logProperties) {
|
public LogInterceptor(LogProperties logProperties, LogHandler logHandler, LogDao logDao) {
|
||||||
this.logDao = logDao;
|
|
||||||
this.logProperties = logProperties;
|
this.logProperties = logProperties;
|
||||||
|
this.logHandler = logHandler;
|
||||||
|
this.logDao = logDao;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -69,8 +66,9 @@ public class LogInterceptor implements HandlerInterceptor {
|
|||||||
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
|
log.info("[{}] {}", request.getMethod(), request.getRequestURI());
|
||||||
timeTtl.set(startTime);
|
timeTtl.set(startTime);
|
||||||
}
|
}
|
||||||
|
// 开始日志记录
|
||||||
if (this.isRequestRecord(handler, request)) {
|
if (this.isRequestRecord(handler, request)) {
|
||||||
LogRecord.Started startedLogRecord = LogRecord.start(startTime, new RecordableServletHttpRequest(request));
|
LogRecord.Started startedLogRecord = logHandler.start(startTime, request);
|
||||||
logTtl.set(startedLogRecord);
|
logTtl.set(startedLogRecord);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -92,21 +90,13 @@ public class LogInterceptor implements HandlerInterceptor {
|
|||||||
if (null == startedLogRecord) {
|
if (null == startedLogRecord) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 结束日志记录
|
||||||
HandlerMethod handlerMethod = (HandlerMethod)handler;
|
HandlerMethod handlerMethod = (HandlerMethod)handler;
|
||||||
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
|
Method targetMethod = handlerMethod.getMethod();
|
||||||
Log classLog = handlerMethod.getBeanType().getDeclaredAnnotation(Log.class);
|
Class<?> targetClass = handlerMethod.getBeanType();
|
||||||
Set<Include> includeSet = this.getIncludes(methodLog, classLog);
|
LogRecord logRecord = logHandler.finish(startedLogRecord, endTime, response, logProperties
|
||||||
LogRecord finishedLogRecord = startedLogRecord
|
.getIncludes(), targetMethod, targetClass);
|
||||||
.finish(endTime, new RecordableServletHttpResponse(response, response.getStatus()), includeSet);
|
logDao.add(logRecord);
|
||||||
// 记录日志描述
|
|
||||||
if (includeSet.contains(Include.DESCRIPTION)) {
|
|
||||||
this.logDescription(finishedLogRecord, methodLog, handlerMethod);
|
|
||||||
}
|
|
||||||
// 记录所属模块
|
|
||||||
if (includeSet.contains(Include.MODULE)) {
|
|
||||||
this.logModule(finishedLogRecord, methodLog, classLog, handlerMethod);
|
|
||||||
}
|
|
||||||
logDao.add(finishedLogRecord);
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("Logging http log occurred an error: {}.", ex.getMessage(), ex);
|
log.error("Logging http log occurred an error: {}.", ex.getMessage(), ex);
|
||||||
throw ex;
|
throw ex;
|
||||||
@@ -116,87 +106,6 @@ public class LogInterceptor implements HandlerInterceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取日志包含信息
|
|
||||||
*
|
|
||||||
* @param methodLog 方法级 Log 注解
|
|
||||||
* @param classLog 类级 Log 注解
|
|
||||||
* @return 日志包含信息
|
|
||||||
*/
|
|
||||||
private Set<Include> getIncludes(Log methodLog, Log classLog) {
|
|
||||||
Set<Include> includeSet = new HashSet<>(logProperties.getIncludes());
|
|
||||||
if (null != classLog) {
|
|
||||||
this.processInclude(includeSet, classLog);
|
|
||||||
}
|
|
||||||
if (null != methodLog) {
|
|
||||||
this.processInclude(includeSet, methodLog);
|
|
||||||
}
|
|
||||||
return includeSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理日志包含信息
|
|
||||||
*
|
|
||||||
* @param includes 日志包含信息
|
|
||||||
* @param logAnnotation Log 注解
|
|
||||||
*/
|
|
||||||
private void processInclude(Set<Include> includes, Log logAnnotation) {
|
|
||||||
Include[] includeArr = logAnnotation.includes();
|
|
||||||
if (includeArr.length > 0) {
|
|
||||||
includes.addAll(Set.of(includeArr));
|
|
||||||
}
|
|
||||||
Include[] excludeArr = logAnnotation.excludes();
|
|
||||||
if (excludeArr.length > 0) {
|
|
||||||
includes.removeAll(Set.of(excludeArr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录描述
|
|
||||||
*
|
|
||||||
* @param logRecord 日志信息
|
|
||||||
* @param methodLog 方法级 Log 注解
|
|
||||||
* @param handlerMethod 处理器方法
|
|
||||||
*/
|
|
||||||
private void logDescription(LogRecord logRecord, Log methodLog, HandlerMethod handlerMethod) {
|
|
||||||
// 例如:@Log("新增部门") -> 新增部门
|
|
||||||
if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.value())) {
|
|
||||||
logRecord.setDescription(methodLog.value());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 例如:@Operation(summary="新增部门") -> 新增部门
|
|
||||||
Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
|
|
||||||
if (null != methodOperation) {
|
|
||||||
logRecord.setDescription(CharSequenceUtil.blankToDefault(methodOperation.summary(), "请在该接口方法上指定日志描述"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录模块
|
|
||||||
*
|
|
||||||
* @param logRecord 日志信息
|
|
||||||
* @param methodLog 方法级 Log 注解
|
|
||||||
* @param classLog 类级 Log 注解
|
|
||||||
* @param handlerMethod 处理器方法
|
|
||||||
*/
|
|
||||||
private void logModule(LogRecord logRecord, Log methodLog, Log classLog, HandlerMethod handlerMethod) {
|
|
||||||
// 例如:@Log(module = "部门管理") -> 部门管理
|
|
||||||
if (null != methodLog && CharSequenceUtil.isNotBlank(methodLog.module())) {
|
|
||||||
logRecord.setModule(methodLog.module());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (null != classLog && CharSequenceUtil.isNotBlank(classLog.module())) {
|
|
||||||
logRecord.setModule(classLog.module());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 例如:@Tag(name = "部门管理") -> 部门管理
|
|
||||||
Tag classTag = handlerMethod.getBeanType().getDeclaredAnnotation(Tag.class);
|
|
||||||
if (null != classTag) {
|
|
||||||
String name = classTag.name();
|
|
||||||
logRecord.setModule(CharSequenceUtil.blankToDefault(name, "请在该接口类上指定所属模块"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否要记录日志
|
* 是否要记录日志
|
||||||
*
|
*
|
Reference in New Issue
Block a user