目标
学习如何:
- 实例化一个Tracer
- 创建一个简单的链路
- 给链路增加一些注解(annotation):即增加Tag和Log。
开发步骤
创建一个简单的Hello-World程序
创建一个简单的打印程序:接受一个参数,输出”Hello, {arg}!”。代码如下:
package lesson01.exercise; public class Hello { private void sayHello(String helloTo) { String helloStr = String.format("Hello, %s!", helloTo); System.out.println(helloStr); } public static void main(String[] args) { if (args.length != 1) { throw new IllegalArgumentException("Expecting one argument"); } String helloTo = args[0]; new Hello().sayHello(helloTo); } }
运行:
$ ./run.sh lesson01.exercise.Hello Bryan Hello, Bryan!
创建一个trace
一个trace是由若干span组成的有向无环图。一个span代表应用中的一个逻辑操作,每个span至少包含三个属性:操作名(an operation time)、开始时间(start time)、结束时间(finish time)。
下面我们使用一个io.opentracing.Tracer
实例创建由一个span组成的trace,可以使用io.opentracing.util.GlobalTracer.get()
创建一个全局的Tracer实例。代码如下:
import io.opentracing.Span; import io.opentracing.Tracer; import io.opentracing.util.GlobalTracer; public class Hello { private final Tracer tracer; private Hello(Tracer tracer) { this.tracer = tracer; } private void sayHello(String helloTo) { Span span = tracer.buildSpan("say-hello").start(); String helloStr = String.format("Hello, %s!", helloTo); System.out.println(helloStr); span.finish(); } public static void main(String[] args) { if (args.length != 1) { throw new IllegalArgumentException("Expecting one argument"); } String helloTo = args[0]; new Hello(GlobalTracer.get()).sayHello(helloTo); } }
这里我们使用了OpenTracing API的一些基本特性:
- 调用
tracer
实例的buildSpan()
方法创建一个span buildSpan()
方法的参数就是span的操作名- 调用
start()
方法真正创建出一个span - 通过调用
finish()
方法结束一个span - span的开始时间和结束时间由具体的tracer实现自动生成(获取创建和结束span时的系统时间戳)
此时,我们运行程序并不会和原来的程序有什么区别,也不会产生链路数据。因为OpenTracing只提供了SDK,并没有提供具体的链路实现,所以要产生真正的链路数据,需要借助具体的链路实现。
部署Jaeger(补充段落,原文没有)
这里我们选择Uber开源的Jaeger(发音为ˈyā-gər ),因为它对OpenTracing支持的比较好,而且部署使用也非常简单。另外Jaeger的作者就是Yurishkuro。这里就不介绍Jaeger的细节了,有兴趣的可以去官网了解:Jaeger官网。
Jaeger部署非常简单,从这里下载安装包或者下载docker镜像。这里我下载的macOS的安装包,解压后可以看到如下文件:
example-hotrod jaeger-agent jaeger-all-in-one jaeger-collector jaeger-ingester jaeger-queryexample-hotrod
直接运行./jaeger-all-in-one
便可以启动一个完整的Jaeger。此时访问http://localhost:16686/即可查看Jaeger的UI:

