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 IteratorThis 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); + + IteratorThis auto-configuration activates when all of the following are true:
+ *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:
+ *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}:
+ *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(); + } +}