OpenTracingチュートリアルをやってみた(その2)
OpenTracingチュートリアルをやったメモ
前回の「OpenTracingチュートリアルをやってみた(その1)」の続きです。
リモートプロセス呼出をトレーシングしてみる
前回のつづきLesson 3 - Tracing RPC Requestsを。
前回作成したformatString
メソッドとprintHello
メソッドの内容をFormatter
サービスとPublisher
サービスの2つのサービスに独立させトレーシングしてみます。
- 前回レッスンの
Hello.java
をベースに処理をHTTP呼出のサービスへ独立させ呼び出します。 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コンテキストはバイト配列にされます。
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で渡されています。
ちなみにTEXT_MAPで行った場合は下記の様になっています。
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; } }
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つのサービスを呼出たトレース結果です。
トレーシングコンテキスト以外の伝播
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に追加されています。
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はサービスを跨いでも伝播されています。