记录Http请求日志(埋点)-过滤器方式

一、需求
1、需求概述

内部管理系统,用于统计用户的使用情况,使用习惯。

2、分析

由于是内部系统,用商业级埋点有点浪费。可以借助ELK日志分析系统,为HTTP API接口增加统一请求日志。

3、统一请求日志要记录以下信息:
  • 请求信息:请求路径、请求参数、请求时间、响应状态
  • 用户信息:用户id、操作系统、浏览器版本
  • 应用信息:接口耗时、响应结果(API统一格式的返回结果)
二、选用AOP or Filter

1、AOP拦截所有方法,可以拦截指定Controller;

2、Filter拦截所有请求,拦截位置比Handler靠前,比较适合用于行为日志记录

以终为始,不管黑猫白猫,能抓住老鼠就是好猫。这两种方式均能满足需求。本章选用Filter方式实现。

3、选用个Filter

  • OncePerRequestFilter(SpringMVC)

抽象类OncePerRequestFilter继承自GenericFilterBean,它保留了GenericFilterBean中的所有方法并对之进行了扩展,在oncePerRequestFilter中的主要方法是doFilter。

  • AbstractRequestLoggingFilter(SpringMVC)

AbstractRequestLoggingFilter是对OncePerRequestFilter的扩展,它除了遗传了其父类及祖先类的所有功能外,还在doFilterInternal中决定了在过滤之前和之后执行的事件,它的子类关注的是beforeRequest和afterRequest。

因为埋点需求需要记录response信息,选用基于OncePerRequestFilter实现,并参考HttpTraceLogFilter一些思路和写法

三、实现代码

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

<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.3.2</version>
		</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、过滤器(Filter)

/**
 *	埋点Filter
 * @author Ice sun
 * @date 2020/11/2 19:27
 */
public class HttpEventLogFilter extends OncePerRequestFilter {

	private final Logger log = LoggerFactory.getLogger("event_log");
	/**
	 * 预留日志总开关,通过配置开启或关闭
	 */
	private boolean logEnabed = true;
	/**
	 * 排除上传文件类请求
	 */
	private static final String IGNORE_CONTENT_TYPE = "multipart/form-data";

	@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);
		}

		int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
		LocalDateTime start = LocalDateTime.now();
		try {
			filterChain.doFilter(request, response);
			status = response.getStatus();
		}catch (Exception e){
			log.error("TraceLogFilter",e);
		} finally {
			if(logEnabed&&!Objects.equals(IGNORE_CONTENT_TYPE, request.getContentType())) {
				ObjectMapper mapper = new ObjectMapper();

				LocalDateTime end = LocalDateTime.now();
				Duration duration = Duration.between(start, end);
				Long useTime = duration.toMillis();
				HttpSession session = request.getSession(false);

				EventLog eventLog = new EventLog();
				String userId = (String) session.getAttribute("userId");
				String email = (String) session.getAttribute("email");
				//请求参数
				StringBuffer requestParams = new StringBuffer();
				if("POST".equals(request.getMethod())){
					String params = getRequestBody(request);
					requestParams.append(params);
				}else {
					String queryString = request.getQueryString();
					requestParams.append(queryString);
				}
				//响应结果,如果没有统一返回格式,此处需要改写
				String result = getResponseBody(response);
				ResultVO resultVO = new ResultVO();//API统一响应封装类
				resultVO = mapper.readValue(result,ResultVO.class);

				String ip = getRealIP(request);
				String header = request.getHeader("User-Agent");
				//hutools工具类
				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));
				
			}
			//非常关键,一定要加
			updateResponse(response);
		}
	}

	/**
	 * 获取请求的真实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();
	}

	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;
	}
	/**
	 * 解决字节流在读取了一次之后清空的问题
	 */
    private void updateResponse(HttpServletResponse response) throws IOException {
		ContentCachingResponseWrapper responseWrapper = WebUtils
				.getNativeResponse(response, ContentCachingResponseWrapper.class);
		Objects.requireNonNull(responseWrapper).copyBodyToResponse();
	}
}

4、SpringBoot,Application类注册Filter

   /**
	 * 记录http请求日志
	 * @return
	 */
	@Bean
	public FilterRegistrationBean httpEventFilter() {
		FilterRegistrationBean registration = new FilterRegistrationBean(new HttpEventLogFilter());
		registration.addUrlPatterns("/api/*");
		return registration;
	}
四、总结

实际需求拆解出来的思考过程和关键代码实现。上述代码未考虑是用户否登录情况,即用户信息日志,加上session值的判断更加完整。
AOP实现地址:https://blog.csdn.net/qq_16465949/article/details/109490660

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

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

参考文章:https://blog.csdn.net/weixin_33941350/article/details/88033066

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
可以通过在过滤器中获取请求参数和响应结果并打印日志方式来实现。 在过滤器中,可以通过HttpServletRequest对象获取请求参数,例如: ```java String queryString = request.getQueryString(); // 获取请求参数 ``` 而获取响应结果则需要在HttpServletResponse对象的输出流中进行捕获,例如: ```java // 获取响应输出流 PrintWriter writer = response.getWriter(); // 创建一个新的输出流 PrintWriter newWriter = new PrintWriter(new OutputStreamWriter(writer, "UTF-8"), true); // 将新输出流设置给HttpServletResponse对象 response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(result); // result为响应结果 // 捕获输出流中的内容 String responseResult = new String(writer.toString().getBytes("UTF-8")); ``` 在获取到请求参数和响应结果后,可以通过日志框架(如log4j、slf4j等)将其打印出来,例如: ```java import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class RequestLoggingFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String queryString = httpRequest.getQueryString(); // 获取请求参数 logger.info("Request URL: {}?{}", httpRequest.getRequestURL(), queryString); HttpServletResponse httpResponse = (HttpServletResponse) response; // 获取响应输出流 PrintWriter writer = httpResponse.getWriter(); // 创建一个新的输出流 PrintWriter newWriter = new PrintWriter(new OutputStreamWriter(writer, "UTF-8"), true); // 将新输出流设置给HttpServletResponse对象 httpResponse.setCharacterEncoding("UTF-8"); httpResponse.setContentType("application/json;charset=UTF-8"); // 捕获输出流中的内容 String responseResult = new String(writer.toString().getBytes("UTF-8")); logger.info("Response: {}", responseResult); // 继续执行过滤器链 chain.doFilter(request, response); } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } } ``` 以上就是在过滤器中实现请求参数和响应结果日志打印的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值