背景
在业务系统中,为了排错或者查询,需要根据业务属性去串联和查询链路日志,比如工作流移交,其中涉及表单保存,业务数据校验,工作流事件执行,环节移交等操作,需要根据业务号查询,然后串联成一个时间轴的方式来展示(类似分布式链路监控)。如果每个场景都是由硬编码去埋点和串联,带来的代价是非常大。业务系统本身也有很多模块组合而成,有基于SpringCloud架构的,有基于SpringMVC架构的,一笔业务操作,可能横跨多个不同架构的模块,所以需要一套自动化提取业务链路日志的手段。
因为有基于SpringCloud的的分布式框架,所以尝试通过接入了Zipkin中间件来实现分布式链路日志,并且结合业务场景,来解决业务链路日志的问题。
解决思路
通过自定义filter过滤器,在请求开始阶段,就产生guid或者获取上一个环节的guid(这个guid后续可以替换成自定义的业务属性,用于前端界面查询)
- 如果当前服务是头一个服务,就自己产生一个guid,并写到requestHeader里面(一般操作都是由前端发起,前端在请求后台之前,在requestHeader中写上guid或业务属性字段,以及相关操作内容)
- 如果不是,就从requestHeader里面获取guid
- 把自定义输入写入到链路trace的span里面
- 通过默认接口,或者自定义es查询模型,去按照guid获取数据
SpringBoot场景下
添加过滤器ZipkinFilter
核心就是ZipkinFilter,可以添加到网关里,用于解决RequestHeader继承的问题。
其中xguid是测试的,全局唯一guid写入到链路日志中的
public class ZipkinFilter implements Filter {
Logger logger = LoggerFactory.getLogger(ZipkinFilter.class);
@Override
public void destroy() {
logger.info("----Filter销毁----");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
// 对request、response进行一些预处理
HttpServletRequest servletRequest = (HttpServletRequest) request;
ZipkinFilterWrapper zipkinFilterWrapper = new ZipkinFilterWrapper(servletRequest);
Tracer tracer = SpringBeanUtil.getBean("tracer");
String uuid = servletRequest.getHeader("xguid");
logger.info("当前xguid是 {}", uuid);
//如果没有xguid传过来,就说明这个是第一个,为了保持完整性,第一个就手动产生xguid
if(StringUtils.isEmpty(uuid)){
uuid = UUID.randomUUID().toString();
zipkinFilterWrapper.putHeader("xguid",uuid);
}
//重点是这句代码
tracer.currentSpan().tag("xguid", uuid);
// 执行目标资源,放行
filterChain.doFilter(request, response);
logger.info("----调用service之后执行一段代码----");
}
@Override
public void init(FilterConfig arg0) throws ServletException {
logger.info("----Filter初始化----");
}
}
SpringBeanUtil
/**
* SpringBean获取, 主要用来获取 trace的Bean,用于自定义添加链路标签
*/
@Component
public class SpringBeanUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
private static void setStaticApplicationContext(ApplicationContext applicationContext){
SpringBeanUtil.applicationContext = applicationContext;
}
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException
{
setStaticApplicationContext(applicationContext);
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException {
return (T)applicationContext.getBean(name);
}
public static <T> T getBean(Class<T> name) throws BeansException {
return applicationContext.getBean(name);
}
}
****
ZipkinFilterWrapper
这个可以不用,后台写入requestHeader比较麻烦,所以加了这个
/**
* 自定义方法,添加requestHeader
*/
public class ZipkinFilterWrapper extends HttpServletRequestWrapper {
private final Map<String, String> customHeaders;
public ZipkinFilterWrapper(HttpServletRequest request){
super(request);
this.customHeaders = new HashMap<>(10);
}
public void putHeader(String name, String value){
this.customHeaders.put(name, value);
}
@Override
public String getHeader(String name) {
String headerValue = customHeaders.get(name);
if (headerValue != null){
return headerValue;
}
return ((HttpServletRequest) getRequest()).getHeader(name);
}
@Override
public Enumeration<String> getHeaderNames() {
Set<String> set = new HashSet<>(customHeaders.keySet());
Enumeration<String> e = ((HttpServletRequest) getRequest()).getHeaderNames();
while (e.hasMoreElements()) {
String n = e.nextElement();
set.add(n);
}
return Collections.enumeration(set);
}
}
配置文件
spring:
sleuth:
web:
client:
enabled: true
sampler:
probability: 1.0
propagation:
tag:
whitelisted-keys: #链路额外传播字段
- xguid
propagation-keys:
- xguid
zipkin:
base-url: http://localhost:9411
引用
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
SpringMVC场景下
增加BraveConfig
import brave.Tracing;
import brave.http.HttpTracing;
import brave.propagation.B3Propagation;
import brave.propagation.ExtraFieldPropagation;
import brave.servlet.TracingFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import zipkin2.Span;
import zipkin2.reporter.AsyncReporter;
import zipkin2.reporter.Sender;
import zipkin2.reporter.okhttp3.OkHttpSender;
import javax.servlet.Filter;
@Configuration
public class BraveConfig {
@Bean
public HttpTracing httpTracing(Tracing tracing){
return HttpTracing.create(tracing);
}
@Bean
public Tracing tracing(){
return Tracing.newBuilder().localServiceName("PlatformDesigner").spanReporter(spanReporter())
//配置propagation工厂类
.propagationFactory(ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "SIGNATURE")).build();
}
@Bean
Filter tracingFilter(HttpTracing httpTracing) {
return TracingFilter.create(httpTracing);
}
@Bean
public AsyncReporter<Span> spanReporter() {
zipkin2.reporter.AsyncReporter.Builder reporterBuilder = AsyncReporter.builder(sender());
return reporterBuilder.build();
}
@Bean
public Sender sender() {
String zipkinUrl = Global.getConfig("zipkin.url");
return OkHttpSender.newBuilder()
.endpoint(zipkinUrl).build();
}
}
增加ZipkinFilter
import brave.Tracer;
import brave.Tracing;
import xxxxx.SpringBeanUtil;
import xxxxx.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Zipkin过滤器
*/
@WebFilter("/*")
public class ZipkinFilter implements Filter {
Logger logger = LoggerFactory.getLogger(ZipkinFilter.class);
@Override
public void destroy() {
logger.info("----Filter销毁----");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
// 对request、response进行一些预处理
HttpServletRequest servletRequest = (HttpServletRequest) request;
System.out.println(servletRequest.getRequestURI());
Tracing tracing = SpringBeanUtil.getBean("tracing");
String uuid = servletRequest.getHeader("xguid");
logger.info("当前xguid是 {}", uuid);
//如果没有xguid传过来,就说明这个是第一个,为了保持完整性,第一个就手动产生xguid
if(!StringUtils.isEmpty(uuid)){
tracing.tracer().currentSpan().tag("xguid",uuid);
//tracing.currentTraceContext()("xguid", uuid);
}
// 执行目标资源,放行
filterChain.doFilter(request, response);
logger.info("----调用service之后执行一段代码----");
}
@Override
public void init(FilterConfig arg0) throws ServletException {
logger.info("----Filter初始化----");
}
}
添加webxml
<filter>
<filter-name>tracingFilter</filter-name>
<filter-class>brave.spring.webmvc.DelegatingTracingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>tracingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
config.properties
增加配置属性
zipkin.enable=false
zipkin.url=http://localhost:9411/api/v2/spans
涉及的jar包
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-spring-beans</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-context-log4j12</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-context-slf4j</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-spring-web</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-httpclient</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-spring-webmvc</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-sender-okhttp3</artifactId>
<version>2.7.6</version>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-sender-amqp-client</artifactId>
<version>2.7.6</version>
</dependency>
HttpClient场景
4.5版本以后的httpclient-core默认支持zipkin链路日志传递, 其他诸如Fegin调用等都是同一个道理
目前代码中会碰到内部用httpclient调用第三方服务,这个可以通过两种方式解决。
一个是采用TracingHttpClientBuilder
public static String executeRequest(HttpUriRequest request,
String username, String password) {
CloseableHttpClient client = null;
CloseableHttpResponse response = null;
try {
Tracing tracing = null;
tracing = SpringBeanUtil.getBean("tracing");
LinkedList<CloseableHttpResponse> httpResponses = new LinkedList<>();
CredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(
username, password);
provider.setCredentials(AuthScope.ANY, credentials);
if (tracing != null) {
client = TracingHttpClientBuilder.create(tracing).setDefaultCredentialsProvider(provider).build();
}else {
client = HttpClientBuilder.create()
.setDefaultCredentialsProvider(provider).build();
}
response = client.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
httpResponses.add(response);
InputStream inputStream = null;
String output = "";
HttpEntity entity = response.getEntity();
if (entity != null && entity.getContent() != null) {
inputStream = response.getEntity().getContent();
output = IOUtils.toString(inputStream, "UTF-8");
inputStream.close();
}
return output;
} catch (Exception e) {
e.printStackTrace();
throw new AssassinException(e.getMessage());
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (client != null) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
一种是自己埋点
public static String get(String serviceUrl, String username,
String password) {
HttpURLConnection connect = null;
try {
// //获取tracer对象,位于io.zipkin.brave 我用的是5.12.7版本
// Tracer tracer = SpringBeanUtil.getBean("tracer");
//
// //最外部链路上增加一些标签,
// tracer.currentSpan().tag("background-sleep-millis",String.valueOf(RandomUtils.nextDouble()));
//
// //创建新的链路。名称为了友好,直接用url地址替代好了
// Span span = tracer.nextSpan().name(serviceUrl).start();
//
// //增加一些查看的标签
// span.tag("http.method", "get");
//默认差不多是这些标签
// http.method GET
// http.path /testZipkin
// mvc.controller.class MainController
// mvc.controller.method testZipkin
// Client Address 192.168.100.100:4101
// 记录启动状态
// span.annotate("ServerStart");
// 实例一个URL资源
URL url = new URL(serviceUrl);
// 实例一个HTTP CONNECT
connect = (HttpURLConnection) url.openConnection();
connect.setRequestMethod("GET");
byte[] bytes = Base64.encodeBase64(
(username + ":" + password).getBytes(StandardCharsets.UTF_8)
);
String encoded = new String(
bytes,StandardCharsets.UTF_8
);
connect.setRequestProperty("Authorization", ConstValue.TOKEN_START + encoded);
connect.connect();
int code = connect.getResponseCode();
// 将返回的值存入到String中
StringBuilder sb;
try (BufferedReader brd = new BufferedReader(new InputStreamReader(
connect.getInputStream(), StandardCharsets.UTF_8))) {
sb = new StringBuilder();
String line;
while ((line = brd.readLine()) != null) {
sb.append(line);
}
}
// //记录结束状态
// span.annotate("ServerEnd");
// //这个链路结束
// span.finish();
return code +sb.toString();
}
catch (RuntimeException o){
throw o;
}
catch (Exception e) {
return "999";
} finally {
if (connect != null) {
connect.disconnect();
}
}
}
数据查询
直接用zip的接口处理数据
接口访问地址:
http://xxxx:9411/zipkin/api/v2//traces?annotationQuery=xguid%3D4aaada24-15b6-418c-b44e-c16c2f89949b
其中 annotationQuery节点用于查询自定义的tag属性,比如xguid
语法格式是 xguid=xxx and http.methd=get 中间用and隔开 翻阅源代码以后,要求and前后有且只有一个空格
其他语法参阅 zipkin.io/zipkin-api/…
可以按照自己的需要去查询数据,然后拼接数据
基于es数据查询
一些插件查看es数据,比如Elastic Head,或者Dbeaver等
ES JAVA代码查询器(关键代码)
public EsTableDataEntity searchAll(EsIndexEntity esIndexEntity, List<EsTableColumnEntity> esTableColumnEntityList, Integer currentIndex,Integer pageSize) throws IOException {
RestHighLevelClient restHighLevelClient = EsConstValue.REST_CLIENT_MAP.get(esIndexEntity.getConnectUuid());
SearchRequest searchRequest = new SearchRequest(esIndexEntity.getIndexName());
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
searchSourceBuilder.from(currentIndex * pageSize);
searchSourceBuilder.size(pageSize);
FieldSortBuilder fieldSortBuilder = new FieldSortBuilder("id");
fieldSortBuilder.order(SortOrder.DESC);
//创建查询条件
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
for(EsTableColumnEntity esTableColumnEntity: esTableColumnEntityList){
if(esTableColumnEntity.getValue()!=null && !StringUtils.isEmpty(esTableColumnEntity.getValue().toString())){
if(StringUtils.equals(
esTableColumnEntity.getType(),
EsConstValue.DATE_TYPE_NAME)){
String[] values = esTableColumnEntity.getValue().toString().split("-");
String valueFrom = values[0];
String valueTo =values[1];
RangeQueryBuilder rangeQueryBuilder = new RangeQueryBuilder(esTableColumnEntity.getDataIndex());
rangeQueryBuilder.from(valueFrom);
rangeQueryBuilder.to(valueTo);
boolQueryBuilder.must(rangeQueryBuilder);
}
else if(StringUtils.equals(esTableColumnEntity.getType(),EsConstValue.OBJECT_TYPE_NAME)){
//如果是嵌套的Object类型,则需要嵌套方式查询
Map<String,String> parseAnnotationQuery = parseAnnotationQuery(esTableColumnEntity.getValue().toString());
for (Map.Entry<String, String> kv : parseAnnotationQuery.entrySet()) {
if (kv.getValue().isEmpty()) {
MatchPhraseQueryBuilder termsQueryBuilder = new MatchPhraseQueryBuilder("_q", kv.getKey());
boolQueryBuilder.must(termsQueryBuilder);
} else {
MatchPhraseQueryBuilder termsQueryBuilder = new MatchPhraseQueryBuilder("_q", kv.getKey() + "=" + kv.getValue());
boolQueryBuilder.must(termsQueryBuilder);
}
}
}
else{
MatchPhraseQueryBuilder termsQueryBuilder = new MatchPhraseQueryBuilder(esTableColumnEntity.getDataIndex(),
esTableColumnEntity.getValue());
boolQueryBuilder.must(termsQueryBuilder);
}
}
}
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
return getEsTableDataEntity(esIndexEntity,restHighLevelClient,searchResponse);
}