这样,一个OpenTracing的实现(Jaeger)就有了。接下来我们看如何在代码中集成。
集成Jaeger
在pom.xml中引入Jaeger的依赖:
<dependency> <groupId>io.jaegertracing</groupId> <artifactId>jaeger-client</artifactId> <version>0.32.0</version> </dependency>
然后写一个创建tracer的函数:
import io.jaegertracing.Configuration; import io.jaegertracing.Configuration.ReporterConfiguration; import io.jaegertracing.Configuration.SamplerConfiguration; import io.jaegertracing.internal.JaegerTracer; public static JaegerTracer initTracer(String service) { SamplerConfiguration samplerConfig = SamplerConfiguration.fromEnv().withType("const").withParam(1); ReporterConfiguration reporterConfig = ReporterConfiguration.fromEnv().withLogSpans(true); Configuration config = new Configuration(service).withSampler(samplerConfig).withReporter(reporterConfig); return config.getTracer(); }
最后,修改原来代码中的main函数:
Tracer tracer = initTracer("hello-world"); new Hello(tracer).sayHello(helloTo);
注意我们给initTracer()
方法传入了一个参数hello-world,这个是服务名。该服务里面产生的所有span公用这个服务名,一般服务名会用来做过滤和聚合。
现在运行代码,可以看到日志中有输出产生的span信息,而且也能看到Tracer实例的一些信息:
19:07:10.645 [main] DEBUG io.jaegertracing.thrift.internal.senders.ThriftSenderFactory - Using the UDP Sender to send spans to the agent. 19:07:10.729 [main] DEBUG io.jaegertracing.internal.senders.SenderResolver - Using sender UdpSender() # tracer实例信息 19:07:10.776 [main] INFO io.jaegertracing.Configuration - Initialized tracer=JaegerTracer(version=Java-1.1.0, serviceName=hello-world, reporter=CompositeReporter(reporters=[RemoteReporter(sender=UdpSender(), closeEnqueueTimeout=1000), LoggingReporter(logger=Logger[io.jaegertracing.internal.reporters.LoggingReporter])]), sampler=ConstSampler(decision=true, tags={sampler.type=const, sampler.param=true}), tags={hostname=NYC-MacBook, jaeger.version=Java-1.1.0, ip=192.168.0.109}, zipkinSharedRpcSpan=false, expandExceptionLogs=false, useTraceId128Bit=false) Hello, Bryan! # span信息 19:07:10.805 [main] INFO io.jaegertracing.internal.reporters.LoggingReporter - Span reported: a86d76defe28d413:a86d76defe28d413:0:1 - say-hello
当然也可以以调试模式启动,观察更多细节。
这个时候,我们打开Jaeger的UI,左侧的Service选择“hello-world”,然后点击最下面的“Find Traces”,就可以查到刚才这次程序运行产生的Trace信息了:

点击链路详情进去后,再次点击操作名,可以查看一些基本信息,Jaeger默认已经加了一些基本的信息。

下面我们来看如何加一些自定义的信息。
增加Tags和Logs
OpenTracing规定了可以给Span增加三种类型的注解信息:
- Tags:key-value格式的数据,key和value完全由用户自定义。需要注意的是Tags增加的信息应该是属于描述整个span的,也就是它是span的一个静态属性,记录的信息适用于span从创建到完成的任何时刻。再说直白点就是记录和时间点无关的信息,这个主要是和下面的Logs作区分。
- Logs:和Tags类似,也是key-value格式的数据,区别在于Logs的信息都会带一个时间戳属性,记录这条属性产生的时间戳,所以比较适合记录日志、异常栈等一些和时间相关的信息。
- Baggage Items:这个主要是用于跨进程全局传输数据,后面的lesson04专门演示这个特性,这里先不展开介绍了。
Tags和Logs的记录非常的简单和方便:
private void sayHello(String helloTo) { Span span = tracer.buildSpan("say-hello").start(); // 增加Tags信息 span.setTag("hello-to", helloTo); String helloStr = String.format("Hello, %s!", helloTo); // 增加Logs信息 span.log(ImmutableMap.of("event", "string-format", "value", helloStr)); System.out.println(helloStr); // 增加Logs信息 span.log(ImmutableMap.of("event", "println")); span.finish(); }
注意这里使用了Guava’s ImmutableMap.of()
来构造一个Map。
再次运行程序,同样会产生一个span,但这次span会多了一个Tag和Log信息(Jaeger默认已经加了一些内部的tag数据):

从图中可以看到代码中加的Tags信息和Logs信息,而且Logs信息是带了时间了(这里展示的是从span开始时间经过的毫秒数)。关于Tags和Logs的规范,OpenTracing做了一些引导规范,可以参考:semantic_conventions.
总结
本文主要展示了如何创建一个span,下篇文章演示如何如果创建一个包含多个span的trace,以及如何在进程内部(不同方法间)传递span信息。