diff --git a/MICROMETER_INTEGRATION_PLAN.md b/MICROMETER_INTEGRATION_PLAN.md
new file mode 100644
index 00000000..da2481b8
--- /dev/null
+++ b/MICROMETER_INTEGRATION_PLAN.md
@@ -0,0 +1,249 @@
+# Micrometer統合計画書
+
+## 概要
+
+UroboroSQLでMicrometerを使用してSQL発行やSQL処理時間をメトリクスとして収集できるようにする改修計画です。
+
+## 目的
+
+- SQL実行回数の計測
+- SQL実行時間の計測
+- SQLの種類(SELECT/UPDATE/INSERT/DELETE等)別のメトリクス収集
+- Micrometerを通じて様々なモニタリングシステム(Prometheus、Datadog、CloudWatch等)へのメトリクス送信を可能にする
+
+## 設計方針
+
+### 1. アーキテクチャ
+
+UroboroSQLは既存のイベントサブスクライバー機構を持っているため、この仕組みを活用します:
+
+- **EventSubscriber**: UroboroSQLの既存イベント機構
+- **AfterSqlQueryEvent/AfterSqlUpdateEvent/AfterSqlBatchEvent等**: SQL実行後のイベント
+- **ExecutionContext**: SQL実行コンテキスト(SQL名、SQLの種類等の情報を保持)
+
+### 2. 実装アプローチ
+
+#### 2.1 依存関係の追加
+
+`pom.xml`にMicrometerの依存を追加(optional):
+- `io.micrometer:micrometer-core` - Micrometerのコアライブラリ
+
+#### 2.2 MicrometerEventSubscriberの実装
+
+新しいイベントサブスクライバー `MicrometerEventSubscriber` を作成:
+
+**場所**: `src/main/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriber.java`
+
+**主な機能**:
+- `MeterRegistry`を保持(Micrometerのメトリクス登録先)
+- 以下のイベントリスナーを実装:
+ - `afterSqlQueryListener` - SELECT文実行後
+ - `afterSqlUpdateListener` - UPDATE/INSERT/DELETE文実行後
+ - `afterSqlBatchListener` - バッチ実行後
+ - `afterProcedureListener` - ストアドプロシージャ実行後
+
+**収集するメトリクス**:
+
+1. **実行回数カウンター** (`Counter`)
+ - メトリクス名: `uroborosql.sql.executions`
+ - タグ:
+ - `sql.kind`: SQL種別(`ExecutionContext.getSqlKind()`で取得した`SqlKind` Enumの値を使用)
+ - `sql.name`: SQL名(オプション、設定可能)
+ - `sql.id`: SQL-ID(オプション、設定可能)
+
+2. **実行時間タイマー** (`Timer`)
+ - メトリクス名: `uroborosql.sql.duration`
+ - タグ:
+ - `sql.kind`: SQL種別(`ExecutionContext.getSqlKind()`で取得した`SqlKind` Enumの値を使用)
+ - `sql.name`: SQL名(オプション、設定可能)
+ - `sql.id`: SQL-ID(オプション、設定可能)
+ - 統計情報: 合計時間、カウント、最大値、パーセンタイル等
+
+3. **処理行数ゲージ/サマリー** (`DistributionSummary`)
+ - メトリクス名: `uroborosql.sql.rows`
+ - タグ:
+ - `sql.kind`: SQL種別(`ExecutionContext.getSqlKind()`で取得した`SqlKind` Enumの値を使用)
+ - `sql.name`: SQL名(オプション、設定可能)
+ - `sql.id`: SQL-ID(オプション、設定可能)
+
+#### 2.3 実行時間の計測
+
+**採用方針**: ExecutionContextに実行時間を記録(オプション1を採用)
+
+既存の`SqlAgentImpl`の実装では、パフォーマンスログ用に実行時間の計測を行っている:
+
+```java
+// SqlAgentImpl#query等のメソッド内
+var startTime = PERFORMANCE_LOG.isDebugEnabled() ? Instant.now(getSqlConfig().getClock()) : null;
+try {
+ // SQL実行
+} finally {
+ debugWith(PERFORMANCE_LOG)
+ .addArgument(() -> formatElapsedTime(startTime, Instant.now(getSqlConfig().getClock())))
+ .log();
+}
+```
+
+この既存の仕組みを活用し、以下の対応を実施:
+
+1. **ExecutionContextの拡張**
+ - `ExecutionContext`に`startTime`フィールドと`endTime`フィールドを追加
+ - `getExecutionTime()`メソッドを追加(Duration型を返す)
+ - `setStartTime(Instant)`/`setEndTime(Instant)`メソッドを追加
+
+2. **SqlAgentImpl内での時刻設定**
+ - 既存のパフォーマンスログ用の`startTime`取得時に、同時に`ExecutionContext`にも設定
+ - finally句での処理時に`endTime`を設定
+ - この変更により、`PERFORMANCE_LOG.isDebugEnabled()`に関係なく、常に実行時間を取得可能にする
+
+3. **Micrometer統合での利用**
+ - イベントリスナー内で`ExecutionContext.getExecutionTime()`を呼び出して実行時間を取得
+ - Timerメトリクスに記録
+
+**メリット**:
+- 既存のイベントクラスの変更が不要
+- ExecutionContextは既に各種情報を保持する設計になっている
+- 既存のパフォーマンスログの仕組みを活用できる
+
+#### 2.4 設定オプション
+
+`MicrometerEventSubscriber`のコンストラクタまたはセッターで以下を設定可能に:
+
+- `meterRegistry`: 使用するMeterRegistry(必須)
+- `includeQueryExecutionTime`: クエリ実行時間を計測するか(デフォルト: true)
+- `includeSqlNameTag`: SQL名をタグに含めるか(デフォルト: false、カーディナリティ対策)
+- `includeSqlIdTag`: SQL-IDをタグに含めるか(デフォルト: false、カーディナリティ対策)
+- `includeRowCount`: 処理行数を計測するか(デフォルト: true)
+
+## 実装計画
+
+### Phase 1: 基本実装
+
+1. ✅ 現状調査とアーキテクチャ理解
+2. `pom.xml`にMicrometer依存を追加
+3. `ExecutionContext`に実行時間計測機能を追加
+ - 開始時刻フィールドの追加
+ - 実行時間取得メソッドの追加
+4. SQL実行箇所で開始時刻の記録を追加
+5. `MicrometerEventSubscriber`の実装
+ - 基本的なカウンターとタイマーの実装
+ - SQL種別ごとのメトリクス記録
+6. ライセンスヘッダーの追加(`mvn license:format`)
+
+### Phase 2: テスト実装
+
+7. `MicrometerEventSubscriberTest`の作成
+ - MeterRegistryのモック/SimpleMeterRegistryを使用
+ - 各SQL種別でメトリクスが正しく記録されることを確認
+ - タグが正しく設定されることを確認
+ - 実行時間が計測されることを確認
+8. 統合テスト
+ - 既存のテストが壊れていないことを確認
+
+### Phase 3: ドキュメント整備
+
+9. README.mdまたは別ドキュメントに使用例を追加
+10. Javadocの整備
+
+## 使用例
+
+```java
+// Micrometer MeterRegistryの作成(例:Prometheus)
+MeterRegistry registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
+
+// UroboroSQL設定
+SqlConfig config = UroboroSQL.builder("jdbc:h2:mem:test", "sa", "")
+ .build();
+
+// MicrometerEventSubscriberを追加
+MicrometerEventSubscriber micrometerSubscriber = new MicrometerEventSubscriber(registry)
+ .setIncludeSqlNameTag(true) // SQL名をタグに含める(オプション)
+ .setIncludeSqlIdTag(false); // SQL-IDはタグに含めない(デフォルト)
+
+config.getEventListenerHolder().addEventSubscriber(micrometerSubscriber);
+
+// SQL実行
+try (SqlAgent agent = config.agent()) {
+ agent.query("example/select_product")
+ .param("product_id", 1)
+ .collect();
+}
+
+// メトリクスの確認
+// registry.counter("uroborosql.sql.executions", "sql.kind", "SELECT").count()
+// registry.timer("uroborosql.sql.duration", "sql.kind", "SELECT").mean()
+```
+
+## セキュリティ・パフォーマンス考慮事項
+
+### カーディナリティ問題
+- デフォルトではSQL名やSQL-IDをタグに含めない
+- これらのタグは動的に増加する可能性があるため、明示的に有効化が必要
+- 代わりに`sql.kind`(`SqlKind` Enumの値)という限定的なタグのみをデフォルトで使用
+
+### パフォーマンス影響
+- メトリクス記録は非同期または軽量な操作のみ
+- Micrometerのライブラリはoptional依存として、使用しない場合は影響なし
+- イベントリスナーの追加/削除は動的に可能
+
+### エラーハンドリング
+- メトリクス記録の失敗がSQL実行に影響を与えないよう、try-catchで保護
+
+## 技術的な詳細
+
+### Micrometerとは
+- Java用の計測ファサードライブラリ
+- SLF4Jのメトリクス版
+- 様々なモニタリングシステムへの統一的なインターフェース提供
+- Spring Boot Actuatorでも採用されている標準的なライブラリ
+
+### サポートされるメトリクスシステム
+- Prometheus
+- Datadog
+- New Relic
+- CloudWatch
+- Graphite
+- InfluxDB
+- その他多数
+
+### メトリクスの種類
+- **Counter**: 増加のみ可能なカウンター(実行回数等)
+- **Timer**: 実行時間の計測と統計
+- **Gauge**: 現在の値(接続数等、今回は使用しない見込み)
+- **DistributionSummary**: 分布の統計(処理行数等)
+
+## リスク・制約事項
+
+1. **既存コードへの影響**
+ - ExecutionContextのインターフェース変更が必要
+ - 後方互換性に注意
+
+2. **依存ライブラリの追加**
+ - Micrometerをoptional依存として追加
+ - ライブラリサイズとライセンスの確認が必要
+
+3. **テストの複雑性**
+ - 時間計測のテストは環境依存の可能性
+ - モックやテスト用のMeterRegistryを使用
+
+## 代替案の検討
+
+### 代替案1: Spring Boot Actuatorのみサポート
+- **メリット**: Spring環境での統合が簡単
+- **デメリット**: Spring以外の環境で使用できない
+
+### 代替案2: 独自のメトリクスインターフェース
+- **メリット**: 依存ライブラリなし
+- **デメリット**: 標準的でない、既存のツールとの統合が困難
+
+### 採用案: Micrometer直接サポート(推奨)
+- **メリット**:
+ - 業界標準
+ - 多様なバックエンドサポート
+ - Spring Bootとも統合可能
+- **デメリット**:
+ - 新しい依存ライブラリの追加
+
+## まとめ
+
+既存のEventSubscriber機構を活用し、Micrometerを統合することで、最小限の変更でメトリクス収集機能を追加できます。optional依存とすることで、既存のユーザーには影響を与えず、必要なユーザーのみが利用できる設計とします。
diff --git a/pom.xml b/pom.xml
index 444129f1..41fc268b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -481,6 +481,12 @@ LICENSE file in the root directory of this source tree.
+
+ io.micrometer
+ micrometer-core
+ ${micrometer.version}
+ true
+
org.junit.jupiter
@@ -572,6 +578,7 @@ LICENSE file in the root directory of this source tree.
2.4.0
2.0.6
1.4.5
+ 1.11.5
5.9.1
2.2
1.20.5
diff --git a/src/main/java/jp/co/future/uroborosql/SqlAgentImpl.java b/src/main/java/jp/co/future/uroborosql/SqlAgentImpl.java
index 65c0ee69..f9477df3 100644
--- a/src/main/java/jp/co/future/uroborosql/SqlAgentImpl.java
+++ b/src/main/java/jp/co/future/uroborosql/SqlAgentImpl.java
@@ -1391,7 +1391,8 @@ public ResultSet query(final ExecutionContext executionContext) throws SQLExcept
.setMessage("Execute query sql. sqlName: {}")
.addArgument(executionContext.getSqlName())
.log();
- var startTime = PERFORMANCE_LOG.isDebugEnabled() ? Instant.now(getSqlConfig().getClock()) : null;
+ var startTime = Instant.now(getSqlConfig().getClock());
+ executionContext.setStartTime(startTime);
try {
// デフォルト最大リトライ回数を取得し、個別指定(ExecutionContextの値)があれば上書き
@@ -1413,6 +1414,8 @@ public ResultSet query(final ExecutionContext executionContext) throws SQLExcept
setSavepoint(RETRY_SAVEPOINT_NAME);
}
rs = stmt.executeQuery();
+ // Query実行後の終了時刻を記録
+ executionContext.setEndTime(Instant.now(getSqlConfig().getClock()));
// Query実行後イベント発行
if (getSqlConfig().getEventListenerHolder().hasAfterSqlQueryListener()) {
var eventObj = new AfterSqlQueryEvent(executionContext, rs, stmt.getConnection(), stmt);
@@ -1471,11 +1474,13 @@ public ResultSet query(final ExecutionContext executionContext) throws SQLExcept
return null;
} finally {
// 後処理
+ var endTime = Instant.now(getSqlConfig().getClock());
+ executionContext.setEndTime(endTime);
debugWith(PERFORMANCE_LOG)
.setMessage("SQL execution time [{}({})] : [{}]")
.addArgument(() -> generateSqlName(executionContext))
.addArgument(executionContext.getSqlKind())
- .addArgument(() -> formatElapsedTime(startTime, Instant.now(getSqlConfig().getClock())))
+ .addArgument(() -> formatElapsedTime(startTime, endTime))
.log();
}
}
@@ -1534,7 +1539,8 @@ public int update(final ExecutionContext executionContext) throws SQLException {
return executionContext.getUpdateDelegate().apply(executionContext);
}
- Instant startTime = null;
+ Instant startTime = Instant.now(getSqlConfig().getClock());
+ executionContext.setStartTime(startTime);
try (var stmt = getPreparedStatement(executionContext)) {
@@ -1546,10 +1552,6 @@ public int update(final ExecutionContext executionContext) throws SQLException {
.addArgument(executionContext.getSqlName())
.log();
- if (PERFORMANCE_LOG.isDebugEnabled()) {
- startTime = Instant.now(getSqlConfig().getClock());
- }
-
// デフォルト最大リトライ回数を取得し、個別指定(ExecutionContextの値)があれば上書き
var maxRetryCount = executionContext.getMaxRetryCount() >= 0
? executionContext.getMaxRetryCount()
@@ -1566,6 +1568,8 @@ public int update(final ExecutionContext executionContext) throws SQLException {
setSavepoint(RETRY_SAVEPOINT_NAME);
}
var count = stmt.executeUpdate();
+ // Update実行後の終了時刻を記録
+ executionContext.setEndTime(Instant.now(getSqlConfig().getClock()));
// Update実行後イベント発行
if (getSqlConfig().getEventListenerHolder().hasAfterSqlUpdateListener()) {
var eventObj = new AfterSqlUpdateEvent(executionContext, count, stmt.getConnection(), stmt);
@@ -1630,12 +1634,13 @@ public int update(final ExecutionContext executionContext) throws SQLException {
return 0;
} finally {
// 後処理
- var curStartTime = startTime;
+ var endTime = Instant.now(getSqlConfig().getClock());
+ executionContext.setEndTime(endTime);
debugWith(PERFORMANCE_LOG)
.setMessage("SQL execution time [{}({})] : [{}]")
.addArgument(() -> generateSqlName(executionContext))
.addArgument(executionContext.getSqlKind())
- .addArgument(() -> formatElapsedTime(curStartTime, Instant.now(getSqlConfig().getClock())))
+ .addArgument(() -> formatElapsedTime(startTime, endTime))
.log();
}
}
@@ -1663,7 +1668,8 @@ public int[] batch(final ExecutionContext executionContext) throws SQLException
return new int[] { executionContext.getUpdateDelegate().apply(executionContext) };
}
- Instant startTime = null;
+ Instant startTime = Instant.now(getSqlConfig().getClock());
+ executionContext.setStartTime(startTime);
try (var stmt = getPreparedStatement(executionContext)) {
@@ -1674,9 +1680,6 @@ public int[] batch(final ExecutionContext executionContext) throws SQLException
.setMessage("Execute batch sql. sqlName: {}")
.addArgument(executionContext.getSqlName())
.log();
- if (PERFORMANCE_LOG.isDebugEnabled()) {
- startTime = Instant.now(getSqlConfig().getClock());
- }
// デフォルト最大リトライ回数を取得し、個別指定(ExecutionContextの値)があれば上書き
var maxRetryCount = executionContext.getMaxRetryCount() >= 0
@@ -1694,6 +1697,8 @@ public int[] batch(final ExecutionContext executionContext) throws SQLException
setSavepoint(RETRY_SAVEPOINT_NAME);
}
var counts = stmt.executeBatch();
+ // Batch実行後の終了時刻を記録
+ executionContext.setEndTime(Instant.now(getSqlConfig().getClock()));
// Batch実行後イベント発行
if (getSqlConfig().getEventListenerHolder().hasAfterSqlBatchListener()) {
var eventObj = new AfterSqlBatchEvent(executionContext, counts, stmt.getConnection(), stmt);
@@ -1757,12 +1762,13 @@ public int[] batch(final ExecutionContext executionContext) throws SQLException
return null;
} finally {
// 後処理
- var curStartTime = startTime;
+ var endTime = Instant.now(getSqlConfig().getClock());
+ executionContext.setEndTime(endTime);
debugWith(PERFORMANCE_LOG)
.setMessage("SQL execution time [{}({})] : [{}]")
.addArgument(() -> generateSqlName(executionContext))
.addArgument(executionContext.getSqlKind())
- .addArgument(() -> formatElapsedTime(curStartTime, Instant.now(getSqlConfig().getClock())))
+ .addArgument(() -> formatElapsedTime(startTime, endTime))
.log();
releaseParameterLogging();
}
@@ -1785,7 +1791,8 @@ public Map procedure(final ExecutionContext executionContext) th
// コンテキスト変換
transformContext(executionContext);
- Instant startTime = null;
+ Instant startTime = Instant.now(getSqlConfig().getClock());
+ executionContext.setStartTime(startTime);
try (var callableStatement = getCallableStatement(executionContext)) {
@@ -1796,9 +1803,6 @@ public Map procedure(final ExecutionContext executionContext) th
.setMessage("Execute stored procedure. sqlName: {}")
.addArgument(executionContext.getSqlName())
.log();
- if (PERFORMANCE_LOG.isDebugEnabled()) {
- startTime = Instant.now(getSqlConfig().getClock());
- }
// デフォルト最大リトライ回数を取得し、個別指定(ExecutionContextの値)があれば上書き
var maxRetryCount = executionContext.getMaxRetryCount() >= 0
@@ -1817,6 +1821,8 @@ public Map procedure(final ExecutionContext executionContext) th
setSavepoint(RETRY_SAVEPOINT_NAME);
}
var result = callableStatement.execute();
+ // Procedure実行後の終了時刻を記録
+ executionContext.setEndTime(Instant.now(getSqlConfig().getClock()));
// Procedure実行後イベント発行
if (getSqlConfig().getEventListenerHolder().hasAfterProcedureListener()) {
var eventObj = new AfterProcedureEvent(executionContext, result,
@@ -1867,12 +1873,13 @@ public Map procedure(final ExecutionContext executionContext) th
handleException(executionContext, ex);
} finally {
// 後処理
- var curStartTime = startTime;
+ var endTime = Instant.now(getSqlConfig().getClock());
+ executionContext.setEndTime(endTime);
debugWith(PERFORMANCE_LOG)
.setMessage("Stored procedure execution time [{}({})] : [{}]")
.addArgument(() -> generateSqlName(executionContext))
.addArgument(executionContext.getSqlKind())
- .addArgument(() -> formatElapsedTime(curStartTime, Instant.now(getSqlConfig().getClock())))
+ .addArgument(() -> formatElapsedTime(startTime, endTime))
.log();
}
return null;
diff --git a/src/main/java/jp/co/future/uroborosql/context/ExecutionContext.java b/src/main/java/jp/co/future/uroborosql/context/ExecutionContext.java
index 111cab7c..17c93b99 100644
--- a/src/main/java/jp/co/future/uroborosql/context/ExecutionContext.java
+++ b/src/main/java/jp/co/future/uroborosql/context/ExecutionContext.java
@@ -315,4 +315,56 @@ default Function getUpdateDelegate() {
* @return SqlConfigが保持するClock
*/
Clock getClock();
+
+ /**
+ * SQL実行開始時刻を取得する.
+ *
+ * @return SQL実行開始時刻. 未設定の場合はnull
+ */
+ default java.time.Instant getStartTime() {
+ return null;
+ }
+
+ /**
+ * SQL実行開始時刻を設定する.
+ *
+ * @param startTime SQL実行開始時刻
+ * @return 自身のExecutionContext
+ */
+ default ExecutionContext setStartTime(final java.time.Instant startTime) {
+ return this;
+ }
+
+ /**
+ * SQL実行終了時刻を取得する.
+ *
+ * @return SQL実行終了時刻. 未設定の場合はnull
+ */
+ default java.time.Instant getEndTime() {
+ return null;
+ }
+
+ /**
+ * SQL実行終了時刻を設定する.
+ *
+ * @param endTime SQL実行終了時刻
+ * @return 自身のExecutionContext
+ */
+ default ExecutionContext setEndTime(final java.time.Instant endTime) {
+ return this;
+ }
+
+ /**
+ * SQL実行時間を取得する.
+ *
+ * @return SQL実行時間. 開始時刻または終了時刻が未設定の場合はnull
+ */
+ default java.time.Duration getExecutionTime() {
+ var start = getStartTime();
+ var end = getEndTime();
+ if (start != null && end != null) {
+ return java.time.Duration.between(start, end);
+ }
+ return null;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/jp/co/future/uroborosql/context/ExecutionContextImpl.java b/src/main/java/jp/co/future/uroborosql/context/ExecutionContextImpl.java
index 3cdb9c94..c8145c7a 100644
--- a/src/main/java/jp/co/future/uroborosql/context/ExecutionContextImpl.java
+++ b/src/main/java/jp/co/future/uroborosql/context/ExecutionContextImpl.java
@@ -163,6 +163,12 @@ public boolean contains(final Object o) {
/** 更新処理実行時に通常の更新SQL発行の代わりに移譲する処理. */
private Function updateDelegate;
+ /** SQL実行開始時刻 */
+ private java.time.Instant startTime;
+
+ /** SQL実行終了時刻 */
+ private java.time.Instant endTime;
+
/**
* コンストラクタ。
*/
@@ -1255,4 +1261,46 @@ public Clock getClock() {
return getSqlConfig().getClock();
}
+ /**
+ * {@inheritDoc}
+ *
+ * @see jp.co.future.uroborosql.context.ExecutionContext#getStartTime()
+ */
+ @Override
+ public java.time.Instant getStartTime() {
+ return this.startTime;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see jp.co.future.uroborosql.context.ExecutionContext#setStartTime(java.time.Instant)
+ */
+ @Override
+ public ExecutionContext setStartTime(final java.time.Instant startTime) {
+ this.startTime = startTime;
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see jp.co.future.uroborosql.context.ExecutionContext#getEndTime()
+ */
+ @Override
+ public java.time.Instant getEndTime() {
+ return this.endTime;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see jp.co.future.uroborosql.context.ExecutionContext#setEndTime(java.time.Instant)
+ */
+ @Override
+ public ExecutionContext setEndTime(final java.time.Instant endTime) {
+ this.endTime = endTime;
+ return this;
+ }
+
}
diff --git a/src/main/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriber.java b/src/main/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriber.java
new file mode 100644
index 00000000..e7fc12a5
--- /dev/null
+++ b/src/main/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriber.java
@@ -0,0 +1,267 @@
+/**
+ * Copyright (c) 2017-present, Future Corporation
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+package jp.co.future.uroborosql.event.subscriber;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Objects;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tag;
+import io.micrometer.core.instrument.Tags;
+import jp.co.future.uroborosql.context.ExecutionContext;
+import jp.co.future.uroborosql.enums.SqlKind;
+import jp.co.future.uroborosql.event.AfterProcedureEvent;
+import jp.co.future.uroborosql.event.AfterSqlBatchEvent;
+import jp.co.future.uroborosql.event.AfterSqlQueryEvent;
+import jp.co.future.uroborosql.event.AfterSqlUpdateEvent;
+
+/**
+ * Micrometerを使用してSQLメトリクスを収集するイベントサブスクライバ
+ *
+ * @author H.Sugimoto
+ * @since v1.0.9
+ */
+public class MicrometerEventSubscriber extends EventSubscriber {
+ /** MeterRegistry */
+ private final MeterRegistry meterRegistry;
+
+ /** SQL名をタグに含めるかどうか */
+ private boolean includeSqlNameTag = false;
+
+ /** SQL-IDをタグに含めるかどうか */
+ private boolean includeSqlIdTag = false;
+
+ /** 処理行数を計測するかどうか */
+ private boolean includeRowCount = true;
+
+ /**
+ * コンストラクタ
+ *
+ * @param meterRegistry MeterRegistry
+ */
+ public MicrometerEventSubscriber(final MeterRegistry meterRegistry) {
+ this.meterRegistry = Objects.requireNonNull(meterRegistry, "meterRegistry");
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see jp.co.future.uroborosql.event.subscriber.EventSubscriber#initialize()
+ */
+ @Override
+ public void initialize() {
+ afterSqlQueryListener(this::afterSqlQuery);
+ afterSqlUpdateListener(this::afterSqlUpdate);
+ afterSqlBatchListener(this::afterSqlBatch);
+ afterProcedureListener(this::afterProcedure);
+ }
+
+ /**
+ * Query実行後の処理
+ *
+ * @param evt AfterSqlQueryEvent
+ */
+ void afterSqlQuery(final AfterSqlQueryEvent evt) {
+ try {
+ var executionContext = evt.getExecutionContext();
+ var tags = createTags(executionContext);
+
+ // 実行回数カウンター
+ meterRegistry.counter("uroborosql.sql.executions", tags).increment();
+
+ // 実行時間タイマー
+ var executionTime = executionContext.getExecutionTime();
+ if (executionTime != null) {
+ meterRegistry.timer("uroborosql.sql.duration", tags).record(executionTime);
+ }
+
+ // 処理行数
+ if (includeRowCount) {
+ var rowCount = getRowCount(evt.getResultSet());
+ if (rowCount >= 0) {
+ meterRegistry.summary("uroborosql.sql.rows", tags).record(rowCount);
+ }
+ }
+ } catch (Exception ex) {
+ // メトリクス記録の失敗がSQL実行に影響を与えないよう握りつぶす
+ }
+ }
+
+ /**
+ * Update実行後の処理
+ *
+ * @param evt AfterSqlUpdateEvent
+ */
+ void afterSqlUpdate(final AfterSqlUpdateEvent evt) {
+ try {
+ var executionContext = evt.getExecutionContext();
+ var tags = createTags(executionContext);
+
+ // 実行回数カウンター
+ meterRegistry.counter("uroborosql.sql.executions", tags).increment();
+
+ // 実行時間タイマー
+ var executionTime = executionContext.getExecutionTime();
+ if (executionTime != null) {
+ meterRegistry.timer("uroborosql.sql.duration", tags).record(executionTime);
+ }
+
+ // 処理行数
+ if (includeRowCount) {
+ var rowCount = evt.getCount();
+ meterRegistry.summary("uroborosql.sql.rows", tags).record(rowCount);
+ }
+ } catch (Exception ex) {
+ // メトリクス記録の失敗がSQL実行に影響を与えないよう握りつぶす
+ }
+ }
+
+ /**
+ * Batch実行後の処理
+ *
+ * @param evt AfterSqlBatchEvent
+ */
+ void afterSqlBatch(final AfterSqlBatchEvent evt) {
+ try {
+ var executionContext = evt.getExecutionContext();
+ var tags = createTags(executionContext);
+
+ // 実行回数カウンター
+ meterRegistry.counter("uroborosql.sql.executions", tags).increment();
+
+ // 実行時間タイマー
+ var executionTime = executionContext.getExecutionTime();
+ if (executionTime != null) {
+ meterRegistry.timer("uroborosql.sql.duration", tags).record(executionTime);
+ }
+
+ // 処理行数
+ if (includeRowCount) {
+ var counts = evt.getCounts();
+ if (counts != null && counts.length > 0) {
+ var totalCount = 0;
+ for (var count : counts) {
+ totalCount += count;
+ }
+ meterRegistry.summary("uroborosql.sql.rows", tags).record(totalCount);
+ }
+ }
+ } catch (Exception ex) {
+ // メトリクス記録の失敗がSQL実行に影響を与えないよう握りつぶす
+ }
+ }
+
+ /**
+ * Procedure実行後の処理
+ *
+ * @param evt AfterProcedureEvent
+ */
+ void afterProcedure(final AfterProcedureEvent evt) {
+ try {
+ var executionContext = evt.getExecutionContext();
+ var tags = createTags(executionContext);
+
+ // 実行回数カウンター
+ meterRegistry.counter("uroborosql.sql.executions", tags).increment();
+
+ // 実行時間タイマー
+ var executionTime = executionContext.getExecutionTime();
+ if (executionTime != null) {
+ meterRegistry.timer("uroborosql.sql.duration", tags).record(executionTime);
+ }
+ } catch (Exception ex) {
+ // メトリクス記録の失敗がSQL実行に影響を与えないよう握りつぶす
+ }
+ }
+
+ /**
+ * メトリクス用のタグを作成する
+ *
+ * @param executionContext ExecutionContext
+ * @return Tags
+ */
+ private Tags createTags(final ExecutionContext executionContext) {
+ var tags = Tags.of(Tag.of("sql.kind", getSqlKindName(executionContext.getSqlKind())));
+
+ if (includeSqlNameTag && executionContext.getSqlName() != null) {
+ tags = tags.and(Tag.of("sql.name", executionContext.getSqlName()));
+ }
+
+ if (includeSqlIdTag && executionContext.getSqlId() != null) {
+ tags = tags.and(Tag.of("sql.id", executionContext.getSqlId()));
+ }
+
+ return tags;
+ }
+
+ /**
+ * SqlKindから文字列を取得する
+ *
+ * @param sqlKind SqlKind
+ * @return SqlKindの文字列表現
+ */
+ private String getSqlKindName(final SqlKind sqlKind) {
+ return sqlKind != null ? sqlKind.name() : "UNKNOWN";
+ }
+
+ /**
+ * ResultSetから行数を取得する
+ *
+ * @param resultSet ResultSet
+ * @return 行数. 取得できない場合は-1
+ */
+ private int getRowCount(final ResultSet resultSet) {
+ var rowCount = -1;
+ try {
+ // resultSetのカーソル種別を取得
+ // 種別「TYPE_FORWARD_ONLY」の場合、beforeFirstメソッドが効かないため除外
+ if (resultSet.getType() != ResultSet.TYPE_FORWARD_ONLY) {
+ // 件数結果取得
+ resultSet.last();
+ rowCount = resultSet.getRow();
+ resultSet.beforeFirst();
+ }
+ } catch (SQLException ex) {
+ // ここでの例外は実処理に影響を及ぼさないよう握りつぶす
+ }
+ return rowCount;
+ }
+
+ /**
+ * SQL名をタグに含めるかどうかを設定する
+ *
+ * @param includeSqlNameTag SQL名をタグに含めるかどうか
+ * @return MicrometerEventSubscriber
+ */
+ public MicrometerEventSubscriber setIncludeSqlNameTag(final boolean includeSqlNameTag) {
+ this.includeSqlNameTag = includeSqlNameTag;
+ return this;
+ }
+
+ /**
+ * SQL-IDをタグに含めるかどうかを設定する
+ *
+ * @param includeSqlIdTag SQL-IDをタグに含めるかどうか
+ * @return MicrometerEventSubscriber
+ */
+ public MicrometerEventSubscriber setIncludeSqlIdTag(final boolean includeSqlIdTag) {
+ this.includeSqlIdTag = includeSqlIdTag;
+ return this;
+ }
+
+ /**
+ * 処理行数を計測するかどうかを設定する
+ *
+ * @param includeRowCount 処理行数を計測するかどうか
+ * @return MicrometerEventSubscriber
+ */
+ public MicrometerEventSubscriber setIncludeRowCount(final boolean includeRowCount) {
+ this.includeRowCount = includeRowCount;
+ return this;
+ }
+}
diff --git a/src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java b/src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java
new file mode 100644
index 00000000..9ad57531
--- /dev/null
+++ b/src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java
@@ -0,0 +1,248 @@
+package jp.co.future.uroborosql.event.subscriber;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.*;
+
+import java.math.BigDecimal;
+import java.nio.file.Paths;
+import java.sql.ResultSet;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import jp.co.future.uroborosql.AbstractDbTest;
+import jp.co.future.uroborosql.enums.SqlKind;
+
+public class MicrometerEventSubscriberTest extends AbstractDbTest {
+ private MicrometerEventSubscriber eventSubscriber;
+ private MeterRegistry meterRegistry;
+
+ @BeforeEach
+ public void setUpLocal() throws Exception {
+ meterRegistry = new SimpleMeterRegistry();
+ }
+
+ @AfterEach
+ public void tearDownLocal() throws Exception {
+ if (eventSubscriber != null) {
+ config.getEventListenerHolder().removeEventSubscriber(eventSubscriber);
+ }
+ }
+
+ @Test
+ void testExecuteQuery() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv"));
+
+ // Add subscriber after data setup to avoid counting setup operations
+ eventSubscriber = new MicrometerEventSubscriber(meterRegistry);
+ config.getEventListenerHolder().addEventSubscriber(eventSubscriber);
+
+ var ctx = agent.context().setSqlName("example/select_product")
+ .setSqlId("111")
+ .param("product_id", List.of(new BigDecimal("0"), new BigDecimal("2")));
+ ctx.setResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
+
+ agent.query(ctx);
+
+ // 実行回数カウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.SELECT.name());
+ assertThat(counter.count(), is(1.0));
+
+ // 実行時間タイマーを確認
+ var timer = meterRegistry.timer("uroborosql.sql.duration",
+ "sql.kind", SqlKind.SELECT.name());
+ assertThat(timer.count(), is(1L));
+ assertThat(timer.totalTime(java.util.concurrent.TimeUnit.NANOSECONDS) > 0, is(true));
+
+ // 処理行数を確認
+ var summaries = meterRegistry.find("uroborosql.sql.rows")
+ .tag("sql.kind", SqlKind.SELECT.name())
+ .summaries();
+ assertThat(summaries.size(), is(1));
+ assertThat(summaries.iterator().next().count(), is(1L));
+ assertThat(summaries.iterator().next().totalAmount() >= 0, is(true));
+ }
+
+ @Test
+ void testExecuteUpdate() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteUpdate.ltsv"));
+
+ // Add subscriber after data setup to avoid counting setup operations
+ eventSubscriber = new MicrometerEventSubscriber(meterRegistry);
+ config.getEventListenerHolder().addEventSubscriber(eventSubscriber);
+
+ var ctx = agent.context().setSqlName("example/selectinsert_product")
+ .setSqlId("222")
+ .param("product_id", new BigDecimal("0"))
+ .param("jan_code", "1234567890123");
+
+ agent.update(ctx);
+
+ // 実行回数カウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.UPDATE.name());
+ assertThat(counter.count(), is(1.0));
+
+ // 実行時間タイマーを確認
+ var timer = meterRegistry.timer("uroborosql.sql.duration",
+ "sql.kind", SqlKind.UPDATE.name());
+ assertThat(timer.count(), is(1L));
+ assertThat(timer.totalTime(java.util.concurrent.TimeUnit.NANOSECONDS) > 0, is(true));
+
+ // 処理行数を確認
+ var summaries = meterRegistry.find("uroborosql.sql.rows")
+ .tag("sql.kind", SqlKind.UPDATE.name())
+ .summaries();
+ assertThat(summaries.size(), is(1));
+ assertThat(summaries.iterator().next().totalAmount(), is(1.0));
+ }
+
+ @Test
+ void testExecuteBatch() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv"));
+
+// Add subscriber after data setup to avoid counting setup operations
+eventSubscriber = new MicrometerEventSubscriber(meterRegistry);
+config.getEventListenerHolder().addEventSubscriber(eventSubscriber);
+ truncateTable("PRODUCT");
+
+ var ctx = agent.context().setSqlName("example/insert_product");
+ ctx.param("product_id", new BigDecimal("3")).param("product_name", "test3").param("product_kana_name", "test3")
+ .param("jan_code", "1234567890124").param("product_description", "test").param("ins_datetime",
+ java.sql.Timestamp.valueOf("2005-12-12 10:10:10.000000000"))
+ .param("upd_datetime", java.sql.Timestamp.valueOf("2005-12-12 10:10:10.000000000"))
+ .param("version_no", new BigDecimal("1")).addBatch();
+ ctx.param("product_id", new BigDecimal("4")).param("product_name", "test4").param("product_kana_name", "test4")
+ .param("jan_code", "1234567890125").param("product_description", "test").param("ins_datetime",
+ java.sql.Timestamp.valueOf("2005-12-12 10:10:10.000000000"))
+ .param("upd_datetime", java.sql.Timestamp.valueOf("2005-12-12 10:10:10.000000000"))
+ .param("version_no", new BigDecimal("1")).addBatch();
+
+ agent.batch(ctx);
+
+ // 実行回数カウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.BATCH_INSERT.name());
+ assertThat(counter.count(), is(1.0));
+
+ // 実行時間タイマーを確認
+ var timer = meterRegistry.timer("uroborosql.sql.duration",
+ "sql.kind", SqlKind.BATCH_INSERT.name());
+ assertThat(timer.count(), is(1L));
+ assertThat(timer.totalTime(java.util.concurrent.TimeUnit.NANOSECONDS) > 0, is(true));
+
+ // 処理行数を確認
+ var summaries = meterRegistry.find("uroborosql.sql.rows")
+ .tag("sql.kind", SqlKind.BATCH_INSERT.name())
+ .summaries();
+ assertThat(summaries.size(), is(1));
+ assertThat(summaries.iterator().next().totalAmount(), is(2.0));
+ }
+
+ @Test
+ void testWithSqlNameTag() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv"));
+
+// Add subscriber after data setup to avoid counting setup operations
+eventSubscriber = new MicrometerEventSubscriber(meterRegistry);
+config.getEventListenerHolder().addEventSubscriber(eventSubscriber);
+
+ eventSubscriber.setIncludeSqlNameTag(true);
+
+ var ctx = agent.context().setSqlName("example/select_product")
+ .param("product_id", List.of(new BigDecimal("0"), new BigDecimal("2")));
+ ctx.setResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
+
+ agent.query(ctx);
+
+ // SQL名タグを含むカウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.SELECT.name(),
+ "sql.name", "example/select_product");
+ assertThat(counter.count(), is(1.0));
+ }
+
+ @Test
+ void testWithSqlIdTag() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv"));
+
+// Add subscriber after data setup to avoid counting setup operations
+eventSubscriber = new MicrometerEventSubscriber(meterRegistry);
+config.getEventListenerHolder().addEventSubscriber(eventSubscriber);
+
+ eventSubscriber.setIncludeSqlIdTag(true);
+
+ var ctx = agent.context().setSqlName("example/select_product")
+ .setSqlId("test_id_001")
+ .param("product_id", List.of(new BigDecimal("0"), new BigDecimal("2")));
+ ctx.setResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
+
+ agent.query(ctx);
+
+ // SQL-IDタグを含むカウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.SELECT.name(),
+ "sql.id", "test_id_001");
+ assertThat(counter.count(), is(1.0));
+ }
+
+ @Test
+ void testWithRowCountDisabled() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteUpdate.ltsv"));
+
+// Add subscriber after data setup to avoid counting setup operations
+eventSubscriber = new MicrometerEventSubscriber(meterRegistry);
+config.getEventListenerHolder().addEventSubscriber(eventSubscriber);
+
+ eventSubscriber.setIncludeRowCount(false);
+
+ var ctx = agent.context().setSqlName("example/selectinsert_product")
+ .param("product_id", new BigDecimal("0"))
+ .param("jan_code", "1234567890123");
+
+ agent.update(ctx);
+
+ // 実行回数カウンターは確認できる
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.UPDATE.name());
+ assertThat(counter.count(), is(1.0));
+
+ // 処理行数のsummaryは存在しない
+ var summaries = meterRegistry.find("uroborosql.sql.rows")
+ .tag("sql.kind", SqlKind.UPDATE.name())
+ .summaries();
+ assertThat(summaries.size(), is(0));
+ }
+
+ @Test
+ void testMultipleExecutions() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv"));
+
+// Add subscriber after data setup to avoid counting setup operations
+eventSubscriber = new MicrometerEventSubscriber(meterRegistry);
+config.getEventListenerHolder().addEventSubscriber(eventSubscriber);
+
+ // 複数回実行
+ for (var i = 0; i < 3; i++) {
+ var ctx = agent.context().setSqlName("example/select_product")
+ .param("product_id", List.of(new BigDecimal("0"), new BigDecimal("2")));
+ ctx.setResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
+ agent.query(ctx);
+ }
+
+ // 実行回数カウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.SELECT.name());
+ assertThat(counter.count(), is(3.0));
+
+ // 実行時間タイマーを確認
+ var timer = meterRegistry.timer("uroborosql.sql.duration",
+ "sql.kind", SqlKind.SELECT.name());
+ assertThat(timer.count(), is(3L));
+ }
+}
diff --git a/src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java.orig b/src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java.orig
new file mode 100644
index 00000000..4ed82394
--- /dev/null
+++ b/src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java.orig
@@ -0,0 +1,220 @@
+package jp.co.future.uroborosql.event.subscriber;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.*;
+
+import java.math.BigDecimal;
+import java.nio.file.Paths;
+import java.sql.ResultSet;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import jp.co.future.uroborosql.AbstractDbTest;
+import jp.co.future.uroborosql.enums.SqlKind;
+
+public class MicrometerEventSubscriberTest extends AbstractDbTest {
+ private MicrometerEventSubscriber eventSubscriber;
+ private MeterRegistry meterRegistry;
+
+ @BeforeEach
+ public void setUpLocal() throws Exception {
+ meterRegistry = new SimpleMeterRegistry();
+ eventSubscriber = new MicrometerEventSubscriber(meterRegistry);
+ config.getEventListenerHolder().addEventSubscriber(eventSubscriber);
+ }
+
+ @AfterEach
+ public void tearDownLocal() throws Exception {
+ config.getEventListenerHolder().removeEventSubscriber(eventSubscriber);
+ }
+
+ @Test
+ void testExecuteQuery() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv"));
+
+ var ctx = agent.context().setSqlName("example/select_product")
+ .setSqlId("111")
+ .param("product_id", List.of(new BigDecimal("0"), new BigDecimal("2")));
+ ctx.setResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
+
+ agent.query(ctx);
+
+ // 実行回数カウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.SELECT.name());
+ assertThat(counter.count(), is(1.0));
+
+ // 実行時間タイマーを確認
+ var timer = meterRegistry.timer("uroborosql.sql.duration",
+ "sql.kind", SqlKind.SELECT.name());
+ assertThat(timer.count(), is(1L));
+ assertThat(timer.totalTime(java.util.concurrent.TimeUnit.NANOSECONDS) > 0, is(true));
+
+ // 処理行数を確認
+ var summaries = meterRegistry.find("uroborosql.sql.rows")
+ .tag("sql.kind", SqlKind.SELECT.name())
+ .summaries();
+ assertThat(summaries.size(), is(1));
+ assertThat(summaries.iterator().next().count(), is(1L));
+ assertThat(summaries.iterator().next().totalAmount() >= 0, is(true));
+ }
+
+ @Test
+ void testExecuteUpdate() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteUpdate.ltsv"));
+
+ var ctx = agent.context().setSqlName("example/selectinsert_product")
+ .setSqlId("222")
+ .param("product_id", new BigDecimal("0"))
+ .param("jan_code", "1234567890123");
+
+ agent.update(ctx);
+
+ // 実行回数カウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.INSERT.name());
+ assertThat(counter.count(), is(1.0));
+
+ // 実行時間タイマーを確認
+ var timer = meterRegistry.timer("uroborosql.sql.duration",
+ "sql.kind", SqlKind.INSERT.name());
+ assertThat(timer.count(), is(1L));
+ assertThat(timer.totalTime(java.util.concurrent.TimeUnit.NANOSECONDS) > 0, is(true));
+
+ // 処理行数を確認
+ var summaries = meterRegistry.find("uroborosql.sql.rows")
+ .tag("sql.kind", SqlKind.INSERT.name())
+ .summaries();
+ assertThat(summaries.size(), is(1));
+ assertThat(summaries.iterator().next().totalAmount(), is(1.0));
+ }
+
+ @Test
+ void testExecuteBatch() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv"));
+ truncateTable("PRODUCT");
+
+ var ctx = agent.context().setSqlName("example/insert_product");
+ ctx.param("product_id", new BigDecimal("3")).param("product_name", "test3").param("product_kana_name", "test3")
+ .param("jan_code", "1234567890124").param("product_description", "test").param("ins_datetime",
+ java.sql.Timestamp.valueOf("2005-12-12 10:10:10.000000000"))
+ .param("upd_datetime", java.sql.Timestamp.valueOf("2005-12-12 10:10:10.000000000"))
+ .param("version_no", new BigDecimal("1")).addBatch();
+ ctx.param("product_id", new BigDecimal("4")).param("product_name", "test4").param("product_kana_name", "test4")
+ .param("jan_code", "1234567890125").param("product_description", "test").param("ins_datetime",
+ java.sql.Timestamp.valueOf("2005-12-12 10:10:10.000000000"))
+ .param("upd_datetime", java.sql.Timestamp.valueOf("2005-12-12 10:10:10.000000000"))
+ .param("version_no", new BigDecimal("1")).addBatch();
+
+ agent.batch(ctx);
+
+ // 実行回数カウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.BATCH_INSERT.name());
+ assertThat(counter.count(), is(1.0));
+
+ // 実行時間タイマーを確認
+ var timer = meterRegistry.timer("uroborosql.sql.duration",
+ "sql.kind", SqlKind.BATCH_INSERT.name());
+ assertThat(timer.count(), is(1L));
+ assertThat(timer.totalTime(java.util.concurrent.TimeUnit.NANOSECONDS) > 0, is(true));
+
+ // 処理行数を確認
+ var summaries = meterRegistry.find("uroborosql.sql.rows")
+ .tag("sql.kind", SqlKind.BATCH_INSERT.name())
+ .summaries();
+ assertThat(summaries.size(), is(1));
+ assertThat(summaries.iterator().next().totalAmount(), is(2.0));
+ }
+
+ @Test
+ void testWithSqlNameTag() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv"));
+
+ eventSubscriber.setIncludeSqlNameTag(true);
+
+ var ctx = agent.context().setSqlName("example/select_product")
+ .param("product_id", List.of(new BigDecimal("0"), new BigDecimal("2")));
+ ctx.setResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
+
+ agent.query(ctx);
+
+ // SQL名タグを含むカウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.SELECT.name(),
+ "sql.name", "example/select_product");
+ assertThat(counter.count(), is(1.0));
+ }
+
+ @Test
+ void testWithSqlIdTag() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv"));
+
+ eventSubscriber.setIncludeSqlIdTag(true);
+
+ var ctx = agent.context().setSqlName("example/select_product")
+ .setSqlId("test_id_001")
+ .param("product_id", List.of(new BigDecimal("0"), new BigDecimal("2")));
+ ctx.setResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
+
+ agent.query(ctx);
+
+ // SQL-IDタグを含むカウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.SELECT.name(),
+ "sql.id", "test_id_001");
+ assertThat(counter.count(), is(1.0));
+ }
+
+ @Test
+ void testWithRowCountDisabled() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteUpdate.ltsv"));
+
+ eventSubscriber.setIncludeRowCount(false);
+
+ var ctx = agent.context().setSqlName("example/selectinsert_product")
+ .param("product_id", new BigDecimal("0"))
+ .param("jan_code", "1234567890123");
+
+ agent.update(ctx);
+
+ // 実行回数カウンターは確認できる
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.INSERT.name());
+ assertThat(counter.count(), is(1.0));
+
+ // 処理行数のsummaryは存在しない
+ var summaries = meterRegistry.find("uroborosql.sql.rows")
+ .tag("sql.kind", SqlKind.INSERT.name())
+ .summaries();
+ assertThat(summaries.size(), is(0));
+ }
+
+ @Test
+ void testMultipleExecutions() throws Exception {
+ cleanInsert(Paths.get("src/test/resources/data/setup", "testExecuteQuery.ltsv"));
+
+ // 複数回実行
+ for (var i = 0; i < 3; i++) {
+ var ctx = agent.context().setSqlName("example/select_product")
+ .param("product_id", List.of(new BigDecimal("0"), new BigDecimal("2")));
+ ctx.setResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
+ agent.query(ctx);
+ }
+
+ // 実行回数カウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.SELECT.name());
+ assertThat(counter.count(), is(3.0));
+
+ // 実行時間タイマーを確認
+ var timer = meterRegistry.timer("uroborosql.sql.duration",
+ "sql.kind", SqlKind.SELECT.name());
+ assertThat(timer.count(), is(3L));
+ }
+}
diff --git a/src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java.rej b/src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java.rej
new file mode 100644
index 00000000..c974b801
--- /dev/null
+++ b/src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java.rej
@@ -0,0 +1,15 @@
+--- src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java
++++ src/test/java/jp/co/future/uroborosql/event/subscriber/MicrometerEventSubscriberTest.java
+@@ -75,6 +75,12 @@ public class MicrometerEventSubscriberTest extends AbstractDbTest {
+
+ agent.update(ctx);
+
++// Debug: List all meters
++System.out.println("All meters after update:");
++meterRegistry.getMeters().forEach(m -> {
++System.out.println(" " + m.getId().getName() + " tags: " + m.getId().getTags());
++});
++
+ // 実行回数カウンターを確認
+ var counter = meterRegistry.counter("uroborosql.sql.executions",
+ "sql.kind", SqlKind.INSERT.name());