切面收集日志
切面收集日志
- 1. 使用切面在方法执行完毕后收集日志
- 1.1 注解
- 1.2 切面
- 1.3 使用
- 1.4 YML中对该注解的操作开关
1. 使用切面在方法执行完毕后收集日志
1.1 注解
import cn.ac.ict.knowledge.graph.enums.annotation.OptTypeEnum;
import java.lang.annotation.*;
/**
* 日志收集注解
*
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperate {
/**
* 操作类型,默认为其他类型(OptTypeEnum.OTHER)
* 用于记录方法执行的操作属于哪种预定义类型
* 这有助于在日志或审计过程中快速识别操作的类别
*/
OptTypeEnum optTypeEnum() default OptTypeEnum.OTHER;
/**
* 操作详细描述,默认为空字符串
* 提供方法执行的具体操作描述,用于详细记录操作内容
* 在日志记录时,这将帮助开发者更好地理解操作的细节
*/
String optDetail() default "";
===========================================================
import lombok.Getter;
/**
* 日志收集操作枚举
*/
@Getter
public enum OptTypeEnum {
LOGIN,
LOGOUT,
QUERY,
VIEW,
EDIT,
DELETE,
IMPORT,
EXPORT,
ADD,
DISABLE,
ENABLE,
OTHER
}
1.2 切面
这个切面是先通过方法调用之后,通过方法是正常执行完成,还是跑出了异常,来进行不同的方式处理。最后在通过POST请求把封装好VO信息发送给远端接口,来保存本次方法调用的请求参数以及想要的参数。
import cn.ac.ict.knowledge.graph.common.auth.LoginUser;
import cn.ac.ict.knowledge.graph.enums.annotation.OptTypeEnum;
import cn.ac.ict.knowledge.graph.framework.common.pojo.CommonResult;
import cn.ac.ict.knowledge.graph.utils.TimeUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import ua_parser.Client;
import ua_parser.Parser;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* 日志操作切面:拦截 @LogOperate 注解的方法,并将操作日志发送到日志采集服务器
* <p>
* 支持 application-dev.yml 配置:
* log-operate.enabled=true 开启日志切面
* log-operate.enabled=false 关闭日志切面
*
*/
@Slf4j
@Aspect
@Component
public class LogOperateAspect {
@Resource
private LoginUser loginUser;
/**
* 应用id
*/
@Value("${xxx000.xxx:1}")
private String appId;
/**
* 是否启用日志切面(通过 application.yml 配置)
*/
@Value("${log-operate.enabled:true}")
private boolean logOperateEnabled;
/**
* 日志采集服务器地址
*/
@Value("${xxx-xxx.host}")
private String collectLogsHost;
/**
* 日志采集 API 路径
*/
@Value("${xxx-xxx.collect}")
private String collectReqPath;
/**
* 切点:拦截所有标注了 @LogOperate 的方法
*/
@Pointcut("@annotation(xxx.xxx.common.annotation.log.LogOperate)")
public void logOperatePointcut() {
}
/**
* 方法执行完毕后,记录操作日志
*
* @param joinPoint 连接点
* @param result 方法返回值
*/
@AfterReturning(pointcut = "logOperatePointcut()", returning = "result")
public void logOperation(JoinPoint joinPoint, Object result) {
if (!logOperateEnabled) {
return;
}
// 获取被拦截方法的签名信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法上标注的LogOperate注解
LogOperate annotation = signature.getMethod().getAnnotation(LogOperate.class);
// 操作结果(0-成功;1-失败)
LogOperateReqVO vo = buildLogOperateReqVO(joinPoint, annotation, result, "", "0");
log.info("======>[LogOperateAspect::sendLog]logOperateReqVO: {}", JSONUtil.toJsonStr(vo));
// 发送日志到日志收集服务器
sendLog(vo);
}
/**
* 在抛出异常后执行的方法,用于记录操作日志
* 该方法使用了AspectJ的@AfterThrowing注解,以便在匹配的切点方法抛出异常后执行日志记录操作
*
* @param joinPoint 切入点对象,提供了关于目标方法的信息,如方法签名和参数
* @param exception 抛出的异常对象,用于获取异常信息
*/
@AfterThrowing(pointcut = "logOperatePointcut()", throwing = "exception")
public void logOperationException(JoinPoint joinPoint, Exception exception) {
// 如果日志记录功能未启用,则直接返回,不执行后续操作
if (!logOperateEnabled) {
return;
}
// 获取方法签名,以便后续获取方法上的注解信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法上的LogOperate注解,用于获取日志记录相关的元数据
LogOperate annotation = signature.getMethod().getAnnotation(LogOperate.class);
// 获取异常信息,如果异常消息为空,则使用空字符串
String exceptionMessage = StringUtils.isNotBlank(exception.getMessage()) ? exception.getMessage() : "";
// 构建日志对象
LogOperateReqVO vo = buildLogOperateReqVO(joinPoint, annotation, "", exceptionMessage, "1");
// 发送日志对象到日志系统
sendLog(vo);
}
/**
* 构建日志对象
*
* @param joinPoint 连接点
* @param annotation 方法上的 @LogOperate 注解
* @param result 方法返回值
* @param exception 异常信息
* @param optResult 操作结果 (0-成功, 1-失败)
* @return 日志对象
*/
private LogOperateReqVO buildLogOperateReqVO(JoinPoint joinPoint, LogOperate annotation, Object result, String exception, String optResult) {
LogOperateReqVO vo = new LogOperateReqVO();
// 应用id
vo.setAppId(appId);
// 获取浏览器信息
String browserInfo = getBrowserInfo();
vo.setBrowserInfo(browserInfo);
// 获取操作IP
String ip = getIpAddress();
vo.setIp(ip);
// 获取操作详情
String optDetail = annotation.optDetail();
vo.setOptDetail(optDetail);
// 操作类型
vo.setOptType(annotation.optTypeEnum() != null ? annotation.optTypeEnum().name() : OptTypeEnum.OTHER.name());
// 操作类型说明
vo.setOptTypeAddition(annotation.optTypeEnum() != null ? annotation.optTypeEnum().name() : OptTypeEnum.OTHER.name());
// 操作结果
vo.setOptResult(optResult);
// 异常信息(仅在失败时记录)
vo.setException(exception);
// 获取请求参数
Object[] args = joinPoint.getArgs();
if (args.length > 0) {
vo.setReqParam(JSONUtil.toJsonStr(args));
} else {
vo.setReqParam("");
}
// 获取返回参数
vo.setReturnParam(result != null ? JSONUtil.toJsonStr(result) : "");
// 获取接口路径
String url = getRequestUrl();
vo.setUrl(url);
// 获取用户ID(假设用户信息从上下文或session中获取)
String userId = getUserId();
vo.setUserId(userId);
// 获取操作时间
String optTime = getCurrentTime();
vo.setOptTime(optTime);
// 设置创建时间为当前时间
String createTime = getCurrentTime();
vo.setCreateTime(createTime);
return vo;
}
/**
* 获取浏览器信息(名称 + 版本)
*
* @return 例如 "Chrome 129.0.0.0"
*/
private String getBrowserInfo() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String userAgentStr = request.getHeader("User-Agent");
if (userAgentStr == null || userAgentStr.isEmpty()) {
return "Unknown Browser";
}
Parser parser = new Parser();
Client client = parser.parse(userAgentStr);
String browserName = client.userAgent.family;
String browserVersion = client.userAgent.major != null ? client.userAgent.major : "Unknown";
return browserName + " " + browserVersion;
}
/**
* 获取操作IP
*
* @return IP 地址
*/
private String getIpAddress() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip;
}
/**
* 获取接口路径
*
* @return 请求接口路径
*/
private String getRequestUrl() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 获取协议(http 或 https)
String scheme = request.getScheme();
// 获取服务器的 IP 地址或主机名
String serverName = request.getServerName();
// 获取服务器端口
int serverPort = request.getServerPort();
// 获取请求路径(不包括协议、主机和端口)
String requestURI = request.getRequestURI();
// 拼接完整的 URL
return scheme + "://" + serverName + ":" + serverPort + requestURI;
}
/**
* 获取当前时间
*
* @return 当前时间的字符串表示
*/
private String getCurrentTime() {
return TimeUtil.getCurrentTime();
}
/**
* 获取当前用户ID
*
* @return 当前用户的ID
*/
private String getUserId() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 从请求头获取 token
String token = request.getHeader("token");
if (StringUtils.isBlank(token)) {
return "";
}
return loginUser.getLoginUserId("token");
}
/**
* 发送日志到日志收集服务器
*
* @param vo 日志对象
*/
private void sendLog(LogOperateReqVO vo) {
if (!logOperateEnabled) {
return;
}
String requestBody = JSONUtil.toJsonStr(vo);
String requestMapping = collectLogsHost + collectReqPath;
log.info("======>[LogOperateAspect::sendLog] 日志采集请求路径:{}", requestMapping);
log.info("======>[LogOperateAspect::sendLog] 日志请求体:{}", requestBody);
try {
HttpResponse response = HttpUtil.createPost(requestMapping)
.body(requestBody)
.execute();
CommonResult<String> commonResult = JSONUtil.toBean(response.body(), CommonResult.class);
if (commonResult.isError()) {
log.error("======>[LogOperateAspect::sendLog] 请求失败,错误信息:{}", commonResult.getMsg());
}
} catch (Exception e) {
log.error("======>[LogOperateAspect::sendLog] 请求异常,异常信息:", e);
}
}
}
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 日志采集请求参数VO,详情见内部日志说明V1.0.1.docx
*
*/
@Data
public class LogOperateReqVO implements Serializable {
private static final long serialVersionUID = 1L;
private String appId;
private String browserInfo;
private String exception;
private String ip;
private String optDetail;
private String optResult;
private String optType;
private String optTypeAddition;
private String reqParam;
private String returnParam;
private String userId;
private String url;
private String optTime;
private String createTime;
}
1.3 使用
在Controller层上直接加上 @LogOperate(optTypeEnum = OptTypeEnum. x, optDetail = "xxxxxx")
1.4 YML中对该注解的操作开关
log-operate:
enabled: true # 日志采集开启与关闭