本文来自作者 张振华 在 GitChat 上分享 「从架构角度来看 Java 分布式日志如何收集」
首先,当我们如果作为架构师的角度去处理一件事情的时候,必须要有一些大局观。
也就是要求我们对个 Logging 的生态有完整的认识,从而来考虑分布式日志如何处理。
我们先来理解一些概念:
身边有很多同事会把这三件可能认识不太彻底,其实这是三件分别侧重点不同的事情,每件事都有各自的深度、边界和重叠部分。
而三者的边界和重叠部分需要我们在整个分布式系统中要非常清楚,而本 chat 就围绕 Logging 和 Tracing 这两件事情展开一下。
Tracing 整体负责干的事情有:
<springProfile name="staging">
<!-- configuration to be enabled when the "staging" profile is active -->
</springProfile>
<springProfile name="dev, staging">
<!-- configuration to be enabled when the "dev" or "staging" profiles are active -->
</springProfile>
<springProfile name="!production">
<!-- configuration to be enabled when the "production" profile is not active -->
</springProfile>
自定义日志实现:
当我们了解了我们需要知道的技术点之后,接下去就是针对我们公司具体业务现状进行选型,以我们公司为例,可能不止一个 Java 团队,还有 Ruby,node.js 等其它语言的开发团队。
好多其它技术选型都是基于 cncf 的,如:k8s、docker、permissions 等,所以我们就一如既往的还选择了 CNCF 的技术体系及 OpenTracing。
其实如果要去真实比较的话,差别也不是特别大,并且都做到了相互的兼容。而 Jaeger VS Zipkin server 选择了 Jaeger,因其启动简单与 Java 解耦。
Java 语言体系采用 Spring 的 Sleuth,这样我们可以省很多事情,并且也是很成熟的解决方案,而 Spring Cloud 生态也非常成熟。
1. 我们利用
import org.springframework.web.filter.CommonsRequestLoggingFilter;
来打印我们的所有的请求的日志配置如下:
//我们只需要将此类在配置文件中加载即可。里面可以设置Logging里面是否打印header 、request payload、query String 、client信息等。唯一的缺点就是没有办法打印responseBody。
@Bean
@ConditionalOnMissingBean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludeClientInfo(true);
loggingFilter.setIncludeQueryString(true);
loggingFilter.setIncludePayload(true);
loggingFilter.setIncludeHeaders(true); return loggingFilter;
}
//源码和原理其实非常简单,做个filter做logging debug即可。public class CommonsRequestLoggingFilter extends AbstractRequestLoggingFilter { @Override
protected boolean shouldLog(HttpServletRequest request) { return logger.isDebugEnabled();
} /**
* Writes a log message before the request is processed.
*/
@Override
protected void beforeRequest(HttpServletRequest request, String message) {
logger.debug(message);
} /**
* Writes a log message after the request is processed.
*/
@Override
protected void afterRequest(HttpServletRequest request, String message) {
logger.debug(message);
}
}
日志输出的格式如下:
[36667] 2018-05-19 20:22:06.185 - [notification-api,93bb291ab411e41a,93bb291ab411e41a,false] - DEBUG [http-nio-8080-exec-1] org.springframework.web.filter.CommonsRequestLoggingFilter.log - Before request [uri=/hello;client=127.0.0.1;headers={host=[127.0.0.1:8080], connection=[keep-alive], accept=[*/*], user-agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36], referer=[http://127.0.0.1:8080/swagger-ui.html], accept-encoding=[gzip, deflate, br], accept-language=[en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7], cookie=[OUTFOX_SEARCH_USER_ID_NCOO=1602949848.9012377; gsScrollPos-73=]}]
[36667] 2018-05-19 20:22:06.434 - [notification-api,93bb291ab411e41a,93bb291ab411e41a,false] - DEBUG [http-nio-8080-exec-1] org.springframework.web.filter.CommonsRequestLoggingFilter.log - After request [uri=/hello;client=127.0.0.1;headers={host=[127.0.0.1:8080], connection=[keep-alive], accept=[*/*], user-agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36], referer=[http://127.0.0.1:8080/swagger-ui.html], accept-encoding=[gzip, deflate, br], accept-language=[en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7], cookie=[OUTFOX_SEARCH_USER_ID_NCOO=1602949848.9012377; gsScrollPos-73=]}]
2. 针对没有 responseBody 的问题,我们可以自定义一个拦截器,和 CommonsRequestLoggingFilter 做差不多的事情即可。这里需要注意的是需要用到:
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
来做参数的输出和 response 的 io 的输出。但是切记很多东西不需要重复写给大家看一个关键代码:
此处我们采用的是 Docker 容器,直接将日志输出到控制台,用 logstash 直接收集 Docker 的日志给 ElasticSearch 在 kibana 显示。如下图所示:
我们只需要 search trackID 即可。
或者以 logback 为例,添加 logstash appender。关键代码如下:
<!-- Appender to log to file in a JSON format -->
<appender name="logstash" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_FILE}.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.json.%d{yyyy-MM-dd}.gz</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC</timeZone>
</timestamp>
<pattern>
<pattern>
{
"severity": "%level",
"service": "${springAppName:-}",
"trace": "%X{X-B3-TraceId:-}",
"span": "%X{X-B3-SpanId:-}",
"parent": "%X{X-B3-ParentSpanId:-}",
"exportable": "%X{X-Span-Export:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{40}",
"rest": "%message"
}
</pattern>
</pattern>
</providers>
</encoder>
</appender>
//从上下文中取到traceId,然后丢到返回的header里面
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String traceId = ThreadContext.get("traceId");
chain.doFilter(request, response);
((HttpServletResponse)response).setHeader("TraceId", traceId);
}
1. 有了上面的理论基础,就是就看看 spring cloud sleuth 怎么支持 OpenTracing 和生成 tracId 和 span,及其将 log 吐给 jaeger。