寝て起きて寝る

過ぎたるは及ばざるが如し

OpenTracingチュートリアルをやってみた(その2)

OpenTracingチュートリアルをやったメモ

前回の「OpenTracingチュートリアルをやってみた(その1)」の続きです。

リモートプロセス呼出をトレーシングしてみる

前回のつづきLesson 3 - Tracing RPC Requestsを。

前回作成したformatStringメソッドとprintHelloメソッドの内容をFormatterサービスとPublisherサービスの2つのサービスに独立させトレーシングしてみます。

  • 前回レッスンのHello.javaをベースに処理をHTTP呼出のサービスへ独立させ呼び出します。
    • FormatterサービスはGET 'http://localhost:8081/format?helloTo=Bryan'でリクエストを受け"Hello, Bryan!"を返却するAPIに変更します。
    • PublisherサービスはGET 'http://localhost:8082/publish?helloStr=Hello%2c%20Bryan'でリクエストを受け"Hello, Bryan"と標準出力します。
  • Hello.javaではHTTP Clientを使用し2つのサービスを呼び出します。
サービス間のコンテキスト伝播について

まず、リモートプロセス呼出のトレースを続けるためにはSpanコンテキストを伝播する必要があります。 OpenTracing APIはそれを実現する為にTracerインターフェースにinject(spanContext, format, carrier)extract(format, carrier)の2つのメソッドを提供します。 引数のformatのパラメータはOpenTracing APIが定義する3つの標準エンコーディングのうちの1つを指定します。

  • TEXT_MAP:Spanコンテキストは文字列のKey:Valueのコレクションにされます。
  • HTTP_HEADERS:TEXT_MAPに似ていますが、安全にHTTPヘッダーを使用します。
  • BINARY:Spanコンテキストはバイト配列にされます。

Inject and Extract

Client側の実装

