これは、なにをしたくて書いたもの?
Spring BootでOpenTelemetryを扱ってみようかなと思って調べてみると、トレーシングまわりの事情がいろいろ変わっているようなので
いい機会だなと思って試してみることにしました。
Spring Cloud Sleuth
Springを使ったアプリケーションのトレーシングといえばSpring Cloud Sleuthだと思いますが、これはMicrometer Tracingへ移されたようです。
Spring Cloud Sleuth’s last minor version is 3.1. You can check the 3.1.x branch for the latest commits. The core of this project got moved to Micrometer Tracing project and the instrumentations will be moved to Micrometer and all respective projects (no longer all instrumentations will be done in a single repository).
今後はMicrometer Tracingを使うようですね。
Micrometer Tracing
Micrometer Tracingについては、こちら。
Micrometer Tracingは、Spring Cloud Sleuthが名を変えたものだと明確に書かれていますね。
In 2016, the Spring Cloud team created a tracing library that could help a lot of developers. It was called Spring Cloud Sleuth. The Spring team realized that tracing could be separated from Spring Cloud and created the Micrometer Tracing project, which is, essentially, a Spring-agnostic copy of Spring Cloud Sleuth. Micrometer Tracing had its 1.0.0 GA release in November 2022 and has been getting steadily better ever since.
サポートしているトレーサーは、Zipkin BraveとOpenTelemetryです。
Micrometer Tracing / Supported Tracers
micrometer-tracing-bridge-brave
とmicrometer-tracing-bridge-otel
のどちらかのブリッジを選択して使うことになります。
サポートされているレポーターは、WavefrontとZipkinです。
Micrometer Tracing / Supported Reporters
Spring BootとMicrometer Tracing
Spring Cloud SleuthはMicrometer Tracingに変わったわけですが、マイグレーションガイドはこちらにあります。
Spring Cloud Sleuth 3.1 Migration Guide · micrometer-metrics/tracing Wiki · GitHub
マイグレーション自体は、基本的にはパッケージ名の変更で済むようです。
Spring Cloud Sleuth 3.1 Migration Guide / Spring Cloud Sleuth API migration
OpenTelemetryにフォーカスすると、OpenTelemetry用のAutoConfigurationがもともとSpring Cloud Sleuthにあったようですが、これが
Spring Boot側に取り込まれたようです。
Spring Boot Actuator Tracing
こうなると、Spring Boot側にMicrometer Tracingの話が出てくることになります。
場所は、Spring Boot Actuatorです。
Production-ready Features / Tracing
ここでも、サポートされているトレーサーはZipkinとWavefrontであることが書かれています。
Production-ready Features / Tracing / Supported Tracers
ただ、後ろを見ているとOTLPも使えそうではありますね。
Production-ready Features / Tracing / Tracer Implementations / OpenTelemetry With OTLP
このあたりは、追加する依存関係でコントロールするようです。
OTLP向けのCommon Application Propertiesもあります。
TraceやSpanのIDをログに出力したり
Production-ready Features / Tracing / Getting Started
AutoConfigurationされたRestTemplateBuilder
やWebClient.Builder
を使うことで、コンテキストの伝播も可能なようです。
Production-ready Features / Tracing / Propagating Traces
トレーシングに関するCommon Application Propertiesはこちら。
Common Application Properties / Actuator Properties / management.tracing.baggage.correlation.enabled
というわけで、ここまで見てきたのでとりあえず試していってみましょう。
お題
今回のお題としては、以下の構成を考えます。
flowchart LR クライアント --> |curl/HTTP| A A[API-A/Spring Boot] --> |RestTemplate/HTTP| B[API-B/Spring Boot] B --> |JDBC| D[(MySQL)] A --> |テレメトリーデータ| J[Jaeger] B --> |テレメトリーデータ| J
アプリケーションからMySQLに発行するSQLは、select now()
くらいの超単純なものにしたいと思います。
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.8.1 2023-08-24 OpenJDK Runtime Environment (build 17.0.8.1+1-Ubuntu-0ubuntu122.04) OpenJDK 64-Bit Server VM (build 17.0.8.1+1-Ubuntu-0ubuntu122.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.5 (57804ffe001d7215b5e7bcb531cf83df38f93546) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.8.1, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-88-generic", arch: "amd64", family: "unix"
MySQLは、172.17.0.2で動作しているものとします。
MySQL localhost:3306 ssl practice SQL > select version(); +-----------+ | version() | +-----------+ | 8.0.35 | +-----------+ 1 row in set (0.0007 sec)
Jaegerは、172.17.0.3で動作しているものとします。
$ ./jaeger-all-in-one version 2023/11/03 13:02:58 maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined 2023/11/03 13:02:58 application version: git-commit=d482f3f7bf780f72a20ddfedbe3aa6ec5c5e4613, git-version=v1.50.0, build-date=2023-10-08T00:14:18Z {"gitCommit":"d482f3f7bf780f72a20ddfedbe3aa6ec5c5e4613","gitVersion":"v1.50.0","buildDate":"2023-10-08T00:14:18Z"}
Spring Bootプロジェクトを作成する
それでは、Spring Bootプロジェクトを作成していきます。2つ作成することになりますね。
API-B
後ろから作っていくことにしましょう。依存関係にweb
、jdbc
、mysql
、そしてactuator
を加えて、Spring Bootプロジェクトを作成。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=3.1.5 \ -d javaVersion=17 \ -d type=maven-project \ -d name=api-b \ -d groupId=org.littlewings \ -d artifactId=api-b \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring.tracing \ -d dependencies=web,jdbc,mysql,actuator \ -d baseDir=api-b | tar zxvf -
プロジェクト内に移動。
$ cd api-b
自動生成されたソースコードは削除。
$ rm src/main/java/org/littlewings/spring/tracing/ApiBApplication.java src/test/java/org/littlewings/spring/tracing/ApiBApplicationTests.java
生成された時点での依存関係は、こちら。
<properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
ここに、以下の依存関係を加えます。
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-otel</artifactId> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-otlp</artifactId> </dependency> <dependency> <groupId>net.ttddyy.observation</groupId> <artifactId>datasource-micrometer-spring-boot</artifactId> <version>1.0.2</version> </dependency>
micrometer-tracing-bridge-otel
はMicrometer Observation APIをOpenTelemetryにブリッジするライブラリ、opentelemetry-exporter-otlp
は
トレースをOTLPを受け付けるCollectorに送信します。
datasource-micrometer-spring-boot
というのは、JDBCに関するObservabilityの情報を取得するのに必要なライブラリです。
これを追加しないとJDBC部分のトレースができないことで気づいたのですが、Observabilityのセクションに記載がありました。
Observability for JDBC and R2DBC can be configured using separate projects. For JDBC, the Datasource Micrometer project provides a Spring Boot starter which automatically creates observations when JDBC operations are invoked. Read more about it in the reference documentation. For R2DBC, the Spring Boot Auto Configuration for R2DBC Observation creates observations for R2DBC query invocations.
Production-ready Features / Observability
JDBCおよびR2DBCについては、Observabiltyを記録するために別のプロジェクトが必要なようです。
JDBCの場合は、Datasource Micrometerです。
Datasource Micrometer Reference Documentation
というわけで、Spring Boot用のライブラリを使えということなので、datasource-micrometer-spring-boot
を追加しました。
ソースコードの作成。
SQLを発行するRestControlelr。
src/main/java/org/littlewings/spring/tracing/NowController.java
package org.littlewings.spring.tracing; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Collections; @RestController public class NowController { private Logger logger = LoggerFactory.getLogger(NowController.class); private NamedParameterJdbcTemplate jdbcTemplate; public NowController(NamedParameterJdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @GetMapping(value = "now", produces = MediaType.TEXT_PLAIN_VALUE) public String now() { logger.info("access"); return jdbcTemplate.queryForObject("select now()", Collections.emptyMap(), String.class); } }
ログも追加しています。こちらは、せっかくなのでログにTraceやSpanのIDが埋め込めることも確認しようと思います。
main
クラス。
src/main/java/org/littlewings/spring/tracing/App.java
package org.littlewings.spring.tracing; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
設定。
src/main/resources/application.properties
spring.application.name=api-b server.port=9080 spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=UTF-8&connectionCollation=utf8mb4_0900_bin spring.datasource.username=kazuhira spring.datasource.password=password management.tracing.sampling.probability=1.0 management.otlp.tracing.endpoint=http://172.17.0.3:4318/v1/traces logging.pattern.console=%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){faint} %clr(-%5p) %clr(${PID:- }){magenta} [${spring.application.name:},%X{traceId:-},%X{spanId:-}] %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx
いくつか説明を。
ポートは9080にずらしておきました。
spring.application.name
は、Collectorに報告されるサービス名に使用されます。デフォルトはapplication
なので、これは変更しておいた方が
よいでしょう。
こちらは、トレーシングの設定です。
management.tracing.sampling.probability=1.0 management.otlp.tracing.endpoint=http://172.17.0.3:4318/v1/traces
management.tracing.sampling.probability
はデフォルト値が0.1で、リクエストの10%を記録します。今回はすべてのリクエストを記録する
ことにします。management.otlp.tracing.endpoint
は、OTLP Collectorのエンドポイントを指定します。
ちょっとハマったのが、/v1/traces
を入れないとまったく通りませんでした…。
最後は、ログフォーマットです。
logging.pattern.console=%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){faint} %clr(-%5p) %clr(${PID:- }){magenta} [${spring.application.name:},%X{traceId:-},%X{spanId:-}] %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx
デフォルトのフォーマットから、[${spring.application.name:},%X{traceId:-},%X{spanId:-}]
を追加しています。
%X{traceId}
でTraceのID、%X{spanId}
でSpanのIDをログ出力できます。
あとはパッケージングして
$ mvn package
起動。
$ java -jar target/api-b-0.0.1-SNAPSHOT.jar
これで、API-B側の準備は完了です。
API-A
続いては、API-A。web
とactuator
を追加して、Spring Bootプロジェクトの作成。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=3.1.5 \ -d javaVersion=17 \ -d type=maven-project \ -d name=api-a \ -d groupId=org.littlewings \ -d artifactId=api-a \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring.tracing \ -d dependencies=web,actuator \ -d baseDir=api-a | tar zxvf -
プロジェクト内へ移動。
$ cd api-a
自動生成されたソースコードは削除。
$ rm src/main/java/org/littlewings/spring/tracing/ApiAApplication.java src/test/java/org/littlewings/spring/tracing/ApiAApplicationTests.java
作成時点でのMaven依存関係など。
<properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
こちらに、以下を追加します。
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-otel</artifactId> </dependency> <dependency> <groupId>io.opentelemetry</groupId> <artifactId>opentelemetry-exporter-otlp</artifactId> </dependency>
ソースコードを作成しましょう。
API-BへHTTPリクエストを送信するRestController。
src/main/java/org/littlewings/spring/tracing/ProxyNowController.java
package org.littlewings.spring.tracing; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class ProxyNowController { private Logger logger = LoggerFactory.getLogger(ProxyNowController.class); private RestTemplate restTemplate; public ProxyNowController(RestTemplateBuilder restTemplateBuilder) { this.restTemplate = restTemplateBuilder.build(); } @GetMapping(value = "proxy/now", produces = MediaType.TEXT_PLAIN_VALUE) public String now() { logger.info("access"); return restTemplate.getForObject("http://localhost:9080/now", String.class); } }
ここでのポイントは、AutoConfigurationされたRestTemplateBuilder
を使うことです。これで、後ろのAPI-BまでTraceが伝播するように
なります。
public ProxyNowController(RestTemplateBuilder restTemplateBuilder) { this.restTemplate = restTemplateBuilder.build(); }
Production-ready Features / Tracing / Propagating Traces
自分でRestTemplate
のインスタンスを作成したりすると、Traceが途切れてしまいます。
main
クラス。
src/main/java/org/littlewings/spring/tracing/App.java
package org.littlewings.spring.tracing; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
設定。API-Bからデータソースとポートの設定が外れ、spring.application.name
がAPI-A用に設定されただけです。
@src/main/resources/application.properties`
spring.application.name=api-a management.tracing.sampling.probability=1.0 management.otlp.tracing.endpoint=http://172.17.0.3:4318/v1/traces logging.pattern.console=%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){faint} %clr(-%5p) %clr(${PID:- }){magenta} [${spring.application.name:},%X{traceId:-},%X{spanId:-}] %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx
ではパッケージングして
$ mvn package
起動。
$ java -jar target/api-a-0.0.1-SNAPSHOT.jar
これで、準備はできました。
確認してみる
動作確認してみましょう。
curlでアクセス。
$ curl localhost:8080/proxy/now 2023-11-03 14:42:00 $ curl localhost:8080/proxy/now 2023-11-03 14:42:01 $ curl localhost:8080/proxy/now 2023-11-03 14:42:02
http://[Jaegerが動作しているホスト:16686]
にアクセスして、JaegerのWeb UIを見てみます。
api-a
で検索すると、トレースが記録されていることが確認できます。
詳細はこんな感じです。
OKですね。
ログの方はどうでしょう。
API-A。
2023-11-03T23:41:59.809+09:00 - INFO 181 [api-a,b1adfffea781739038f2b333315a8908,abf373f5df79b6ba] --- [nio-8080-exec-1] o.l.spring.tracing.ProxyNowController : access 2023-11-03T23:42:01.265+09:00 - INFO 181 [api-a,be2e1b232edc8f4133af651baabbba72,86bb92886ae202dd] --- [nio-8080-exec-2] o.l.spring.tracing.ProxyNowController : access 2023-11-03T23:42:02.953+09:00 - INFO 181 [api-a,010aa96dd63bfb9b03241e01ff066136,7e414e21d2e5a69a] --- [nio-8080-exec-3] o.l.spring.tracing.ProxyNowController : access
API-B。
2023-11-03T23:42:00.017+09:00 - INFO 127 [api-b,b1adfffea781739038f2b333315a8908,21e04ebe3d3e4894] --- [nio-9080-exec-1] o.l.spring.tracing.NowController : access 2023-11-03T23:42:01.270+09:00 - INFO 127 [api-b,be2e1b232edc8f4133af651baabbba72,804fb4c3ca1316e6] --- [nio-9080-exec-2] o.l.spring.tracing.NowController : access 2023-11-03T23:42:02.958+09:00 - INFO 127 [api-b,010aa96dd63bfb9b03241e01ff066136,32570f0eea270af2] --- [nio-9080-exec-3] o.l.spring.tracing.NowController : access
こちらはAPI-AとAPI-Bで同じTrace IDがそれぞれ伝播、記録され、SpanのIDもログに記録できていることが確認できました。
おわりに
Spring Bootで、Micrometer TracingとOpenTelemetryを使ってトレーシングをしてみました。
最近のSpring Bootのトレーシング事情はあまり知らなかったので、良いキャッチアップの機会になりました。