记录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、日志配置

logback-spring.xml

<!-- 行为埋点 -->
    <appender name="EVENT_LOG" class="ch.qos.logback.core.rolling.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.qos.logback.core.rolling.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 = LoggerFactory.getLogger("event_log");

	/**
	 * 获取请求的真实IP
	 * @param request
	 * @return
	 */
	public static String getRealIP(HttpServletRequest request) {
		String ip = request.getHeader("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 = request.getHeader("X-Real-IP");
		if (!StringUtils.isEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)) {
			return ip;
		}
		return request.getRemoteAddr();
	}

	@Pointcut("execution(* cn.ice.demo.modules.*.web.*Controller.*(..))")
	public void doEventLog() {
	}

	@Around("doEventLog()")
	public Object doAround(ProceedingJoinPoint point) throws Throwable {
		ObjectMapper mapper = new ObjectMapper();
		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		if (attributes == null) {
			Object result = point.proceed();
			return result;
		}
		HttpServletRequest request = attributes.getRequest();
		HttpServletResponse response = attributes.getResponse();
		HttpSession session = request.getSession(false);
		//请求参数
		StringBuffer requestParams = new StringBuffer();
		if ("POST".equals(request.getMethod())) {
			String params = getRequestBody(request);
			requestParams.append(params);
		} else {
			String queryString = request.getQueryString();
			requestParams.append(queryString);
		}

		LocalDateTime start = LocalDateTime.now();
		Object result = point.proceed();
		int status = response.getStatus();
		//响应结果,如果没有统一返回格式,此处需要改写
		String resp = getResponseBody(response);
		ResultVO resultVO = new ResultVO();
		resultVO = mapper.readValue(resp, ResultVO.class);
		
		EventLog eventLog = new EventLog();
		LocalDateTime end = LocalDateTime.now();
		Duration duration = Duration.between(start, end);
		Long useTime = duration.toMillis();
		String userId = (String) session.getAttribute("userId");
		String email = (String) session.getAttribute("email");
		String ip = getRealIP(request);
		String header = request.getHeader("User-Agent");
		UserAgent ua = UserAgentUtil.parse(header);

		eventLog.setUserId(userId);
		eventLog.setEmail(email);
		eventLog.setParams(requestParams.toString());
		eventLog.setUrl(request.getRequestURI());
		eventLog.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.setCode(resultVO.getCode());
		eventLog.setData(resultVO.getDate());
		eventLog.setMsg(resultVO.getMsg());
		eventLog.setCreateTime(start);

		log.info(mapper.writeValueAsString(eventLog));

		return result;
	}

	private String getRequestBody(HttpServletRequest request) {
		String requestBody = "";
		ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
		if (wrapper != null) {
			try {
				requestBody = IOUtils.toString(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding());
			} 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 = IOUtils.toString(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding());
			} catch (IOException e) {
				// NOOP
			}
		}
		return responseBody;
	}
}
四、总结

实际需求拆解出来的思考过程和关键代码实现。上述代码未考虑是用户否登录情况,即用户信息日志,加上session值的判断更加完整。

Filter实现地址

参考链接:https://www.sofastack.tech/projects/

SOFATracer 是蚂蚁金服开发的基于 OpenTracing 规范 的分布式链路跟踪系统,其核心理念就是通过一个全局的 TraceId 将分布在各个服务节点上的同一次请求串联起来。通过统一的 TraceId 将调用链路中的各种网络调用情况以日志的方式记录下来同时也提供远程汇报到 Zipkin 进行展示的能力,以此达到透视化网络调用的目的。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值