Note

2026-02-04 datasource-micrometer-opentelemetryの説明を追加。

Datasource Micrometer (ドキュメントリンク) は、JDBC の Proxy であり、JDBC の操作を Micrometer の Observation API で計測して、Observability を高められます。Trace、Metrics だけでなく、SQL ログやスローログも取得可能です。現在は Micrometer とは独立した開発になっていますが、Micrometer と近い位置で開発されています。

Datasource Micrometer は 2025 年末あたりから Spring Initializr からも選択可能になり、便利に利用できるようになりました。Datasource Micrometer 1 系は Spring Boot 3.5 系で、Datasource Micrometer 2 系は Spring Boot 4 系で利用可能です。
image

題材アプリとして、簡単な Counter API を作成します。次のコマンドでプロジェクトの雛形を作成します。

curl -s https://start.spring.io/starter.tgz \
       -d artifactId=counter-api\
       -d name=counter-api \
       -d baseDir=counter-api \
       -d packageName=com.example \
       -d dependencies=web,jdbc,postgresql,actuator,configuration-processor,opentelemetry,datasource-micrometer,testcontainers \
       -d type=maven-project \
       -d applicationName=CounterApiApplication | tar -xzvf -
cd counter-api

すでに Datasource Micrometer の dependency は設定済みです。dependenciesdatasource-micrometerを含めない場合に比べて、以下の設定が追加されます。

30a31
> 		<datasource-micrometer.version>2.1.0</datasource-micrometer.version>
48a50,57
> 		<dependency>
> 			<groupId>net.ttddyy.observation</groupId>
> 			<artifactId>datasource-micrometer-opentelemetry</artifactId>
> 		</dependency>
> 		<dependency>
> 			<groupId>net.ttddyy.observation</groupId>
> 			<artifactId>datasource-micrometer-spring-boot</artifactId>
> 		</dependency>
95a105,115
> 	<dependencyManagement>
> 		<dependencies>
> 			<dependency>
> 				<groupId>net.ttddyy.observation</groupId>
> 				<artifactId>datasource-micrometer-bom</artifactId>
> 				<version>${datasource-micrometer.version}</version>
> 				<type>pom</type>
> 				<scope>import</scope>
> 			</dependency>
> 		</dependencies>
> 	</dependencyManagement>

dependenciesopentelemetryも含まれる場合は、datasource-micrometer-opentelemetryも合わせて追加されます。

せっかくなので こちらの記事 で紹介した、OpenTelemetry Logback Appender の AutoConfiguration も追加します。

<dependency>
    <groupId>am.ik.spring.opentelemetry</groupId>
    <artifactId>otel-logs-autoconfigure</artifactId>
    <version>0.5.0</version>
</dependency>

簡単な API を実装します。

cat <<EOF > src/main/java/com/example/CounterController.java
package com.example;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CounterController {

    private final JdbcClient jdbcClient;

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    public CounterController(JdbcClient jdbcClient) {
        this.jdbcClient = jdbcClient;
    }

    @PostMapping(path = "/counter")
    @Transactional
    public CounterResponse increment(@RequestBody CounterRequest request) {
        CounterResponse counterResponse = this.jdbcClient.sql("""
                INSERT INTO counters (entry_id, counter)
                VALUES (?, 1)
                ON CONFLICT (entry_id)
                DO UPDATE
                  SET counter = counters.counter + 1
                RETURNING entry_id, counter
                """).param(request.entryId()).query(CounterResponse.class).single();
        logger.atInfo()
            .addKeyValue("entryId", counterResponse.entryId())
            .addKeyValue("counter", counterResponse.counter())
            .log("event=increment entryId={} counter={}", counterResponse.entryId(), counterResponse.counter());
        return counterResponse;
    }

    @GetMapping(path = "/counter")
    public List<CounterResponse> getAll() {
        return this.jdbcClient.sql("""
                SELECT entry_id, counter FROM counters ORDER BY counter DESC
                """).query(CounterResponse.class).list();
    }

    public record CounterRequest(int entryId) {
    }

    public record CounterResponse(int entryId, long counter) {
    }

}
EOF

application.properties を設定します。Datasource Micrometer による SQL ログとスロークエリログの設定を行います。

cat <<EOF >> src/main/resources/application.properties
jdbc.datasource-proxy.json-format=true
jdbc.datasource-proxy.logging=slf4j
jdbc.datasource-proxy.multiline=false
jdbc.datasource-proxy.query.enable-logging=true
jdbc.datasource-proxy.slow-query.enable-logging=true
jdbc.datasource-proxy.slow-query.threshold=5
logging.level.net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener=debug
management.opentelemetry.instrumentation.logback-appender.capture-experimental-attributes=true
management.opentelemetry.instrumentation.logback-appender.capture-key-value-pair-attributes=true
management.otlp.metrics.export.base-time-unit=seconds
management.otlp.metrics.export.step=30s
management.tracing.sampling.probability=1.0
spring.sql.init.mode=always
EOF

簡単なスキーマを定義します。

cat <<EOF > src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS counters
(
    entry_id BIGINT PRIMARY KEY,
    counter  BIGINT NOT NULL
);
EOF

次のコマンドを実行すると、Testcontainers を使った PostgreSQL と LGTM スタック のローカル開発用コンテナが立ち上がります (src/test/java/com/example/TestcontainersConfiguration.java を確認してみてください)。DataSource の設定や OTLP エンドポイントの設定は自動で行われます。

