Spring Boot x Micrometer x Zipkinでトレーシング

# やりたい事
- Spring Boot、Micrometer、RestTemplate を使用する
- 複数アプリケーション間でトレースIDを伝播する
- トレース情報をZipkinに送信する

# 参考にした記事
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator.metrics.micrometer-observation
https://kazuhira-r.hatenablog.com/entry/2023/11/03/234914

# 構成
api-service から customer-service にリクエストを投げる単純なシステムになります。
![distributed-tracing.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/5a567653-9542-f5bb-d1a4-fe12d3a0b72b.png)

# Spring Boot アプリケーションの作成
## api-service の実装
構成図の api-service に該当するアプリケーションを作成します。

``` build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
	id("org.springframework.boot") version "3.2.0"
	id("io.spring.dependency-management") version "1.1.4"
	kotlin("jvm") version "1.9.20"
	kotlin("plugin.spring") version "1.9.20"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"

java {
	sourceCompatibility = JavaVersion.VERSION_17
}

repositories {
	mavenCentral()
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter")
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.springframework.boot:spring-boot-starter-actuator")
	//	Micrometer Observation APIをOpenTelemetryにブリッジするライブラリ
	implementation("io.micrometer:micrometer-tracing-bridge-otel:1.2.0")
	// トレース情報を Zipkin に送信するライブラリ
	implementation("io.opentelemetry:opentelemetry-exporter-zipkin:1.32.0")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
	kotlinOptions {
		freeCompilerArgs += "-Xjsr305=strict"
		jvmTarget = "17"
	}
}

tasks.withType<Test> {
	useJUnitPlatform()
}
```

ネットワーク上でトレースを自動的に伝播させるには、auto-configured された `RestTemplateBuilder`を使用する必要があります。
``` RestTemplateAutoConfiguration.kt
import org.springframework.boot.autoconfigure.AutoConfigureAfter
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration
import org.springframework.boot.web.client.RestTemplateBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.client.RestTemplate

@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(HttpMessageConvertersAutoConfiguration::class)
class RestTemplateAutoConfiguration(
    private val restTemplateBuilder: RestTemplateBuilder
) {

    @Bean
    fun restTemplate(): RestTemplate {
        return restTemplateBuilder.build()
    }
}
```

``` CustomerService.kt
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
import org.springframework.web.client.getForEntity
import org.springframework.web.util.UriComponentsBuilder

@Service
class CustomerService(
    private val restTemplate: RestTemplate
) {
    fun get(): CustomerRes {
        val uri = UriComponentsBuilder.fromUriString("http://localhost:8081")
            .path("customer")
            .toUriString()

        val response: ResponseEntity<CustomerRes> = restTemplate.getForEntity(
            uri,
            CustomerRes::class
        )

        return response.body!!
    }
}

data class CustomerRes(
    val value: String
)
```

``` CustomerController.kt
import org.apache.commons.logging.LogFactory
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class CustomerController(
    private val customerService: CustomerService
) {
    private val logger = LogFactory.getLog(CustomerController::class.java)

    @RequestMapping("/customer")
    fun get(): HelloRes {
        logger.info("get() has been called")
        val result = customerService.get()
        return HelloRes(
            result.value
        )
    }

    class HelloRes(
        val value: String
    )
}
```

``` application.yml
spring:
  application:
    name: api-service customer-service # Zipkin に送信されるサービス名。同一のサービスと認識されてしまう為、一意である必要がある

management:
  tracing:
    sampling:
      probability: 1.0 # トレース情報の送信割合。1.0 で全てのトレース情報が送信される

logging:
  level:
    root: INFO
```

## customer-service の実装
構成図の customer-service に該当するアプリケーションを作成します。
build.gradle.kts は api-service と同じです。

``` CustomerController.kt
import org.apache.commons.logging.LogFactory
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class CustomerController {
    private val logger = LogFactory.getLog(CustomerController::class.java)

    @GetMapping("/customer")
    fun get(): CustomerRes {
        logger.info("get() has been called")
        return CustomerRes("AAA株式会社")
    }

    class CustomerRes(
        val value: String
    )
}
```

``` application.yml
spring:
  application:
    name: api-service customer-service # Zipkin に送信されるサービス名。同一のサービスと認識されてしまう為、一意である必要がある

management:
  tracing:
    sampling:
      probability: 1.0 # トレース情報の送信割合。1.0 で全てのトレース情報が送信される

logging:
  level:
    root: INFO
```

# Zipkin の起動
```
docker run -d -p 9411:9411 openzipkin/zipkin
```

# Zipkin でトレース情報を見る
環境の構築が終わったので、実際に Zipkin でトレース情報を見てみます。
アプリケーションを何回か動かします。
http://localhost:8080/customer

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

以下のようなトレース情報が表示されます。
![Zipkin_list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/5b763614-7eb2-4e92-5ab5-8aadcd4d19fb.png)

![Zipkin_detail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/28a190d6-b2d7-6748-1ca2-71f151b3b91c.png)

![Zipkin_dependencies.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149245/814248ec-02bc-0b29-d1c6-ef20c1bfd397.png)

コンソールログにはそれぞれ以下のlogが出力されます。
`fd1b6573c72d8270f7b993bff6cccb4b`の部分がTrace IDになり、`73daf3d1a44fa0ce`や`bab80bc50c856162`は、Span IDになります。
こちらに記載している設定においては、Trace IDは一連のリクエストに対して同一の値が出力され、Span IDは各アプリケーション内で一連の処理に対して同一の値が出力されます。

#### api-service
```
2023-12-16T22:04:05.057+09:00  INFO 32049 --- [api-service customer-service] [nio-8080-exec-3] [fd1b6573c72d8270f7b993bff6cccb4b-73daf3d1a44fa0ce] c.e.s.controller.CustomerController      : get() has been called
2023-12-16T22:27:51.215+09:00  INFO 32049 --- [api-service customer-service] [nio-8080-exec-8] [b968352258fd27290e96238293776cb4-c5738f85f6f22a95] c.e.s.controller.CustomerController      : get() has been called
```
#### customer-service
```
2023-12-16T22:04:05.090+09:00  INFO 27445 --- [customer-service] [nio-8081-exec-7] [fd1b6573c72d8270f7b993bff6cccb4b-bab80bc50c856162] c.e.s.controller.CustomerController      : get() has been called
2023-12-16T22:27:51.225+09:00  INFO 27445 --- [customer-service] [nio-8081-exec-8] [b968352258fd27290e96238293776cb4-b57d4c22ce9b8af4] c.e.s.controller.CustomerController      : get() has been called
```