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はサービスを跨いでも伝播されています。
