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());