本文内容主要翻译(意译)自Yurishkuro大神的opentracing-tutorial java,加了一些补充说明,方便理解,习惯看英文的也可以看原文。总共4篇,本文是第1篇。如果你还没接触过OpenTracing,建议先读这篇文章《OpenTracing概念术语介绍》和官方文档

  1. 第1篇:单span的trace创建本文)。
  2. 第2篇:多span的trace创建(进程内SpanContext传递)
  3. 第3篇:跨服务(进程)传递SpanContext
  4. 第4篇:Baggage介绍

目标

学习如何:

  • 实例化一个Tracer
  • 创建一个简单的链路
  • 给链路增加一些注解(annotation):即增加Tag和Log。

开发步骤

说明:源代码的exercise包下面的类是空的,是留给我们按教程一步步补充完善的;solution包是已经编写好的代码。我翻译的时候,都直接运行的是solution里面的代码,但教程里面是逐步完善代码的,也就是会有一个中间状态。所以我会根据内容作了一些必要的注释和修改。但如果你是第一次看的话,建议按照教程自己手动在exercise里面完善。跟着教程一步步学习。

创建一个简单的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-query

直接运行./jaeger-all-in-one便可以启动一个完整的Jaeger。此时访问http://localhost:16686/即可查看Jaeger的UI:

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信息了:

lesson-01-1.png

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

lesson-01-2.png

下面我们来看如何加一些自定义的信息。

增加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数据):

lesson-01-3.png

从图中可以看到代码中加的Tags信息和Logs信息,而且Logs信息是带了时间了(这里展示的是从span开始时间经过的毫秒数)。关于Tags和Logs的规范,OpenTracing做了一些引导规范,可以参考:semantic_conventions.

总结

本文主要展示了如何创建一个span,下篇文章演示如何如果创建一个包含多个span的trace,以及如何在进程内部(不同方法间)传递span信息。