看了看手上的项目日志参差不齐,有点难以忍受日志的打印情况,于是手撸两个
Filter,让我在排查问题快速下手!!!
基于Dubbo的SPI拓展的日志Filter
关键代码
public class DubboLogFilter implements Filter {
private final Logger logger = LoggerFactory.getLogger(getClass());
private ThreadLocal<DubboLog> log = new ThreadLocal<DubboLog>();
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
log.set(new DubboLog());
log.get().setInterfaceName(invocation.getInvoker().getInterface().getName());
log.get().setMethodName(invocation.getMethodName());
log.get().setArgs(invocation.getArguments());
log.get().setClient(RpcContext.getContext().getRemoteAddressString());
log.get().setHost(RpcContext.getContext().getLocalAddressString());
log.get().setRequestUrl(invoker.getUrl().getAbsolutePath());
long startTime = System.currentTimeMillis();
Result result = invoker.invoke(invocation);
long elapsed = System.currentTimeMillis() - startTime;
if (result.hasException() && invoker.getInterface() != GenericService.class) {
log.get().setThrowableStr(ExceptionUtils.getStackTrace(result.getException()));
logger.error(JSONObject.toJSONString(log.get()));
} else {
log.get().setResult(new Object[]{result.getValue()});
log.get().setSpendTime(elapsed);
logger.info(JSONObject.toJSONString(log.get()));
}
return result;
} finally {
log.remove();
}
}
}
使用阶段
上述代码只是定义了一个
Filter,我们还需要在我们的项目中去使用它。
-
定义好这个
filter之后,我们需要在对应filter所在的工程里面,再定义一个资源文件com.alibaba.dubbo.rpc.Filter。 写法:filter别 = filter全路径。 -
以上东西都准备好了之后,我们就可以使用。
- 当选择提供方打印日志,我们定义对外提供的
service中加入该filter。(例:@Service(version = "1.0.0", filter = "dubboLogFilter")),这样每次有消费者来调用都会在服务方打印日志 - 当选择消费方打印日志,我们定义该引用所需要加载的
filter。(例:referenceBean.setFilter("dubboLogFilter"))
- 这样我们就可以在对应的日志文件中看到打印结果集。
[:20881-thread-2] com.montos.filter.DubboLogFilter : {"args":[4],"client":"192.168.124.34:53302","host":"192.168.124.34:20881","interfaceName":"com.montos.interfaces.UserInterface","methodName":"getUser","requestUrl":"/com.montos.interfaces.UserInterface","result":[{"address":"4","age":4,"id":4,"name":"4"}],"spendTime":1}
拓展部分
目前自定义
filter只是基于日志的请求参数以及相应返回值的打印。后期还可以进行这方面的拓展,主要有下面这个入口:
RpcContext 是一个 ThreadLocal 的临时状态记录器,当接收到 RPC 请求,或发起 RPC 请求时,RpcContext 的状态都会变化。比如:A 调 B,B 再调 C,则 B 机器上,在 B 调 C 之前,RpcContext 记录的是 A 调 B 的信息,在 B 调 C 之后,RpcContext 记录的是 B 调 C 的信息。后期可以考虑实现一个日志追踪上报的Filter。
基于HTTP的SPI拓展的日志Filter
关键代码
public class HttpRequestFilter implements Filter {
private final Logger logger = LoggerFactory.getLogger(getClass());
public ThreadLocal<HttpLog> log = new ThreadLocal<HttpLog>();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
log.set(new HttpLog());
RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);
ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) servletResponse);
Thread thread = Thread.currentThread();
Map<String, String> map = new HashMap<String, String>();
@SuppressWarnings("rawtypes")
Enumeration headerNames = requestWrapper.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = (String) headerNames.nextElement();
String value = requestWrapper.getHeader(key);
map.put(key, value);
}
log.get().setUrl(requestWrapper.getRequestURL().toString());
log.get().setRequest(StringUtils.isNotEmpty(requestWrapper.getQueryString()) ? requestWrapper.getQueryString() : getJSON(requestWrapper));
log.get().setMethod(requestWrapper.getMethod());
log.get().setClientIp(servletRequest.getRemoteAddr());
log.get().setThread(new StringBuilder(thread.getName()).append("-").append(thread.getId()).toString());
log.get().setHeader(map);
log.get().setStart(System.currentTimeMillis());
// before
filterChain.doFilter(requestWrapper, responseWrapper);
// after
servletResponse.getOutputStream().write(responseWrapper.getContentAsBytes());//最后注意需要请reponsewrapper的内容写入到原始response
log.get().setResponse(responseWrapper.getContent());
log.get().setEnd(System.currentTimeMillis());
if (logger.isTraceEnabled()) {
logger.trace(JSONObject.toJSONString(log.get()));
}
} catch (Exception e) {
// error
log.get().setEnd(System.currentTimeMillis());
log.get().setError(e.getMessage());
if (logger.isErrorEnabled()) {
logger.error(JSONObject.toJSONString(log.get()));
}
}
}
/**
* @param request
* @return
* @throws IOException
*/
public String getJSON(HttpServletRequest request) throws IOException {
BufferedReader streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
StringBuilder responseStrBuilder = new StringBuilder();
String inputStr;
while ((inputStr = streamReader.readLine()) != null) {
responseStrBuilder.append(inputStr);
}
return responseStrBuilder.toString();
}
@Override
public void destroy() {
log.remove();
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
}
使用阶段
上面是
Filter具体的实现,里面就是多了两个类RequestWrapper以及ResponseWrapper(这里是为了缓存InputStream以及OutputStream,原因是Servlet中获取参数的方法有冲突)。
-
框架中我们将该
Filter以Bean的方式注入到容器中(或者有小伙伴直接基于注解@WebFilter以及@ServletComponentScan也是可以的)。 -
其次在
FilterRegistrationBean中注入该Filter即可。
@Bean(name = "httpRequestFilter")
public Filter httpRequestFilter() {
return new HttpRequestFilter();
}
@Bean
public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(httpRequestFilter());
registration.addUrlPatterns("/*");
registration.setName("httpRequestFilter");
registration.setOrder(1);
return registration;
}
- 日志中我们就可以看到我们刚刚的结果集
2020-08-26 19:56:35.134 [http-nio-8080-exec-9] TRACE com.montos.filter.HttpRequestFilter - {"clientIp":"***.***.***.***","end":1598442995134,"header":{"content-length":"103","referer":"referer","remoteip":"***.***.***.***","accept-language":"zh-CN,zh;q=0.9","cookie":"cookie","origin":"url","x-forwarded-for":"***.***.***.***,***.***.***.***","accept":"application/json, text/plain, */*","x-real-ip":"***.***.***.***","host":"host","connection":"close","content-type":"application/json","accept-encoding":"gzip, deflate","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36"},"method":"POST","request":"request","response":"response","start":1598442994744,"thread":"http-nio-8080-exec-9-65","url":"url"}
上面两个就是基于现有的项目进行开出来的一个日志拦截。对于现有项目可以进行直接加入,然后指定对应的日志级别再进行打印等等(没有采用
Aspect的方式,因为觉得太重了,于是直接利用框架中的SPI进行拓展)。