CLOVER🍀

That was when it all began.

Spring Boot Actuator × Micrometer Tracing × OpenTelemetryでトレーシング

これは、なにをしたくて書いたもの?

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).

Spring Cloud Sleuth

今後はMicrometer Tracingを使うようですね。

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.

Micrometer Tracing / Purpose

サポートしているトレーサーは、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側に取り込まれたようです。

https://github.com/spring-projects/spring-boot/blob/v3.1.5/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/OpenTelemetryAutoConfiguration.java

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もあります。

Common Application Properties / Actuator Properties / management.otlp.metrics.export.aggregation-temporality

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です。

GitHub - jdbc-observations/datasource-micrometer: Micrometer Tracing instrumentation and Spring Boot 3 Auto-Configuration for JDBC APIs

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のトレーシング事情はあまり知らなかったので、良いキャッチアップの機会になりました。