Zipkin導入最小ステップ

# まえがき

既存のSpring BootプロジェクトへのZipkin導入をできるだけ少ないステップでまとめてみました。
サービス間のやり取りの結果についても書きたかったですが、別に何も意識せずとも勝手にやってくれるので、今回の記事では言及しません。
HTTP周りのやり取りは`spring-mvc`, `RestTemplate`を使っていれば勝手にやってくれます。

# Step1 依存の追加

## Maven

### Spring IO Platform を使っている場合

```xml:pom.xml
<properties>
  <spring-cloud.version>Edgware.RELEASE</spring-cloud.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
  </dependency>
  <!-- Kafka経由で送る場合はこれも必要 -->
  <dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
  </dependency>
</dependencies>
```

### Spring Boot のみの場合

[公式リファレンス](https://cloud.spring.io/spring-cloud-sleuth/single/spring-cloud-sleuth.html#_only_sleuth_log_correlation)を参照してください。

## Gradle

[公式リファレンス](https://cloud.spring.io/spring-cloud-sleuth/single/spring-cloud-sleuth.html#_only_sleuth_log_correlation)を参照してください。

# Step2 設定値の追加

Localマシンに全てデフォルトポートで構築済みであれば不要です。

## HTTP送信

```yaml:application.yaml
spring:
  zipkin:
    base-url: http://localhost:9411/ # default
```

## Kafka送信

```yaml:application.yaml
spring:
  kafka:
    bootstrap-servers: localhost:9092 # default
```

# Step3 動作確認環境構築

Mac前提です。

## Kafka & Zookeepr

```sh
$ brew install kafka
$ brew services start zookeeper
$ brew services start kafka
```

## Zipkin

```sh
$ wget -O zipkin.jar 'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec'
```

### HTTP経由のみ

```sh
$ java -jar zipkin.jar
```

### Kafka対応

```sh
$ KAFKA_ZOOKEEPER=localhost:2181 java -jar zipkin.jar
```

## Step4 動作確認

```java
@RequestMapping("/hello")
String hello() {
  return "hello world"
}
```

同作確認なので全てのリクエストをZipkinに送ります。

```yaml:application.yaml
spring:
  sleuth:
    sampler:
      percentage: 1.0 # 昔の名残でプロパティ名がパーセンテージになっていますが、割合指定らしいです。
```

<img width="676" alt="スクリーンショット 2018-02-20 13.19.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/66760/88907954-a928-7571-7ca7-d410a3e2795a.png">

`http://localhost:9411/`にアクセスする。

<img width="1712" alt="スクリーンショット 2018-02-20 13.21.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/66760/3aaa33c5-55ed-885a-6aee-b6730ee61f81.png">

<img width="1712" alt="スクリーンショット 2018-02-20 13.22.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/66760/b3dada7c-1564-8aba-ceca-ea1875170070.png">

<img width="1712" alt="スクリーンショット 2018-02-20 13.23.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/66760/c841bc1d-f613-0d5b-89f3-d9ef3e71b1d7.png">

ここまでの手順は依存ライブラリを追加しただけです。
送信先がデフォルトでない場合は、送信先のを指定をします。

## Step5 Spanの追加

これだけではリクエスト受信からレスポンス返却までの時間しかわかりません。
もう少しSpanを増やしてみます。

```java
@Autowired
DemoService demoService;

@RequestMapping("/hello")
public String helloWorld() throws Exception {
  Thread.sleep(300);
  return demoService.greeting("world");
}


@Service
static class DemoService {

  @NewSpan("greeting")
  String greeting(String target) throws Exception {
    Thread.sleep(200);
    return "Hello " + target;
  }
}
```

<img width="1184" alt="スクリーンショット 2018-02-20 13.45.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/66760/e824d15a-abbc-7a27-0b30-2f4a7c499d25.png">

## タグの追加

```java
@Service
static class DemoService {

  @NewSpan("greeting")
  String greeting(@SpanTag("target") String target) throws Exception {
    Thread.sleep(200);
    return "Hello " + target;
  }
}
```

`@SpanTag("target")` をパラメータに追加しただけです。

<img width="1699" alt="スクリーンショット 2018-02-20 13.50.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/66760/adce9d79-8136-aa65-2638-0dbd03d8cb8d.png">

### 色々な型のタグ

Zipkinはタグの値を文字列でしか受け取れないので、JSONにできるならそうして送ると良いです。

```java
@NewSpan("greeting-name")
public String greeting(@SpanTag(value = "name", resolver = AsJson.class) Name name) throws Exception {
  Thread.sleep(200);
  return "Hello " + name + ".";
}
```

```java:AsJson.java
@Component
public class AsJson implements TagValueResolver {

  ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
      .indentOutput(true)
      .build();

  @Override
  public String resolve(Object parameter) {
    if (parameter == null) {
      return null;
    }
    if (String.class.isAssignableFrom(parameter.getClass())) {
      return (String) parameter;
    }

    try {
      return objectMapper.writeValueAsString(parameter);
    } catch (JsonProcessingException e) {
      return null;
    }
  }
}
```

`@SpanTag(key = "name")` だと手元の環境だと認識されなかったので、`value =`で送りました。
`@Alias`の処理ができていないのか、後でコードを追ってみます。

JSONタグを確認してみます。

<img width="1699" alt="スクリーンショット 2018-02-20 14.02.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/66760/d17c1892-e1d6-2b1c-3ce0-7ca68a679859.png">

大丈夫そうです。

# トラブルシューティング

## ZipkinサーバーがV1でJSONフォーマットが違うんだけど。

JSONフォーマットはバージョン指定ができます。

```yaml:application.yaml
spring:
  zipkin:
    encoder: json_v1
```

## Zipkinサーバーでサービス名のラベルが空文字列で表示される

V1のZipkinはラベル表示に使用するサービス名を規定のタグでしか見ないようです。
カスタムタグのみのSpanではサービス名を見てくれないらしいです。
https://github.com/openzipkin/zipkin/issues/962

そこで適当にコンポーネント名を設定しておきます。

```java
@Bean
@ConditionalOnProperty(name = "spring.zipkin.encoder", havingValue = "json_v1")
SpanAdjuster spanAdjuster() {
  return span -> {
    span.tag(Span.SPAN_LOCAL_COMPONENT_TAG_NAME, "my-component");
    return span;
  };
}
```

これで大丈夫です。

## 非同期処理で途切れる

[公式リファレンス](https://cloud.spring.io/spring-cloud-sleuth/single/spring-cloud-sleuth.html#_asynchronous_communication)を参照してください。

`@Async`ではデフォルトで途切れない設定になっていると思います。

`ExecutorService`を使っている場合は、

```java
ExecutorService traceableExecutorService = new TraceableExecutorService(beanFactory, executorService);
```

として、使えば大丈夫です。

## 処理の途中でタグを追加したい

アノテーションで指定できないタイミングでタグを追加したい時があります。

```java
@Autowired
Tracer tracer

public void something() {
  tracer.addTag("$name", "$value");
}
```

これでOKです。現在のSpanにタグ付けされます。