diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 38c9a025a772..189c60b583ed 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -61,6 +61,7 @@ These are the supported libraries and frameworks: | [Guava ListenableFuture](https://guava.dev/releases/snapshot/api/docs/com/google/common/util/concurrent/ListenableFuture.html) | 10.0+ | | [GWT](http://www.gwtproject.org/) | 2.0+ | | [Hibernate](https://github.com/hibernate/hibernate-orm) | 3.3+ | +| [HikariCP](https://github.com/brettwooldridge/HikariCP) | 3.0+ | | [HttpURLConnection](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/HttpURLConnection.html) | Java 8+ | | [Hystrix](https://github.com/Netflix/Hystrix) | 1.4+ | | [Java Executors](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executor.html) | Java 8+ | diff --git a/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/metrics/db/DbConnectionPoolMetrics.java b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/metrics/db/DbConnectionPoolMetrics.java new file mode 100644 index 000000000000..e792130eeb1b --- /dev/null +++ b/instrumentation-api-semconv/src/main/java/io/opentelemetry/instrumentation/api/metrics/db/DbConnectionPoolMetrics.java @@ -0,0 +1,158 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.api.metrics.db; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterBuilder; +import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; +import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties; +import java.util.function.LongSupplier; + +/** + * A helper class that models the database + * client connection pool metrics semantic conventions. + */ +public final class DbConnectionPoolMetrics { + + static final AttributeKey POOL_NAME = stringKey("pool.name"); + static final AttributeKey CONNECTION_STATE = stringKey("state"); + + static final String STATE_IDLE = "idle"; + static final String STATE_USED = "used"; + + public static DbConnectionPoolMetrics create( + OpenTelemetry openTelemetry, String instrumentationName, String poolName) { + + MeterBuilder meterBuilder = openTelemetry.getMeterProvider().meterBuilder(instrumentationName); + String version = EmbeddedInstrumentationProperties.findVersion(instrumentationName); + if (version != null) { + meterBuilder.setInstrumentationVersion(version); + } + return new DbConnectionPoolMetrics(meterBuilder.build(), Attributes.of(POOL_NAME, poolName)); + } + + private final Meter meter; + private final Attributes attributes; + private final Attributes usedConnectionsAttributes; + private final Attributes idleConnectionsAttributes; + + DbConnectionPoolMetrics(Meter meter, Attributes attributes) { + this.meter = meter; + this.attributes = attributes; + usedConnectionsAttributes = attributes.toBuilder().put(CONNECTION_STATE, STATE_USED).build(); + idleConnectionsAttributes = attributes.toBuilder().put(CONNECTION_STATE, STATE_IDLE).build(); + } + + public ObservableLongUpDownCounter usedConnections(LongSupplier usedConnectionsGetter) { + return meter + .upDownCounterBuilder("db.client.connections.usage") + .setUnit("connections") + .setDescription( + "The number of connections that are currently in state described by the state attribute.") + .buildWithCallback( + measurement -> + measurement.record(usedConnectionsGetter.getAsLong(), usedConnectionsAttributes)); + } + + public ObservableLongUpDownCounter idleConnections(LongSupplier idleConnectionsGetter) { + return meter + .upDownCounterBuilder("db.client.connections.usage") + .setUnit("connections") + .setDescription( + "The number of connections that are currently in state described by the state attribute.") + .buildWithCallback( + measurement -> + measurement.record(idleConnectionsGetter.getAsLong(), idleConnectionsAttributes)); + } + + public ObservableLongUpDownCounter minIdleConnections(LongSupplier minIdleConnectionsGetter) { + return meter + .upDownCounterBuilder("db.client.connections.idle.min") + .setUnit("connections") + .setDescription("The minimum number of idle open connections allowed.") + .buildWithCallback( + measurement -> measurement.record(minIdleConnectionsGetter.getAsLong(), attributes)); + } + + public ObservableLongUpDownCounter maxIdleConnections(LongSupplier maxIdleConnectionsGetter) { + return meter + .upDownCounterBuilder("db.client.connections.idle.max") + .setUnit("connections") + .setDescription("The maximum number of idle open connections allowed.") + .buildWithCallback( + measurement -> measurement.record(maxIdleConnectionsGetter.getAsLong(), attributes)); + } + + public ObservableLongUpDownCounter maxConnections(LongSupplier maxConnectionsGetter) { + return meter + .upDownCounterBuilder("db.client.connections.max") + .setUnit("connections") + .setDescription("The maximum number of open connections allowed.") + .buildWithCallback( + measurement -> measurement.record(maxConnectionsGetter.getAsLong(), attributes)); + } + + public ObservableLongUpDownCounter pendingRequestsForConnection( + LongSupplier pendingRequestsGetter) { + return meter + .upDownCounterBuilder("db.client.connections.pending_requests") + .setUnit("requests") + .setDescription( + "The number of pending requests for an open connection, cumulative for the entire pool.") + .buildWithCallback( + measurement -> measurement.record(pendingRequestsGetter.getAsLong(), attributes)); + } + + // TODO: should be a BoundLongCounter + public LongCounter connectionTimeouts() { + return meter + .counterBuilder("db.client.connections.timeouts") + .setUnit("timeouts") + .setDescription( + "The number of connection timeouts that have occurred trying to obtain a connection from the pool.") + .build(); + } + + // TODO: should be a BoundDoubleHistogram + public DoubleHistogram connectionCreateTime() { + return meter + .histogramBuilder("db.client.connections.create_time") + .setUnit("ms") + .setDescription("The time it took to create a new connection.") + .build(); + } + + // TODO: should be a BoundDoubleHistogram + public DoubleHistogram connectionWaitTime() { + return meter + .histogramBuilder("db.client.connections.wait_time") + .setUnit("ms") + .setDescription("The time it took to obtain an open connection from the pool.") + .build(); + } + + // TODO: should be a BoundDoubleHistogram + public DoubleHistogram connectionUseTime() { + return meter + .histogramBuilder("db.client.connections.use_time") + .setUnit("ms") + .setDescription("The time between borrowing a connection and returning it to the pool.") + .build(); + } + + // TODO: should be removed once bound instruments are back + public Attributes getAttributes() { + return attributes; + } +} diff --git a/instrumentation/hikaricp-3.0/javaagent/build.gradle.kts b/instrumentation/hikaricp-3.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..1558b737eb39 --- /dev/null +++ b/instrumentation/hikaricp-3.0/javaagent/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("com.zaxxer") + module.set("HikariCP") + versions.set("[3.0.0,)") + // muzzle does not detect PoolStats method references used - some of these methods were introduced in 3.0 and we can't assertInverse + + // 4.0.0 uses a broken slf4j version: the "${slf4j.version}" placeholder is taken literally + skip("4.0.0") + } +} + +dependencies { + library("com.zaxxer:HikariCP:3.0.0") +} diff --git a/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/HikariCpInstrumentationModule.java b/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/HikariCpInstrumentationModule.java new file mode 100644 index 000000000000..bedff419d4ac --- /dev/null +++ b/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/HikariCpInstrumentationModule.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hikaricp; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class HikariCpInstrumentationModule extends InstrumentationModule { + + public HikariCpInstrumentationModule() { + super("hikaricp", "hikaricp-3.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new HikariPoolInstrumentation()); + } +} diff --git a/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/HikariPoolInstrumentation.java b/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/HikariPoolInstrumentation.java new file mode 100644 index 000000000000..e4cd141668ab --- /dev/null +++ b/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/HikariPoolInstrumentation.java @@ -0,0 +1,49 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hikaricp; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.zaxxer.hikari.metrics.MetricsTrackerFactory; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class HikariPoolInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.zaxxer.hikari.pool.HikariPool"); + } + + @Override + public void transform(TypeTransformer transformer) { + // this method is always called in the HikariPool constructor, even if the user does not + // configure anything + transformer.applyAdviceToMethod( + named("setMetricsTrackerFactory") + .and(takesArguments(1)) + .and(takesArgument(0, named("com.zaxxer.hikari.metrics.MetricsTrackerFactory"))), + this.getClass().getName() + "$SetMetricsTrackerFactoryAdvice"); + } + + @SuppressWarnings("unused") + public static class SetMetricsTrackerFactoryAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onEnter( + @Advice.Argument(value = 0, readOnly = false) MetricsTrackerFactory metricsTrackerFactory) { + + if (!(metricsTrackerFactory instanceof OpenTelemetryMetricsTrackerFactory)) { + metricsTrackerFactory = new OpenTelemetryMetricsTrackerFactory(metricsTrackerFactory); + } + } + } +} diff --git a/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/OpenTelemetryMetricsTracker.java b/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/OpenTelemetryMetricsTracker.java new file mode 100644 index 000000000000..b9f977692cab --- /dev/null +++ b/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/OpenTelemetryMetricsTracker.java @@ -0,0 +1,78 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hikaricp; + +import com.zaxxer.hikari.metrics.IMetricsTracker; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; +import java.util.List; +import java.util.concurrent.TimeUnit; + +final class OpenTelemetryMetricsTracker implements IMetricsTracker { + + private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1); + + private final IMetricsTracker userMetricsTracker; + + private final List observableInstruments; + private final LongCounter timeouts; + private final DoubleHistogram createTime; + private final DoubleHistogram waitTime; + private final DoubleHistogram useTime; + private final Attributes attributes; + + OpenTelemetryMetricsTracker( + IMetricsTracker userMetricsTracker, + List observableInstruments, + LongCounter timeouts, + DoubleHistogram createTime, + DoubleHistogram waitTime, + DoubleHistogram useTime, + Attributes attributes) { + this.userMetricsTracker = userMetricsTracker; + this.observableInstruments = observableInstruments; + this.timeouts = timeouts; + this.createTime = createTime; + this.waitTime = waitTime; + this.useTime = useTime; + this.attributes = attributes; + } + + @Override + public void recordConnectionCreatedMillis(long connectionCreatedMillis) { + createTime.record((double) connectionCreatedMillis, attributes); + userMetricsTracker.recordConnectionCreatedMillis(connectionCreatedMillis); + } + + @Override + public void recordConnectionAcquiredNanos(long elapsedAcquiredNanos) { + double millis = elapsedAcquiredNanos / NANOS_PER_MS; + waitTime.record(millis, attributes); + userMetricsTracker.recordConnectionAcquiredNanos(elapsedAcquiredNanos); + } + + @Override + public void recordConnectionUsageMillis(long elapsedBorrowedMillis) { + useTime.record((double) elapsedBorrowedMillis, attributes); + userMetricsTracker.recordConnectionUsageMillis(elapsedBorrowedMillis); + } + + @Override + public void recordConnectionTimeout() { + timeouts.add(1, attributes); + userMetricsTracker.recordConnectionTimeout(); + } + + @Override + public void close() { + for (ObservableLongUpDownCounter observable : observableInstruments) { + observable.close(); + } + userMetricsTracker.close(); + } +} diff --git a/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/OpenTelemetryMetricsTrackerFactory.java b/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/OpenTelemetryMetricsTrackerFactory.java new file mode 100644 index 000000000000..058f13f72949 --- /dev/null +++ b/instrumentation/hikaricp-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/hikaricp/OpenTelemetryMetricsTrackerFactory.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hikaricp; + +import com.zaxxer.hikari.metrics.IMetricsTracker; +import com.zaxxer.hikari.metrics.MetricsTrackerFactory; +import com.zaxxer.hikari.metrics.PoolStats; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.metrics.ObservableLongUpDownCounter; +import io.opentelemetry.instrumentation.api.metrics.db.DbConnectionPoolMetrics; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nullable; + +public final class OpenTelemetryMetricsTrackerFactory implements MetricsTrackerFactory { + + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.hikaricp-3.0"; + + @Nullable private final MetricsTrackerFactory userMetricsFactory; + + public OpenTelemetryMetricsTrackerFactory(@Nullable MetricsTrackerFactory userMetricsFactory) { + this.userMetricsFactory = userMetricsFactory; + } + + @Override + public IMetricsTracker create(String poolName, PoolStats poolStats) { + IMetricsTracker userMetricsTracker = + userMetricsFactory == null + ? NoopMetricsTracker.INSTANCE + : userMetricsFactory.create(poolName, poolStats); + + DbConnectionPoolMetrics metrics = + DbConnectionPoolMetrics.create(GlobalOpenTelemetry.get(), INSTRUMENTATION_NAME, poolName); + + List observableInstruments = + Arrays.asList( + metrics.usedConnections(poolStats::getActiveConnections), + metrics.idleConnections(poolStats::getIdleConnections), + metrics.minIdleConnections(poolStats::getMinConnections), + metrics.maxConnections(poolStats::getMaxConnections), + metrics.pendingRequestsForConnection(poolStats::getPendingThreads)); + + return new OpenTelemetryMetricsTracker( + userMetricsTracker, + observableInstruments, + metrics.connectionTimeouts(), + metrics.connectionCreateTime(), + metrics.connectionWaitTime(), + metrics.connectionUseTime(), + metrics.getAttributes()); + } + + enum NoopMetricsTracker implements IMetricsTracker { + INSTANCE + } +} diff --git a/instrumentation/hikaricp-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hikaricp/HikariCpInstrumentationTest.java b/instrumentation/hikaricp-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hikaricp/HikariCpInstrumentationTest.java new file mode 100644 index 000000000000..c82890c3e111 --- /dev/null +++ b/instrumentation/hikaricp-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/hikaricp/HikariCpInstrumentationTest.java @@ -0,0 +1,120 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.hikaricp; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.metrics.IMetricsTracker; +import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.db.DbConnectionPoolMetricsAssertions; +import java.sql.Connection; +import java.util.concurrent.TimeUnit; +import javax.sql.DataSource; +import org.assertj.core.api.AbstractIterableAssert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HikariCpInstrumentationTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create(); + + @Mock DataSource dataSourceMock; + @Mock Connection connectionMock; + @Mock IMetricsTracker userMetricsMock; + + @Test + void shouldReportMetrics() throws Exception { + // given + when(dataSourceMock.getConnection()).thenReturn(connectionMock); + + HikariDataSource hikariDataSource = new HikariDataSource(); + hikariDataSource.setPoolName("testPool"); + hikariDataSource.setDataSource(dataSourceMock); + + // when + Connection hikariConnection = hikariDataSource.getConnection(); + TimeUnit.MILLISECONDS.sleep(100); + hikariConnection.close(); + + // then + DbConnectionPoolMetricsAssertions.create(testing, "io.opentelemetry.hikaricp-3.0", "testPool") + .disableMaxIdleConnections() + // no timeouts happen during this test + .disableConnectionTimeouts() + .assertConnectionPoolEmitsMetrics(); + + // when + hikariDataSource.close(); + + // sleep exporter interval + Thread.sleep(100); + testing.clearData(); + Thread.sleep(100); + + // then + testing.waitAndAssertMetrics( + "io.opentelemetry.hikaricp-3.0", + "db.client.connections.usage", + AbstractIterableAssert::isEmpty); + testing.waitAndAssertMetrics( + "io.opentelemetry.hikaricp-3.0", + "db.client.connections.idle.min", + AbstractIterableAssert::isEmpty); + testing.waitAndAssertMetrics( + "io.opentelemetry.hikaricp-3.0", + "db.client.connections.max", + AbstractIterableAssert::isEmpty); + testing.waitAndAssertMetrics( + "io.opentelemetry.hikaricp-3.0", + "db.client.connections.pending_requests", + AbstractIterableAssert::isEmpty); + } + + @Test + void shouldNotBreakCustomUserMetrics() throws Exception { + // given + when(dataSourceMock.getConnection()).thenReturn(connectionMock); + + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setPoolName("anotherTestPool"); + hikariConfig.setDataSource(dataSourceMock); + hikariConfig.setMetricsTrackerFactory((poolName, poolStats) -> userMetricsMock); + + HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig); + cleanup.deferCleanup(hikariDataSource); + + // when + Connection hikariConnection = hikariDataSource.getConnection(); + TimeUnit.MILLISECONDS.sleep(100); + hikariConnection.close(); + + // then + DbConnectionPoolMetricsAssertions.create( + testing, "io.opentelemetry.hikaricp-3.0", "anotherTestPool") + .disableMaxIdleConnections() + // no timeouts happen during this test + .disableConnectionTimeouts() + .assertConnectionPoolEmitsMetrics(); + + verify(userMetricsMock, atLeastOnce()).recordConnectionCreatedMillis(anyLong()); + verify(userMetricsMock, atLeastOnce()).recordConnectionAcquiredNanos(anyLong()); + verify(userMetricsMock, atLeastOnce()).recordConnectionUsageMillis(anyLong()); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 120713163c05..999668df36cf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -220,6 +220,7 @@ include(":instrumentation:hibernate:hibernate-3.3:javaagent") include(":instrumentation:hibernate:hibernate-4.0:javaagent") include(":instrumentation:hibernate:hibernate-common:javaagent") include(":instrumentation:hibernate:hibernate-procedure-call-4.3:javaagent") +include(":instrumentation:hikaricp-3.0:javaagent") include(":instrumentation:http-url-connection:javaagent") include(":instrumentation:hystrix-1.4:javaagent") include(":instrumentation:java-http-client:javaagent") diff --git a/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/db/DbConnectionPoolMetricsAssertions.java b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/db/DbConnectionPoolMetricsAssertions.java new file mode 100644 index 000000000000..30ae076b462e --- /dev/null +++ b/testing-common/src/main/java/io/opentelemetry/instrumentation/testing/junit/db/DbConnectionPoolMetricsAssertions.java @@ -0,0 +1,213 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.testing.junit.db; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; + +public final class DbConnectionPoolMetricsAssertions { + + public static DbConnectionPoolMetricsAssertions create( + InstrumentationExtension testing, String instrumentationName, String poolName) { + return new DbConnectionPoolMetricsAssertions(testing, instrumentationName, poolName); + } + + private final InstrumentationExtension testing; + private final String instrumentationName; + private final String poolName; + + private boolean testMaxIdleConnections = true; + private boolean testConnectionTimeouts = true; + + DbConnectionPoolMetricsAssertions( + InstrumentationExtension testing, String instrumentationName, String poolName) { + this.testing = testing; + this.instrumentationName = instrumentationName; + this.poolName = poolName; + } + + public DbConnectionPoolMetricsAssertions disableMaxIdleConnections() { + testMaxIdleConnections = false; + return this; + } + + public DbConnectionPoolMetricsAssertions disableConnectionTimeouts() { + testConnectionTimeouts = false; + return this; + } + + public void assertConnectionPoolEmitsMetrics() { + testing.waitAndAssertMetrics( + instrumentationName, + "db.client.connections.usage", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("connections") + .hasDescription( + "The number of connections that are currently in state described by the state attribute.") + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point.hasAttributes( + Attributes.of( + stringKey("pool.name"), + poolName, + stringKey("state"), + "idle")), + point -> + point.hasAttributes( + Attributes.of( + stringKey("pool.name"), + poolName, + stringKey("state"), + "used")))))); + testing.waitAndAssertMetrics( + instrumentationName, + "db.client.connections.idle.min", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("connections") + .hasDescription("The minimum number of idle open connections allowed.") + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point.hasAttributes( + Attributes.of( + stringKey("pool.name"), poolName)))))); + if (testMaxIdleConnections) { + testing.waitAndAssertMetrics( + instrumentationName, + "db.client.connections.idle.max", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("connections") + .hasDescription("The maximum number of idle open connections allowed.") + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point.hasAttributes( + Attributes.of( + stringKey("pool.name"), poolName)))))); + } + testing.waitAndAssertMetrics( + instrumentationName, + "db.client.connections.max", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("connections") + .hasDescription("The maximum number of open connections allowed.") + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point.hasAttributes( + Attributes.of( + stringKey("pool.name"), poolName)))))); + testing.waitAndAssertMetrics( + instrumentationName, + "db.client.connections.pending_requests", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("requests") + .hasDescription( + "The number of pending requests for an open connection, cumulative for the entire pool.") + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point.hasAttributes( + Attributes.of( + stringKey("pool.name"), poolName)))))); + if (testConnectionTimeouts) { + testing.waitAndAssertMetrics( + instrumentationName, + "db.client.connections.timeouts", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("timeouts") + .hasDescription( + "The number of connection timeouts that have occurred trying to obtain a connection from the pool.") + .hasLongSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + point -> + point.hasAttributes( + Attributes.of( + stringKey("pool.name"), poolName)))))); + } + testing.waitAndAssertMetrics( + instrumentationName, + "db.client.connections.create_time", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasDescription("The time it took to create a new connection.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributes( + Attributes.of(stringKey("pool.name"), poolName)))))); + testing.waitAndAssertMetrics( + instrumentationName, + "db.client.connections.wait_time", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasDescription( + "The time it took to obtain an open connection from the pool.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributes( + Attributes.of(stringKey("pool.name"), poolName)))))); + testing.waitAndAssertMetrics( + instrumentationName, + "db.client.connections.use_time", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasUnit("ms") + .hasDescription( + "The time between borrowing a connection and returning it to the pool.") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point.hasAttributes( + Attributes.of(stringKey("pool.name"), poolName)))))); + } +} diff --git a/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/IgnoredTestTypesConfigurer.java b/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/IgnoredTestTypesConfigurer.java new file mode 100644 index 000000000000..49d9dde86a17 --- /dev/null +++ b/testing/agent-exporter/src/main/java/io/opentelemetry/javaagent/testing/IgnoredTestTypesConfigurer.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.testing; + +import com.google.auto.service.AutoService; +import io.opentelemetry.instrumentation.api.config.Config; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesBuilder; +import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer; + +@AutoService(IgnoredTypesConfigurer.class) +public class IgnoredTestTypesConfigurer implements IgnoredTypesConfigurer { + + @Override + public void configure(Config config, IgnoredTypesBuilder builder) { + // we don't want to instrument auto-generated mocks + builder + .ignoreClass("org.mockito") + .ignoreClass("com.zaxxer.hikari.metrics.IMetricsTracker$MockitoMock$"); + } +}