记录Http请求⽇志(埋点)-AOP⽅式
⼀、需求
1、需求概述
内部管理系统,⽤于统计⽤户的使⽤情况,使⽤习惯。
2、分析
由于是内部系统,⽤商业级埋点有点浪费。可以借助ELK⽇志分析系统,为HTTP API接⼝增加统⼀请求⽇志。
3、统⼀请求⽇志要记录以下信息:
请求信息:请求路径、请求参数、请求时间、响应状态
⽤户信息:⽤户id、操作系统、浏览器版本
应⽤信息:接⼝耗时、响应结果(API统⼀格式的返回结果)
⼆、AOP⽅式
1、AOP拦截所有⽅法,可以拦截指定Controller;
⾯向切⾯编程通常⽤在实现⽇志记录、性能统计、事务处理、异常处理等场景。通过AOP可以降低模块间的耦合度,不改变业务模块代码的情况下实现功能。
2、AOP 的核⼼概念
切⾯(Aspect) :通常是⼀个类,在⾥⾯可以定义切⼊点和通知。
连接点(Joint Point) :被拦截到的点,因为 Spring 只⽀持⽅法类型的连接点,所以在 Spring 中连接点指的就是被拦截的到的⽅法,实际上连接点还可以是字段或者构造器。
切⼊点(Pointcut) :对连接点进⾏拦截的定义。
通知(Advice) :拦截到连接点之后所要执⾏的代码,通知分为前置、后置、异常、最终、环绕通知五类。
AOP 代理 :AOP 框架创建的对象,代理就是⽬标对象的加强。Spring 中的 AOP 代理可以使 JDK 动态代理,也可以是 CGLIB 代理,前者基于接⼝,后者基于⼦类。
3、Spring AOP 相关注解
@Aspect : 将⼀个 java 类定义为切⾯类。
@Pointcut :定义⼀个切⼊点,可以是⼀个规则表达式,⽐如下例中某个 package 下的所有函数,也可以是⼀个注解等。
@Before :在切⼊点开始处切⼊内容。
@After :在切⼊点结尾处切⼊内容。
@AfterReturning :在切⼊点 return 内容之后切⼊内容(可以⽤来对处理返回值做⼀些加⼯处理)。
@Around :在切⼊点前后切⼊内容,并⾃⼰控制何时执⾏切⼊点⾃⾝的内容。
@AfterThrowing :⽤来处理当切⼊内容部分抛出异常之后的处理逻辑。
其中 @Before 、 @After 、 @AfterReturning 、 @Around 、 @AfterThrowing 都属于通知。
三、实现代码
1、⽇志配置
<!-- ⾏为埋点 -->
<appender name="EVENT_LOG"class="ch.olling.RollingFileAppender">
<File>${LOG_HOME}/event.log</File>
<append>true</append>
<!-- ThresholdFilter:临界值过滤器,过滤掉 TRACE 和 DEBUG 级别的⽇志 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<Pattern>%msg%n</Pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.olling.SizeAndTimeBasedRollingPolicy">
<!-- 每天⽣成⼀个⽇志⽂件,保存30天的⽇志⽂件
- 如果隔⼀段时间没有输出⽇志,前⾯过期的⽇志不会被删除,只有再重新打印⽇志的时候,会触发删除过期⽇志的操作。            -->
<fileNamePattern>${LOG_HOME}/event.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxFileSize>100MB</maxFileSize>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 省略其他 -->
<logger name="event_log"level="info"additivity="false">
<appender-ref ref="EVENT_LOG"/>
</logger>
pom
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、⽇志格式封装(model)
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
* ⾏为埋点
* @author Ice sun
* @date 2020/10/29 18:01
*/
@Data
public class EventLog {
/**
* ⽤户id
*/
private String userId;
/**
* ⽤户邮箱
*/
private String email;
/**
* 机构名称
*/
private String orgName;
/**
* 部门名称
*/
*/
private String deptName;
/**
* 请求路径
*/
private String url;
/
**
* 请求 IP
*/
private String ip;
/**
* 请求参数
*/
private String params;
/**
* 接⼝⽤时(毫秒)
*/
private Long useTime;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统类型
*/
private String operatingSystem;
/**
* 请求响应状态,⽰例:200 302等
*/
private Integer status;
/**
* 统⼀接⼝响应状态代码 1 成功其他见码表
*/
private String code;
/**
* 统⼀接⼝响应状态描述
*/
private String msg;
/**
* 统⼀接⼝响应状态结果
*/
private String data;
/**
* 记录时间
*/
@JsonSerialize(using = LocalDateTimeSerializer.class)
@DateTimeFormat(iso = DateTimeFormat.ISO.TIME)
@JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss", timezone ="GMT+8") private LocalDateTime createTime;
}
3、(AOP)
/**
*
* @author Ice sun
* @date 2020/11/5 19:48
*/
@Aspect
@Component
@Order(100)
public class HttpEventLogAspect {
private final Logger logger = Logger("event_log");
/**
* 获取请求的真实IP
* @param request
* @param request
* @return
*/
public static String getRealIP(HttpServletRequest request){
String ip = Header("X-Forwarded-For");
if(!StringUtils.isEmpty(ip)&&!"unKnown".equalsIgnoreCase(ip)){
//多次反向代理后会有多个ip值,第⼀个ip才是真实ip
int index = ip.indexOf(",");
if(index !=-1){
return ip.substring(0, index);
}else{
return ip;
}
}
ip = Header("X-Real-IP");
if(!StringUtils.isEmpty(ip)&&!"unKnown".equalsIgnoreCase(ip)){
return ip;
}
RemoteAddr();
}
@Pointcut("execution(* cn.dules.*.web.*Controller.*(..))")
public void doEventLog(){
}
@Around("doEventLog()")
public Object doAround(ProceedingJoinPoint point)throws Throwable {
ObjectMapper mapper =new ObjectMapper();
ServletRequestAttributes attributes =(ServletRequestAttributes) RequestAttributes(); if(attributes == null){
Object result = point.proceed();
return result;
}
HttpServletRequest request = Request();
HttpServletResponse response = Response();
HttpSession session = Session(false);
//请求参数
StringBuffer requestParams =new StringBuffer();
if("POST".Method())){
String params =getRequestBody(request);
requestParams.append(params);
}else{
String queryString = QueryString();
requestParams.append(queryString);
}
LocalDateTime start = w();
Object result = point.proceed();
int status = Status();
//响应结果,如果没有统⼀返回格式,此处需要改写
String resp =getResponseBody(response);
ResultVO resultVO =new ResultVO();
resultVO = adValue(resp, ResultVO.class);
EventLog eventLog =new EventLog();
LocalDateTime end = w();
Duration duration = Duration.between(start, end);
Long useTime = Millis();
String userId =(String) Attribute("userId");
String email =(String) Attribute("email");
String ip =getRealIP(request);
String header = Header("User-Agent");
UserAgent ua = UserAgentUtil.parse(header);
eventLog.setUserId(userId);
eventLog.setEmail(email);
eventLog.String());
eventLog.RequestURI());
springboot aopeventLog.setIp(ip);
eventLog.setBrowser(ua == null ?"": ua.getBrowser()== null ?"": ua.getBrowser().toString());
eventLog.setOperatingSystem(ua == null ?"": ua.getPlatform()== null ?"": ua.getPlatform().toString());
eventLog.setStatus(status);
eventLog.setUseTime(useTime);
eventLog.Code());
eventLog.Date());
eventLog.Msg());
eventLog.setCreateTime(start);
log.info(mapper.writeValueAsString(eventLog));
return result;
}
private String getRequestBody(HttpServletRequest request){
String requestBody ="";
ContentCachingRequestWrapper wrapper = NativeRequest(request, ContentCachingRequestWrapper.class);
if(wrapper != null){
try{
requestBody = ContentAsByteArray(), CharacterEncoding());
}catch(IOException e){
// NOOP
}
}
return requestBody;
}
private String getResponseBody(HttpServletResponse response){
String responseBody ="";
ContentCachingResponseWrapper wrapper = WebUtils
.getNativeResponse(response, ContentCachingResponseWrapper.class);
if(wrapper != null){
try{
responseBody = ContentAsByteArray(), CharacterEncoding());
}catch(IOException e){
// NOOP
}
}
return responseBody;
}
}
四、总结
实际需求拆解出来的思考过程和关键代码实现。上述代码未考虑是⽤户否登录情况,即⽤户信息⽇志,加上session值的判断更加完整。
SOFATracer 是蚂蚁⾦服开发的基于 OpenTracing 规范 的分布式链路跟踪系统,其核⼼理念就是通过⼀个全局的 TraceId 将分布在各个服务节点上的同⼀次请求串联起来。通过统⼀的 TraceId 将调⽤链路中的各种⽹络调⽤情况以⽇志的⽅式记录下来同时也提供远程汇报到 Zipkin 进⾏展⽰的能⼒,以此达到透视化⽹络调⽤的⽬的。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。