tracer.injectを利用してspanContextをHTTPリクエストに乗せて伝播させます。下記の例ではHTTP_HEADERSを使用します。

    private String formatString(String helloTo) {
        try (Scope scope = tracer.buildSpan("formatString").startActive(true)) {
            String helloStr = getHttp(8081, "format", "helloTo", helloTo);
            scope.span().log(ImmutableMap.of("event", "string-format", "value", helloStr));
            return helloStr;
        }
    }

    private void printHello(String helloStr) {
        try (Scope scope = tracer.buildSpan("printHello").startActive(true)) {
            getHttp(8082, "publish", "helloStr", helloStr);
            scope.span().log(ImmutableMap.of("event", "println"));
        }
    }

    private String getHttp(int port, String path, String param, String value) {
        try {
            HttpUrl url = new HttpUrl.Builder().scheme("http").host("localhost").port(port).addPathSegment(path)
                    .addQueryParameter(param, value).build();
            Request.Builder requestBuilder = new Request.Builder().url(url);

            Tags.SPAN_KIND.set(tracer.activeSpan(), Tags.SPAN_KIND_CLIENT);
            Tags.HTTP_METHOD.set(tracer.activeSpan(), "GET");
            Tags.HTTP_URL.set(tracer.activeSpan(), url.toString());
            tracer.inject(tracer.activeSpan().context(), Format.Builtin.HTTP_HEADERS, new HttpHeadersCarrier(requestBuilder));

            Request request = requestBuilder.build();
            Response response = client.newCall(request).execute();
            if (response.code() != 200) {
                throw new RuntimeException("Bad HTTP result: " + response);
            }
            return response.body().string();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static class HttpHeadersCarrier implements TextMap {
        private final Request.Builder builder;

        HttpHeadersCarrier(Request.Builder builder) {
            this.builder = builder;
        }
        @Override
        public Iterator<Map.Entry<String, String>> iterator() {
            throw new UnsupportedOperationException("carrier is write-only");
        }
        @Override
        public void put(String key, String value) {
            builder.addHeader(key, value);
        }
    }

パケットを確認するとuber-trace-idというheaderで渡されています。

f:id:yasu7ri:20181202200726p:plain

ちなみにTEXT_MAPで行った場合は下記の様になっています。

f:id:yasu7ri:20181202222146p:plain

TraceId、SpanId、ParentIdがコロン区切りで繋げた文字列をHTTP_HEADERSを指定した場合はURLエンコーディングした物になり、TEXT_MAPの場合はそのままheaderに設定されるようです。

io.jaegertracing.internal.propagation.TextMapCodec#contextAsString(JaegerSpanContext context)

  public static String contextAsString(JaegerSpanContext context) {
    int intFlag = context.getFlags() & 0xFF;
    return new StringBuilder()
        .append(context.getTraceId()).append(":")
        .append(Long.toHexString(context.getSpanId())).append(":")
        .append(Long.toHexString(context.getParentId())).append(":")
        .append(Integer.toHexString(intFlag))
        .toString();
  }

  private String encodedValue(String value) {
    if (!urlEncoding) {
      return value;
    }
    try {
      return URLEncoder.encode(value, "UTF-8");
    } catch (UnsupportedEncodingException e) {
      // not much we can do, try raw value
      return value;
    }
  }

jaeger-client-java/TextMapCodec.java at 88a849722d7056557a2643330737967d77b87f88 · jaegertracing/jaeger-client-java · GitHub

Service側の実装

チュートリアルではサービスはDropwizardを使用してサービスを実装しています。

まず、各サービスではHello.javaと同様にTracerインスタンを生成します。

各サービスではClient側と同じようにbuildSpan()を使用してScopeオブジェクトを生成しますが、 まず標準アダプタのTextMapExtractAdapterを使用してリクエストのheaderから伝播されたspanContext情報をHashMap <String、String>に変換し extract(format, carrier)を使用してspanContextを取得します。

header情報からspanContextが生成された場合は、その子SpanとしてScopeオブジェクトを生成します。

public class Formatter extends Application<Configuration> {
    private final Tracer tracer;

    private Formatter(Tracer tracer) {
        this.tracer = tracer;
    }

    public static void main(String[] args) throws Exception {
        System.setProperty("dw.server.applicationConnectors[0].port", "8081");
        System.setProperty("dw.server.adminConnectors[0].port", "9081");
        io.jaegertracing.Configuration.SamplerConfiguration samplerConfiguration = io.jaegertracing.Configuration.SamplerConfiguration.fromEnv().withType("const").withParam(1);
        io.jaegertracing.Configuration.ReporterConfiguration reporterConfiguration = io.jaegertracing.Configuration.ReporterConfiguration.fromEnv().withLogSpans(true);
        io.jaegertracing.Configuration configuration = new io.jaegertracing.Configuration("formatter").withSampler(samplerConfiguration).withReporter(reporterConfiguration);
        Tracer tracer =  configuration.getTracer();
        new Formatter(tracer).run("server");
    }

    @Override
    public void run(Configuration configuration, Environment environment) throws Exception {
        environment.jersey().register(new FormatterResource());
    }

    @Path("/format")
    @Produces(MediaType.TEXT_PLAIN)
    public class FormatterResource {
        @GET
        public String format(@QueryParam("helloTo") String helloTo, @Context HttpHeaders httpHeaders) {
            try (Scope scope = startServerSpan(tracer, httpHeaders, "format")) {
                return String.format("Hello, %s!", helloTo);
            }
        }
    }

    protected static Scope startServerSpan(Tracer tracer, HttpHeaders httpHeaders, String operationName) {
        MultivaluedMap<String, String> rawHeaders = httpHeaders.getRequestHeaders();
        final HashMap<String, String> headers = new HashMap<String, String>();
        for (String key : rawHeaders.keySet()) {
            headers.put(key, rawHeaders.get(key).get(0));
        }

        Tracer.SpanBuilder spanBuilder;
        try {
            SpanContext parentSpan = tracer.extract(Format.Builtin.HTTP_HEADERS, new TextMapExtractAdapter(headers));
            if (parentSpan == null) {
                spanBuilder = tracer.buildSpan(operationName);
            } else {
                spanBuilder = tracer.buildSpan(operationName).asChildOf(parentSpan);
            }
        } catch (IllegalArgumentException e) {
            spanBuilder = tracer.buildSpan(operationName);
        }
        return spanBuilder.withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER).startActive(true);
    }
}

Publisherサービスの方も同様に実装します。違いはサービスのポートと実際の処理部分だけです。

トレース結果

Client側のmainメソッドを実行じ2つのサービスを呼出たトレース結果です。

f:id:yasu7ri:20181202231300p:plain

トレーシングコンテキスト以外の伝播

最後にLesson 4 - Baggage

OpenTracingを使用することトレーシングコンテキスト以外のものも伝播出来る様にすることが出来ます。 このトレーシングコンテキスト以外で伝播させたい情報を手荷物と同じように全てのリモートプロセスで持ち回れる事からBaggageと呼びます。

BaggageはScope.span().setBaggageItem(String key, Syring vlue)で設定します。取得はScope.span().getBaggageItem(String key)で行います。

Client側の実装
    private void sayHello(String helloTo) {
        try (Scope scope = tracer.buildSpan("say-hello").startActive(true)) {
            scope.span().setTag("hello-to", helloTo);
            scope.span().setBaggageItem("item1","this is item1");
            String helloStr = formatString(helloTo);
            printHello(helloStr);
        }
    }

設定したBaggageはhaederに追加されています。

f:id:yasu7ri:20181203002535p:plain

Service側の実装
  • Formatterサービス
    @Path("/format")
    @Produces(MediaType.TEXT_PLAIN)
    public class FormatterResource {
        @GET
        public String format(@QueryParam("helloTo") String helloTo, @Context HttpHeaders httpHeaders) {
            try (Scope scope = startServerSpan(tracer, httpHeaders, "format")) {
                String item1 = scope.span().getBaggageItem("item1");
                scope.span().setTag("item1", item1);
                return String.format("Hello, %s!", helloTo);
            }
        }
    }
  • Publisherサービス
    @Path("/publish")
    @Produces(MediaType.TEXT_PLAIN)
    public class PublisherResource {
        @GET
        public String publish(@QueryParam("helloStr") String helloStr, @Context HttpHeaders httpHeaders) {
            try (Scope scope = startServerSpan(tracer, httpHeaders, "publish")) {
                String item1 = scope.span().getBaggageItem("item1");
                scope.span().setTag("item1", item1);
                System.out.println(helloStr);
                return "published";
            }
        }
    }

設定したBaggageはサービスを跨いでも伝播されています。

f:id:yasu7ri:20181203002305p:plain