./mvnw spring-boot:test-run

起動時に次のようなログが出力され、Grafana の URL がわかります。

2026-01-26T13:08:41.512+09:00  INFO 28654 --- [counter-api] [           main] [                                                 ] tc.grafana/otel-lgtm:latest              : Creating container for image: grafana/otel-lgtm:latest
2026-01-26T13:08:41.579+09:00  INFO 28654 --- [counter-api] [           main] [                                                 ] tc.grafana/otel-lgtm:latest              : Container grafana/otel-lgtm:latest is starting: e0bb34b52593b45152a1236b6766c8ba2b1d3949b5ade2f4126dfbe8bf681790
2026-01-26T13:08:46.762+09:00  INFO 28654 --- [counter-api] [           main] [                                                 ] tc.grafana/otel-lgtm:latest              : Container grafana/otel-lgtm:latest started in PT5.249929S
2026-01-26T13:08:46.762+09:00  INFO 28654 --- [counter-api] [           main] [                                                 ] o.t.grafana.LgtmStackContainer           : Access to the Grafana dashboard: http://localhost:35507
2026-01-26T13:08:46.862+09:00  INFO 28654 --- [counter-api] [           main] [                                                 ] i.m.c.instrument.push.PushMeterRegistry  : Publishing metrics for OtlpMeterRegistry every 30s to http://localhost:35511/v1/metrics with resource attributes {service.name=counter-api}

適当にリクエストを送ります。

curl -s http://localhost:8080/counter --json '{"entryId":100}'
curl -s http://localhost:8080/counter 

次のような SQL ログを確認できます。

2026-01-26T14:11:44.165+09:00 DEBUG 28654 --- [counter-api] [nio-8080-exec-1] [382b35f274086c23f3a45d4c94cfb785-6c507dd2b93e2c20] n.t.d.l.l.SLF4JQueryLoggingListener      : {"name":"test", "connection":4, "time":2, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["INSERT INTO counters (entry_id, counter)\nVALUES (?, 1)\nON CONFLICT (entry_id)\nDO UPDATE\n  SET counter = counters.counter + 1\nRETURNING entry_id, counter\n"], "params":[["100"]]}
2026-01-26T14:11:44.169+09:00  INFO 28654 --- [counter-api] [nio-8080-exec-1] [382b35f274086c23f3a45d4c94cfb785-25067aeeaad66229] com.example.CounterController            : event=increment entryId=100 counter=1
2026-01-26T14:11:45.923+09:00 DEBUG 28654 --- [counter-api] [nio-8080-exec-3] [1a250337750c5fbed850028f8fe5c636-72c7abbd0c14e831] n.t.d.l.l.SLF4JQueryLoggingListener      : {"name":"test", "connection":5, "time":0, "success":true, "type":"Prepared", "batch":false, "querySize":1, "batchSize":0, "query":["SELECT entry_id, counter FROM counters ORDER BY counter DESC\n"], "params":[[]]}

次に、以下のコマンドで vegeta を使い、負荷をかけてみます。

for round in $(seq 20); do
  echo "=== Round $round/20 ==="
  for i in $(seq 3000); do
    id=$((RANDOM % 50 + 1))
    if [ $((RANDOM % 3)) -eq 0 ]; then
      echo '{"method":"GET","url":"http://localhost:8080/counter"}'
    else
      echo '{"method":"POST","url":"http://localhost:8080/counter","header":{"Content-Type":["application/json"]},"body":"'$(echo -n "{\"entryId\":$id}" | base64)'"}'
    fi
  done | vegeta attack -rate=100 -duration=30s -format=json | vegeta report
done

Grafanaで確認

Grafana にアクセスします。Grafana の URL はログに出力されています。

image

Traces

左のメニューの Drilldown から "Traces" を選択します。

image

Span Rate の ◇ マーク (Exemplar、代表点) をクリックし、"View trace" リンクをクリックします。

image

そのリクエストの Trace View が表示されます。

image

"connection" Span をクリックすると、commitのタイミング/回数や時間を確認することができます。

image

"INSERT ..." Span をクリックすると、そのタイミングでの SQL を確認することができます。

image

datasource-micrometer-opentelemetryが追加されることにより、Attribute名は OpenTelemetry semantic conventions に準拠するようになりました。

"Log" ボタンをクリックしてみます。
image

この Trace におけるログを確認することができます。

image

Metrics

次に、左のメニューの Drilldown から "Metrics" を選択します。"Prefix filters""jdbc" を選択すると、表示を Datasource Micrometer が送信したメトリクスのみに絞ることができます。

image

取得できるメトリクスは こちらのドキュメント を参照してください。Connection の開始から終了までの時間、コミット・ロールバックの回数、クエリの実行時間、回数などが取得できます。

Logs

最後に、左のメニューの Drilldown から "Logs" を選択します。

image

"counter-api""Show logs" ボタンをクリックします。

image

特定のログをクリックし、"Links""Trace" ボタンをクリックすると、

image

また、Trace View に飛ぶことができ、このログに関する SQL などを確認することもできます。

image


Datasource Micrometer を使い、Spring Boot アプリの JDBC 操作を計測する方法を紹介しました。Spring Initializr から利用できるので、簡単に組み込むことができます。ぜひ、使ってみてください。

Found a mistake? Update the entry.
Share this article: