From d44f2fcb8f94d7d06c6f1bad4d2a6a6a566bd80a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:17:32 +0000 Subject: [PATCH 1/2] Initial plan From 060ca01c2fe704e80b732b9b206adfdc5b722145 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:38:16 +0000 Subject: [PATCH 2/2] Add Micrometer metrics integration to OJP JDBC driver and Spring Boot starter Co-authored-by: rrobetti <7221783+rrobetti@users.noreply.github.com> --- .../grpc/client/OjpMetricsIterator.java | 72 +++++++++ .../client/StatementServiceGrpcClient.java | 12 +- .../java/org/openjproxy/jdbc/Connection.java | 1 + .../main/java/org/openjproxy/jdbc/Driver.java | 2 + .../openjproxy/jdbc/NoOpOjpDriverMetrics.java | 36 +++++ .../org/openjproxy/jdbc/OjpDriverMetrics.java | 41 +++++ .../jdbc/OjpDriverMetricsHolder.java | 59 +++++++ .../grpc/client/OjpMetricsIteratorTest.java | 120 ++++++++++++++ pom.xml | 1 + spring-boot-starter-ojp/pom.xml | 8 + .../OjpMicrometerAutoConfiguration.java | 75 +++++++++ .../OjpMicrometerDriverMetrics.java | 104 ++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../OjpMicrometerAutoConfigurationTest.java | 149 ++++++++++++++++++ .../OjpMicrometerDriverMetricsTest.java | 106 +++++++++++++ 15 files changed, 785 insertions(+), 2 deletions(-) create mode 100644 ojp-jdbc-driver/src/main/java/org/openjproxy/grpc/client/OjpMetricsIterator.java create mode 100644 ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/NoOpOjpDriverMetrics.java create mode 100644 ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/OjpDriverMetrics.java create mode 100644 ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/OjpDriverMetricsHolder.java create mode 100644 ojp-jdbc-driver/src/test/java/org/openjproxy/grpc/client/OjpMetricsIteratorTest.java create mode 100644 spring-boot-starter-ojp/src/main/java/org/openjproxy/autoconfigure/OjpMicrometerAutoConfiguration.java create mode 100644 spring-boot-starter-ojp/src/main/java/org/openjproxy/autoconfigure/OjpMicrometerDriverMetrics.java create mode 100644 spring-boot-starter-ojp/src/test/java/org/openjproxy/autoconfigure/OjpMicrometerAutoConfigurationTest.java create mode 100644 spring-boot-starter-ojp/src/test/java/org/openjproxy/autoconfigure/OjpMicrometerDriverMetricsTest.java diff --git a/ojp-jdbc-driver/src/main/java/org/openjproxy/grpc/client/OjpMetricsIterator.java b/ojp-jdbc-driver/src/main/java/org/openjproxy/grpc/client/OjpMetricsIterator.java new file mode 100644 index 000000000..1d5e39b8e --- /dev/null +++ b/ojp-jdbc-driver/src/main/java/org/openjproxy/grpc/client/OjpMetricsIterator.java @@ -0,0 +1,72 @@ +package org.openjproxy.grpc.client; + +import com.openjproxy.grpc.OpResult; +import org.openjproxy.jdbc.OjpDriverMetricsHolder; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Iterator wrapper that records Micrometer metrics for a streaming gRPC query result. + * + *

For server-streaming RPCs ({@code executeQuery}), the gRPC stub returns an + * {@link Iterator} whose elements arrive lazily over the network. Timing and error + * metrics must therefore be measured across the full lifetime of the iterator, not + * just at the moment the stub call is made.

+ * + *

This wrapper captures the start time at construction and reports the elapsed + * wall-clock time (in milliseconds) to + * {@link OjpDriverMetricsHolder#get()} once the stream is exhausted + * ({@link #hasNext()} returns {@code false}) or a {@link RuntimeException} is thrown + * during iteration. If the iterator is abandoned before exhaustion, the metric will + * not be recorded – a known trade-off documented in the {@code OjpMicrometerDriverMetrics} + * class Javadoc.

+ */ +class OjpMetricsIterator implements Iterator { + + private final Iterator delegate; + private final long startNs; + private boolean completed = false; + + OjpMetricsIterator(Iterator delegate) { + this.delegate = delegate; + this.startNs = System.nanoTime(); + } + + @Override + public boolean hasNext() { + try { + boolean more = delegate.hasNext(); + if (!more && !completed) { + completed = true; + OjpDriverMetricsHolder.get().onStatementExecuted(elapsedMs()); + } + return more; + } catch (RuntimeException e) { + if (!completed) { + completed = true; + OjpDriverMetricsHolder.get().onStatementFailed(); + } + throw e; + } + } + + @Override + public OpResult next() { + try { + return delegate.next(); + } catch (NoSuchElementException e) { + throw e; + } catch (RuntimeException e) { + if (!completed) { + completed = true; + OjpDriverMetricsHolder.get().onStatementFailed(); + } + throw e; + } + } + + private long elapsedMs() { + return (System.nanoTime() - startNs) / 1_000_000L; + } +} diff --git a/ojp-jdbc-driver/src/main/java/org/openjproxy/grpc/client/StatementServiceGrpcClient.java b/ojp-jdbc-driver/src/main/java/org/openjproxy/grpc/client/StatementServiceGrpcClient.java index 934f9c059..397ca7240 100644 --- a/ojp-jdbc-driver/src/main/java/org/openjproxy/grpc/client/StatementServiceGrpcClient.java +++ b/ojp-jdbc-driver/src/main/java/org/openjproxy/grpc/client/StatementServiceGrpcClient.java @@ -25,6 +25,7 @@ import org.openjproxy.grpc.GrpcChannelFactory; import org.openjproxy.jdbc.Connection; import org.openjproxy.jdbc.LobGrpcIterator; +import org.openjproxy.jdbc.OjpDriverMetricsHolder; import java.sql.SQLException; import java.util.Iterator; @@ -99,6 +100,7 @@ public OpResult executeUpdate(SessionInfo sessionInfo, String sql, List params, String statementUUID, Map properties) throws SQLException { + long startNs = System.nanoTime(); try { StatementRequest.Builder builder = StatementRequest.newBuilder() .setSession(sessionInfo) @@ -113,8 +115,11 @@ public OpResult executeUpdate(SessionInfo sessionInfo, String sql, List executeQuery(SessionInfo sessionInfo, String sql, List builder.addAllProperties(propertiesToProto(properties)); } - return this.statemetServiceBlockingStub.executeQuery(builder.build()); + // Wrap the streaming iterator so that metrics are recorded across the full + // result-stream lifetime, not just at RPC initiation. + return new OjpMetricsIterator(this.statemetServiceBlockingStub.executeQuery(builder.build())); } catch (StatusRuntimeException e) { + OjpDriverMetricsHolder.get().onStatementFailed(); throw handle(e); } } diff --git a/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/Connection.java b/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/Connection.java index 7c39db47a..ef6bba133 100644 --- a/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/Connection.java +++ b/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/Connection.java @@ -188,6 +188,7 @@ public void close() throws SQLException { this.session = null; } this.closed = true; + OjpDriverMetricsHolder.get().onConnectionClosed(); } @Override diff --git a/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/Driver.java b/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/Driver.java index 2edf205d7..f6e143104 100644 --- a/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/Driver.java +++ b/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/Driver.java @@ -113,8 +113,10 @@ public java.sql.Connection connect(String url, Properties info) throws SQLExcept sessionInfo.getSessionUUID(), sessionInfo.getConnHash()); } catch (Exception e) { log.error("Failed to establish connection", e); + OjpDriverMetricsHolder.get().onConnectionFailed(); throw e; } + OjpDriverMetricsHolder.get().onConnectionCreated(); log.debug("Returning new Connection with sessionInfo: {}", sessionInfo); return new Connection(sessionInfo, statementService, DatabaseUtils.resolveDbName(cleanUrl)); } diff --git a/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/NoOpOjpDriverMetrics.java b/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/NoOpOjpDriverMetrics.java new file mode 100644 index 000000000..36ab5f537 --- /dev/null +++ b/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/NoOpOjpDriverMetrics.java @@ -0,0 +1,36 @@ +package org.openjproxy.jdbc; + +/** + * No-op implementation of {@link OjpDriverMetrics} that discards all measurements. + * + *

This is the default implementation used when no metrics integration (e.g. Micrometer) + * has been registered via {@link OjpDriverMetricsHolder}. It imposes zero overhead.

+ */ +public final class NoOpOjpDriverMetrics implements OjpDriverMetrics { + + /** Singleton instance. */ + public static final NoOpOjpDriverMetrics INSTANCE = new NoOpOjpDriverMetrics(); + + private NoOpOjpDriverMetrics() { + } + + @Override + public void onConnectionCreated() { + } + + @Override + public void onConnectionFailed() { + } + + @Override + public void onConnectionClosed() { + } + + @Override + public void onStatementExecuted(long durationMs) { + } + + @Override + public void onStatementFailed() { + } +} diff --git a/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/OjpDriverMetrics.java b/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/OjpDriverMetrics.java new file mode 100644 index 000000000..9292a8703 --- /dev/null +++ b/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/OjpDriverMetrics.java @@ -0,0 +1,41 @@ +package org.openjproxy.jdbc; + +/** + * Interface for collecting driver-side metrics for OJP JDBC connections and statement executions. + * + *

This abstraction decouples the JDBC driver from any specific metrics library, + * allowing integrations such as Micrometer (Spring Boot) or no-op (default) to be + * plugged in at runtime via {@link OjpDriverMetricsHolder}.

+ * + *

Implementations must be thread-safe as methods may be called concurrently + * from multiple connections and statement executions.

+ */ +public interface OjpDriverMetrics { + + /** + * Called when a new JDBC connection is successfully established to the OJP server. + */ + void onConnectionCreated(); + + /** + * Called when a JDBC connection attempt fails. + */ + void onConnectionFailed(); + + /** + * Called when a JDBC connection is closed. + */ + void onConnectionClosed(); + + /** + * Called after a SQL statement (query or update) is successfully executed. + * + * @param durationMs the round-trip execution time in milliseconds + */ + void onStatementExecuted(long durationMs); + + /** + * Called when a SQL statement execution fails with an exception. + */ + void onStatementFailed(); +} diff --git a/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/OjpDriverMetricsHolder.java b/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/OjpDriverMetricsHolder.java new file mode 100644 index 000000000..c4f920657 --- /dev/null +++ b/ojp-jdbc-driver/src/main/java/org/openjproxy/jdbc/OjpDriverMetricsHolder.java @@ -0,0 +1,59 @@ +package org.openjproxy.jdbc; + +/** + * Global holder for the active {@link OjpDriverMetrics} implementation. + * + *

The OJP JDBC driver uses this holder as a lightweight service-locator to decouple + * the driver's hot paths from any specific metrics library. By default the holder contains + * {@link NoOpOjpDriverMetrics#INSTANCE}, so the driver incurs no overhead when no metrics + * integration has been registered.

+ * + *

To activate metrics, integrations such as the {@code spring-boot-starter-ojp} Micrometer + * auto-configuration call {@link #set(OjpDriverMetrics)} early in the application lifecycle, + * before the first JDBC connection is created. Example:

+ * + *
+ *   OjpDriverMetricsHolder.set(new OjpMicrometerDriverMetrics(meterRegistry));
+ * 
+ * + *

All methods are thread-safe. The {@code volatile} field ensures that a newly registered + * implementation is immediately visible to all threads.

+ */ +public final class OjpDriverMetricsHolder { + + private static volatile OjpDriverMetrics instance = NoOpOjpDriverMetrics.INSTANCE; + + private OjpDriverMetricsHolder() { + } + + /** + * Returns the currently active {@link OjpDriverMetrics} implementation. + * Never {@code null}. + * + * @return the active metrics instance + */ + public static OjpDriverMetrics get() { + return instance; + } + + /** + * Replaces the active {@link OjpDriverMetrics} implementation. + * + * @param metrics the new implementation; must not be {@code null} + * @throws IllegalArgumentException if {@code metrics} is {@code null} + */ + public static void set(OjpDriverMetrics metrics) { + if (metrics == null) { + throw new IllegalArgumentException("metrics must not be null"); + } + instance = metrics; + } + + /** + * Resets the holder to the default {@link NoOpOjpDriverMetrics} instance. + * Primarily intended for use in tests. + */ + public static void reset() { + instance = NoOpOjpDriverMetrics.INSTANCE; + } +} diff --git a/ojp-jdbc-driver/src/test/java/org/openjproxy/grpc/client/OjpMetricsIteratorTest.java b/ojp-jdbc-driver/src/test/java/org/openjproxy/grpc/client/OjpMetricsIteratorTest.java new file mode 100644 index 000000000..cc30646b2 --- /dev/null +++ b/ojp-jdbc-driver/src/test/java/org/openjproxy/grpc/client/OjpMetricsIteratorTest.java @@ -0,0 +1,120 @@ +package org.openjproxy.grpc.client; + +import com.openjproxy.grpc.OpResult; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.openjproxy.jdbc.OjpDriverMetrics; +import org.openjproxy.jdbc.OjpDriverMetricsHolder; + +import java.util.Collections; +import java.util.Iterator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OjpMetricsIteratorTest { + + private long recordedDuration = -1; + private int failedCount = 0; + + private final OjpDriverMetrics capturingMetrics = new OjpDriverMetrics() { + @Override public void onConnectionCreated() {} + @Override public void onConnectionFailed() {} + @Override public void onConnectionClosed() {} + @Override public void onStatementExecuted(long durationMs) { recordedDuration = durationMs; } + @Override public void onStatementFailed() { failedCount++; } + }; + + @AfterEach + void resetHolder() { + OjpDriverMetricsHolder.reset(); + } + + @Test + void recordsExecutedMetricAfterFullConsumption() { + OjpDriverMetricsHolder.set(capturingMetrics); + + Iterator delegate = Collections.emptyList().iterator(); + OjpMetricsIterator iterator = new OjpMetricsIterator(delegate); + + boolean hasNext = iterator.hasNext(); + + assertFalse(hasNext); + assertTrue(recordedDuration >= 0, "duration should be >= 0 but was " + recordedDuration); + } + + @Test + void recordsExecutedMetricOnlyOnce() { + OjpDriverMetricsHolder.set(capturingMetrics); + + Iterator delegate = Collections.emptyList().iterator(); + OjpMetricsIterator iterator = new OjpMetricsIterator(delegate); + + iterator.hasNext(); + recordedDuration = -99; + iterator.hasNext(); + + // Second hasNext() should not re-record the metric + assertEquals(-99L, recordedDuration); + } + + @Test + void recordsFailedMetricWhenHasNextThrows() { + OjpDriverMetricsHolder.set(capturingMetrics); + + Iterator failingDelegate = new Iterator<>() { + @Override public boolean hasNext() { throw new RuntimeException("stream error"); } + @Override public OpResult next() { return null; } + }; + OjpMetricsIterator iterator = new OjpMetricsIterator(failingDelegate); + + assertThrows(RuntimeException.class, iterator::hasNext); + assertEquals(1, failedCount); + } + + @Test + void recordsFailedMetricWhenNextThrows() { + OjpDriverMetricsHolder.set(capturingMetrics); + + Iterator failingDelegate = new Iterator<>() { + @Override public boolean hasNext() { return true; } + @Override public OpResult next() { throw new RuntimeException("stream error"); } + }; + OjpMetricsIterator iterator = new OjpMetricsIterator(failingDelegate); + + assertThrows(RuntimeException.class, iterator::next); + assertEquals(1, failedCount); + } + + @Test + void recordsFailedMetricOnlyOnce() { + OjpDriverMetricsHolder.set(capturingMetrics); + + Iterator failingDelegate = new Iterator<>() { + @Override public boolean hasNext() { throw new RuntimeException("stream error"); } + @Override public OpResult next() { return null; } + }; + OjpMetricsIterator iterator = new OjpMetricsIterator(failingDelegate); + + assertThrows(RuntimeException.class, iterator::hasNext); + assertThrows(RuntimeException.class, iterator::hasNext); + + assertEquals(1, failedCount); + } + + @Test + void delegatesHasNextAndNext() { + OjpDriverMetricsHolder.set(capturingMetrics); + + OpResult result = OpResult.getDefaultInstance(); + Iterator delegate = Collections.singletonList(result).iterator(); + OjpMetricsIterator iterator = new OjpMetricsIterator(delegate); + + assertTrue(iterator.hasNext()); + assertSame(result, iterator.next()); + assertFalse(iterator.hasNext()); + } +} diff --git a/pom.xml b/pom.xml index 94c50aa06..8b408ca79 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ 4.1.130.Final 3.18.0 2.18.6 + 1.16.3 diff --git a/spring-boot-starter-ojp/pom.xml b/spring-boot-starter-ojp/pom.xml index 131c5620a..774dd9327 100644 --- a/spring-boot-starter-ojp/pom.xml +++ b/spring-boot-starter-ojp/pom.xml @@ -91,6 +91,14 @@ provided + + + io.micrometer + micrometer-core + ${micrometer.version} + true + + org.springframework.boot diff --git a/spring-boot-starter-ojp/src/main/java/org/openjproxy/autoconfigure/OjpMicrometerAutoConfiguration.java b/spring-boot-starter-ojp/src/main/java/org/openjproxy/autoconfigure/OjpMicrometerAutoConfiguration.java new file mode 100644 index 000000000..1a3c82b19 --- /dev/null +++ b/spring-boot-starter-ojp/src/main/java/org/openjproxy/autoconfigure/OjpMicrometerAutoConfiguration.java @@ -0,0 +1,75 @@ +package org.openjproxy.autoconfigure; + +import io.micrometer.core.instrument.MeterRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.openjproxy.jdbc.OjpDriverMetricsHolder; + +/** + * Spring Boot auto-configuration that bridges OJP driver metrics to Micrometer. + * + *

This auto-configuration activates when all of the following are true:

+ *
    + *
  1. The OJP JDBC driver ({@code org.openjproxy.jdbc.Driver}) is on the classpath.
  2. + *
  3. At least one datasource URL starts with {@code jdbc:ojp}.
  4. + *
  5. Micrometer's {@link MeterRegistry} class is on the classpath.
  6. + *
  7. A {@link MeterRegistry} bean is present in the Spring application context + * (typically provided by {@code spring-boot-starter-actuator}).
  8. + *
+ * + *

When active, this configuration registers an {@link OjpMicrometerDriverMetrics} bean + * and installs it as the active driver metrics implementation via + * {@link OjpDriverMetricsHolder#set(org.openjproxy.jdbc.OjpDriverMetrics)}. The following + * Micrometer meters are then populated:

+ *
    + *
  • {@code ojp.driver.connections.created} – counter
  • + *
  • {@code ojp.driver.connections.failed} – counter
  • + *
  • {@code ojp.driver.connections.closed} – counter
  • + *
  • {@code ojp.driver.connections.active} – gauge
  • + *
  • {@code ojp.driver.statements.executed} – counter
  • + *
  • {@code ojp.driver.statements.failed} – counter
  • + *
  • {@code ojp.driver.statements.execution.time} – distribution summary (ms)
  • + *
+ * + *

These metrics are automatically exported to any Micrometer backend that the application + * has configured (e.g. Prometheus via {@code micrometer-registry-prometheus}, Datadog, + * InfluxDB, etc.) without any additional configuration.

+ * + *

To disable OJP Micrometer metrics while keeping other actuator metrics active, exclude + * this auto-configuration class:

+ *
+ * @SpringBootApplication(exclude = OjpMicrometerAutoConfiguration.class)
+ * 
+ */ +@AutoConfiguration(after = OjpAutoConfiguration.class) +@ConditionalOnClass({MeterRegistry.class, org.openjproxy.jdbc.Driver.class}) +@Conditional(OnAnyOjpDatasourceUrlCondition.class) +@ConditionalOnBean(MeterRegistry.class) +public class OjpMicrometerAutoConfiguration { + + private static final Logger log = LoggerFactory.getLogger(OjpMicrometerAutoConfiguration.class); + + /** + * Creates and registers the {@link OjpMicrometerDriverMetrics} bean. + * + *

The bean is installed into {@link OjpDriverMetricsHolder} so that the OJP JDBC driver + * records connection and statement metrics for every subsequent operation.

+ * + * @param registry the Micrometer registry provided by Spring Boot Actuator + * @return the metrics implementation + */ + @Bean + @ConditionalOnMissingBean(OjpMicrometerDriverMetrics.class) + public OjpMicrometerDriverMetrics ojpMicrometerDriverMetrics(MeterRegistry registry) { + log.info("Registering OJP Micrometer driver metrics"); + OjpMicrometerDriverMetrics metrics = new OjpMicrometerDriverMetrics(registry); + OjpDriverMetricsHolder.set(metrics); + return metrics; + } +} diff --git a/spring-boot-starter-ojp/src/main/java/org/openjproxy/autoconfigure/OjpMicrometerDriverMetrics.java b/spring-boot-starter-ojp/src/main/java/org/openjproxy/autoconfigure/OjpMicrometerDriverMetrics.java new file mode 100644 index 000000000..d5473fd88 --- /dev/null +++ b/spring-boot-starter-ojp/src/main/java/org/openjproxy/autoconfigure/OjpMicrometerDriverMetrics.java @@ -0,0 +1,104 @@ +package org.openjproxy.autoconfigure; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import org.openjproxy.jdbc.OjpDriverMetrics; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Micrometer-based implementation of {@link OjpDriverMetrics}. + * + *

Registers the following meters with the provided {@link MeterRegistry}:

+ *
    + *
  • ojp.driver.connections.created – counter: total JDBC connections successfully opened
  • + *
  • ojp.driver.connections.failed – counter: total JDBC connection attempts that failed
  • + *
  • ojp.driver.connections.closed – counter: total JDBC connections closed
  • + *
  • ojp.driver.connections.active – gauge: current number of open JDBC connections
  • + *
  • ojp.driver.statements.executed – counter: total SQL statements executed successfully
  • + *
  • ojp.driver.statements.failed – counter: total SQL statement executions that failed
  • + *
  • ojp.driver.statements.execution.time – distribution summary: client-side + * round-trip time of SQL statement executions in milliseconds
  • + *
+ * + *

An instance of this class is created and registered with {@link org.openjproxy.jdbc.OjpDriverMetricsHolder} + * by {@link OjpMicrometerAutoConfiguration} when both the OJP driver and a + * {@link MeterRegistry} bean are present on the classpath.

+ */ +public class OjpMicrometerDriverMetrics implements OjpDriverMetrics { + + private final Counter connectionsCreated; + private final Counter connectionsFailed; + private final Counter connectionsClosed; + private final AtomicLong activeConnections = new AtomicLong(0); + private final Counter statementsExecuted; + private final Counter statementsFailed; + private final DistributionSummary statementExecutionTime; + + /** + * Creates a new {@link OjpMicrometerDriverMetrics} and registers all meters with the + * given {@link MeterRegistry}. + * + * @param registry the Micrometer registry to register meters with; must not be {@code null} + */ + public OjpMicrometerDriverMetrics(MeterRegistry registry) { + this.connectionsCreated = Counter.builder("ojp.driver.connections.created") + .description("Total number of OJP JDBC connections successfully opened") + .register(registry); + + this.connectionsFailed = Counter.builder("ojp.driver.connections.failed") + .description("Total number of OJP JDBC connection attempts that failed") + .register(registry); + + this.connectionsClosed = Counter.builder("ojp.driver.connections.closed") + .description("Total number of OJP JDBC connections closed") + .register(registry); + + Gauge.builder("ojp.driver.connections.active", activeConnections, AtomicLong::doubleValue) + .description("Current number of open OJP JDBC connections") + .register(registry); + + this.statementsExecuted = Counter.builder("ojp.driver.statements.executed") + .description("Total number of OJP SQL statements executed successfully") + .register(registry); + + this.statementsFailed = Counter.builder("ojp.driver.statements.failed") + .description("Total number of OJP SQL statement executions that failed") + .register(registry); + + this.statementExecutionTime = DistributionSummary.builder("ojp.driver.statements.execution.time") + .description("Client-side round-trip execution time of OJP SQL statements in milliseconds") + .baseUnit("ms") + .register(registry); + } + + @Override + public void onConnectionCreated() { + connectionsCreated.increment(); + activeConnections.incrementAndGet(); + } + + @Override + public void onConnectionFailed() { + connectionsFailed.increment(); + } + + @Override + public void onConnectionClosed() { + connectionsClosed.increment(); + activeConnections.decrementAndGet(); + } + + @Override + public void onStatementExecuted(long durationMs) { + statementsExecuted.increment(); + statementExecutionTime.record(durationMs); + } + + @Override + public void onStatementFailed() { + statementsFailed.increment(); + } +} diff --git a/spring-boot-starter-ojp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starter-ojp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index aa9435f0d..c18f28122 100644 --- a/spring-boot-starter-ojp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-starter-ojp/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1,2 @@ org.openjproxy.autoconfigure.OjpAutoConfiguration +org.openjproxy.autoconfigure.OjpMicrometerAutoConfiguration diff --git a/spring-boot-starter-ojp/src/test/java/org/openjproxy/autoconfigure/OjpMicrometerAutoConfigurationTest.java b/spring-boot-starter-ojp/src/test/java/org/openjproxy/autoconfigure/OjpMicrometerAutoConfigurationTest.java new file mode 100644 index 000000000..009db9402 --- /dev/null +++ b/spring-boot-starter-ojp/src/test/java/org/openjproxy/autoconfigure/OjpMicrometerAutoConfigurationTest.java @@ -0,0 +1,149 @@ +package org.openjproxy.autoconfigure; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.openjproxy.jdbc.OjpDriverMetrics; +import org.openjproxy.jdbc.OjpDriverMetricsHolder; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +class OjpMicrometerAutoConfigurationTest { + + private static final String OJP_URL = "spring.datasource.url=jdbc:ojp[localhost:1059]_postgresql://user@localhost/mydb"; + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of( + OjpAutoConfiguration.class, + OjpMicrometerAutoConfiguration.class)); + + @AfterEach + void resetMetricsHolder() { + OjpDriverMetricsHolder.reset(); + } + + @Test + void shouldRegisterMicrometerMetricsBeanWhenMeterRegistryIsPresent() { + contextRunner + .withPropertyValues(OJP_URL) + .withUserConfiguration(SimpleMeterRegistryConfiguration.class) + .run(context -> { + assertThat(context).hasSingleBean(OjpMicrometerDriverMetrics.class); + assertThat(OjpDriverMetricsHolder.get()).isInstanceOf(OjpMicrometerDriverMetrics.class); + }); + } + + @Test + void shouldNotRegisterMicrometerMetricsBeanWhenNoMeterRegistryPresent() { + contextRunner + .withPropertyValues(OJP_URL) + .run(context -> assertThat(context).doesNotHaveBean(OjpMicrometerDriverMetrics.class)); + } + + @Test + void shouldNotRegisterMicrometerMetricsBeanWhenNoDatasourceUrlConfigured() { + contextRunner + .withUserConfiguration(SimpleMeterRegistryConfiguration.class) + .run(context -> assertThat(context).doesNotHaveBean(OjpMicrometerDriverMetrics.class)); + } + + @Test + void shouldRegisterConnectionsCreatedCounter() { + contextRunner + .withPropertyValues(OJP_URL) + .withUserConfiguration(SimpleMeterRegistryConfiguration.class) + .run(context -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("ojp.driver.connections.created").counter()).isNotNull(); + }); + } + + @Test + void shouldRegisterConnectionsFailedCounter() { + contextRunner + .withPropertyValues(OJP_URL) + .withUserConfiguration(SimpleMeterRegistryConfiguration.class) + .run(context -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("ojp.driver.connections.failed").counter()).isNotNull(); + }); + } + + @Test + void shouldRegisterConnectionsClosedCounter() { + contextRunner + .withPropertyValues(OJP_URL) + .withUserConfiguration(SimpleMeterRegistryConfiguration.class) + .run(context -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("ojp.driver.connections.closed").counter()).isNotNull(); + }); + } + + @Test + void shouldRegisterActiveConnectionsGauge() { + contextRunner + .withPropertyValues(OJP_URL) + .withUserConfiguration(SimpleMeterRegistryConfiguration.class) + .run(context -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("ojp.driver.connections.active").gauge()).isNotNull(); + }); + } + + @Test + void shouldRegisterStatementsExecutedCounter() { + contextRunner + .withPropertyValues(OJP_URL) + .withUserConfiguration(SimpleMeterRegistryConfiguration.class) + .run(context -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("ojp.driver.statements.executed").counter()).isNotNull(); + }); + } + + @Test + void shouldRegisterStatementsFailedCounter() { + contextRunner + .withPropertyValues(OJP_URL) + .withUserConfiguration(SimpleMeterRegistryConfiguration.class) + .run(context -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("ojp.driver.statements.failed").counter()).isNotNull(); + }); + } + + @Test + void shouldRegisterStatementExecutionTimeDistributionSummary() { + contextRunner + .withPropertyValues(OJP_URL) + .withUserConfiguration(SimpleMeterRegistryConfiguration.class) + .run(context -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("ojp.driver.statements.execution.time").summary()).isNotNull(); + }); + } + + @Test + void shouldNotRegisterMicrometerMetricsBeanWhenNonOjpDatasourceUrlConfigured() { + contextRunner + .withPropertyValues("spring.datasource.url=jdbc:postgresql://localhost:5432/mydb") + .withUserConfiguration(SimpleMeterRegistryConfiguration.class) + .run(context -> assertThat(context).doesNotHaveBean(OjpMicrometerDriverMetrics.class)); + } + + @Configuration + static class SimpleMeterRegistryConfiguration { + @Bean + MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + } +} diff --git a/spring-boot-starter-ojp/src/test/java/org/openjproxy/autoconfigure/OjpMicrometerDriverMetricsTest.java b/spring-boot-starter-ojp/src/test/java/org/openjproxy/autoconfigure/OjpMicrometerDriverMetricsTest.java new file mode 100644 index 000000000..4750c8900 --- /dev/null +++ b/spring-boot-starter-ojp/src/test/java/org/openjproxy/autoconfigure/OjpMicrometerDriverMetricsTest.java @@ -0,0 +1,106 @@ +package org.openjproxy.autoconfigure; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class OjpMicrometerDriverMetricsTest { + + private MeterRegistry registry; + private OjpMicrometerDriverMetrics metrics; + + @BeforeEach + void setUp() { + registry = new SimpleMeterRegistry(); + metrics = new OjpMicrometerDriverMetrics(registry); + } + + @Test + void onConnectionCreated_incrementsCreatedCounterAndActiveGauge() { + metrics.onConnectionCreated(); + + Counter created = registry.find("ojp.driver.connections.created").counter(); + Gauge active = registry.find("ojp.driver.connections.active").gauge(); + + assertThat(created).isNotNull(); + assertThat(created.count()).isEqualTo(1.0); + assertThat(active).isNotNull(); + assertThat(active.value()).isEqualTo(1.0); + } + + @Test + void onConnectionFailed_incrementsFailedCounter() { + metrics.onConnectionFailed(); + + Counter failed = registry.find("ojp.driver.connections.failed").counter(); + assertThat(failed).isNotNull(); + assertThat(failed.count()).isEqualTo(1.0); + } + + @Test + void onConnectionClosed_incrementsClosedCounterAndDecrementsActiveGauge() { + metrics.onConnectionCreated(); + metrics.onConnectionCreated(); + metrics.onConnectionClosed(); + + Counter closed = registry.find("ojp.driver.connections.closed").counter(); + Gauge active = registry.find("ojp.driver.connections.active").gauge(); + + assertThat(closed).isNotNull(); + assertThat(closed.count()).isEqualTo(1.0); + assertThat(active).isNotNull(); + assertThat(active.value()).isEqualTo(1.0); + } + + @Test + void onStatementExecuted_incrementsExecutedCounterAndRecordsTime() { + metrics.onStatementExecuted(42L); + + Counter executed = registry.find("ojp.driver.statements.executed").counter(); + DistributionSummary execTime = registry.find("ojp.driver.statements.execution.time").summary(); + + assertThat(executed).isNotNull(); + assertThat(executed.count()).isEqualTo(1.0); + assertThat(execTime).isNotNull(); + assertThat(execTime.count()).isEqualTo(1L); + assertThat(execTime.totalAmount()).isEqualTo(42.0); + } + + @Test + void onStatementFailed_incrementsFailedCounter() { + metrics.onStatementFailed(); + + Counter failed = registry.find("ojp.driver.statements.failed").counter(); + assertThat(failed).isNotNull(); + assertThat(failed.count()).isEqualTo(1.0); + } + + @Test + void activeGaugeTracksMultipleConnectionsCorrectly() { + metrics.onConnectionCreated(); + metrics.onConnectionCreated(); + metrics.onConnectionCreated(); + metrics.onConnectionClosed(); + + Gauge active = registry.find("ojp.driver.connections.active").gauge(); + assertThat(active).isNotNull(); + assertThat(active.value()).isEqualTo(2.0); + } + + @Test + void allMetersRegisteredOnConstruction() { + assertThat(registry.find("ojp.driver.connections.created").counter()).isNotNull(); + assertThat(registry.find("ojp.driver.connections.failed").counter()).isNotNull(); + assertThat(registry.find("ojp.driver.connections.closed").counter()).isNotNull(); + assertThat(registry.find("ojp.driver.connections.active").gauge()).isNotNull(); + assertThat(registry.find("ojp.driver.statements.executed").counter()).isNotNull(); + assertThat(registry.find("ojp.driver.statements.failed").counter()).isNotNull(); + assertThat(registry.find("ojp.driver.statements.execution.time").summary()).isNotNull(); + } +}