最近在研究ELK,想通过ELK来统一管理日志,并简单分析系统的一些功能,比如:机构下的交易量,交易成功/失败的比例,单位时间内某种交易的笔数,访问系统前50IP……,但是苦于无法建立统一的分析标准,无法实施,想法是把一些业务参数打印到日志中,进行分析统计。
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是 Web 应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。
一种解决的办法是采用自定义的日志格式,把用户的信息采用某种方式编码在日志记录中。这种方式的问题在于要求在每个使用日志记录器的类中,都可以访问到用户相关的信息。这样才可能在记录日志时使用。这样的条件通常是比较难以满足的。MDC 的作用是解决这个问题。
MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
过滤器(Filter)
遇到问题:如何在过滤器中读取controller的返回值(因为直接Response没有提供直接拿到返回值的方法。所以要通过代理来取得返回值)
@WebFilter(urlPatterns = "/*")
@Order(1)
public class MyFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(MyFilter.class);
private static ObjectMapper mapper = new ObjectMapper();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@SuppressWarnings("unchecked")
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String body = IOUtils.toString(request.getInputStream());
Map reqMap = mapper.readValue(body, Map.class);
// 可以在此处解析请求报文,然后将业务参数放入日志打印
MDC.put("service_name", "my_service1");
MDC.put("trcode", "123456");
MDC.put("chid", "999999");
ResponseWrapper wrapperResponse = new ResponseWrapper((HttpServletResponse) response);// 转换成代理类
ParameterRequestWrapper wrapRequest = new ParameterRequestWrapper(req, reqMap);
chain.doFilter(wrapRequest, wrapperResponse);
byte[] content = wrapperResponse.getContent();// 获取返回值
String str = new String(content, "UTF-8");
Map rspMap = JSONUtil.jsonToObj(str, Map.class);
Map rspHeadMap = (Map) rspMap.get("rspHead");
MDC.put("rspcode", rspHeadMap.get("rspcode"));
MDC.clear();// 清除数据
}
/*
* (non-Javadoc)
*
* @see javax.servlet.Filter#destroy()
*/
@Override
public void destroy() {
}
}
/**
* 返回值输出代理类
*/
public class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream buffer;
private ServletOutputStream out;
public ResponseWrapper(HttpServletResponse httpServletResponse) {
super(httpServletResponse);
buffer = new ByteArrayOutputStream();
out = new WrapperOutputStream(buffer);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return out;
}
@Override
public void flushBuffer() throws IOException {
if (out != null) {
out.flush();
}
}
public byte[] getContent() throws IOException {
flushBuffer();
return buffer.toByteArray();
}
class WrapperOutputStream extends ServletOutputStream {
private ByteArrayOutputStream bos;
public WrapperOutputStream(ByteArrayOutputStream bos) {
this.bos = bos;
}
@Override
public void write(int b) throws IOException {
bos.write(b);
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener arg0) {
}
}
}
/**
* 拦截器
*
* @author Fan.W
* @since 1.8
*/
public class MDCInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(MDCInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
MDC.put("user_name", "fan wei");
MDC.put("user_id", "123456");
return true;
}
@Override
public void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
}
/**
*这个方法的主要作用是用于清理资源的,当然这个方法也只能在当前这个Interceptor的preHandle方法的返回值为true时才会执行。
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
MDC.clear();// 清除
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"
default-lazy-init="false">
<mvc:interceptors>
<bean class="com.seeker.interceptor.MDCInterceptor"/>
mvc:interceptors>
beans>
异常通知要先于controller中ExceptionHandler捕获异常!!!!!
@Aspect
@Component
public class MyInterceptor {
private static Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
/**
* 拦截类的入口--拦截所有controller类
*/
@Pointcut("execution(public * com.seeker.controller..*.*(..)) ")
public void pointCut() {
}
/**
* 方法调用之前调用
*
* @param joinPoint
*/
@Before(value = "pointCut()")
public void doBefore(JoinPoint joinPoint) {
}
/**
* 环绕通知
*
* @param pjp
* @return
* @throws Throwable
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Around("pointCut()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
String className = pjp.getTarget().getClass().getName(); // 拦截类
String methodName = pjp.getSignature().getName() + "()";
Object[] args = pjp.getArgs();// 获取请求参数,可以校验属性
// 解析请求报文,使用MDC,打印日志,也可在前置通知中获取
MDC.put("service_name", "my_service1");
MDC.put("trcode", "123456");
MDC.put("chid", "999999");
// 此处返回的是拦截的方法的返回值,如果不执行此方法,则不会执行被拦截的方法,利用环绕通知可以很好的做权限管理
Object obj = pjp.proceed();
MDC.put("rspcode", "0000");
return obj;
}
/**
* 异常通知:pjp.proceed();跑出异常即捕获,先于@ExceptionHandler中捕获到
*
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "pointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
if (e instanceof BizException) {
MDC.put("rspcode", ((BizException) e).messageCode);
} else {
MDC.put("rspcode", "9999");
}
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:aspectj-autoproxy expose-proxy="true">aop:aspectj-autoproxy>
beans>
"ch.qos.logback.classic.encoder.PatternLayoutEncoder">
[%X{service_name}] %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{chid}] [%X{trcode}] [%X{rspcode}] - %m%n</pattern>
>