Implement Oracle UCP connection pool metrics (#6099)

* Implement Oracle UCP connection pool metrics

* add additional instrumentation name

* change asserting no metrics reported after shutdown
This commit is contained in:
Lauri Tulmin 2022-06-03 08:47:04 +03:00 committed by GitHub
parent cbf5bc31da
commit 157ab7f754
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 528 additions and 2 deletions

View File

@ -21,6 +21,7 @@ that can be used if you prefer that over using the Java agent:
* [Logback](../instrumentation/logback/logback-appender-1.0/library)
* [MongoDB Driver](../instrumentation/mongo/mongo-3.1/library)
* [OkHttp](../instrumentation/okhttp/okhttp-3.0/library)
* [Oracle UCP](../instrumentation/oracle-ucp-11.2/library)
* [OSHI](../instrumentation/oshi/library)
* [Reactor](../instrumentation/reactor/reactor-3.1/library)
* [RocketMQ](../instrumentation/rocketmq-client-4.8/library)

View File

@ -83,6 +83,7 @@ These are the supported libraries and frameworks:
| [MongoDB Driver](https://mongodb.github.io/mongo-java-driver/) | 3.1+ |
| [Netty](https://github.com/netty/netty) | 3.8+ |
| [OkHttp](https://github.com/square/okhttp/) | 2.2+ |
| [Oracle UCP](https://docs.oracle.com/database/121/JJUCP/) | 11.2+ |
| [OSHI](https://github.com/oshi/oshi/) | 5.3.1+ |
| [Play](https://github.com/playframework/playframework) | 2.4+ (not including 2.8.x yet) |
| [Play WS](https://github.com/playframework/play-ws) | 1.0+ |

View File

@ -0,0 +1,20 @@
plugins {
id("otel.javaagent-instrumentation")
}
muzzle {
pass {
group.set("com.oracle.database.jdbc")
module.set("ucp")
versions.set("[,)")
assertInverse.set(true)
}
}
dependencies {
library("com.oracle.database.jdbc:ucp:11.2.0.4")
implementation(project(":instrumentation:oracle-ucp-11.2:library"))
testImplementation(project(":instrumentation:oracle-ucp-11.2:testing"))
}

View File

@ -0,0 +1,26 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.oracleucp;
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 OracleUcpInstrumentationModule extends InstrumentationModule {
public OracleUcpInstrumentationModule() {
super("oracle-ucp", "oracle-ucp-11.2");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new UniversalConnectionPoolInstrumentation());
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.oracleucp;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.oracleucp.OracleUcpTelemetry;
public final class OracleUcpSingletons {
private static final OracleUcpTelemetry oracleUcpTelemetry =
OracleUcpTelemetry.create(GlobalOpenTelemetry.get());
public static OracleUcpTelemetry telemetry() {
return oracleUcpTelemetry;
}
private OracleUcpSingletons() {}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.oracleucp;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.implementsInterface;
import static io.opentelemetry.javaagent.instrumentation.oracleucp.OracleUcpSingletons.telemetry;
import static net.bytebuddy.matcher.ElementMatchers.named;
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
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;
import oracle.ucp.UniversalConnectionPool;
public class UniversalConnectionPoolInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<ClassLoader> classLoaderOptimization() {
return hasClassesNamed("oracle.ucp.UniversalConnectionPool");
}
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return implementsInterface(named("oracle.ucp.UniversalConnectionPool"));
}
@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
named("start")
.and(takesArguments(0).or(takesArguments(1).and(takesArgument(0, boolean.class)))),
this.getClass().getName() + "$StartAdvice");
transformer.applyAdviceToMethod(
named("stop").and(takesArguments(0)), this.getClass().getName() + "$StopAdvice");
}
@SuppressWarnings("unused")
public static class StartAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(@Advice.This UniversalConnectionPool connectionPool) {
telemetry().registerMetrics(connectionPool);
}
}
@SuppressWarnings("unused")
public static class StopAdvice {
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
public static void onExit(@Advice.This UniversalConnectionPool connectionPool) {
telemetry().unregisterMetrics(connectionPool);
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.oracleucp;
import io.opentelemetry.instrumentation.oracleucp.AbstractOracleUcpInstrumentationTest;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import oracle.ucp.jdbc.PoolDataSource;
import org.junit.jupiter.api.extension.RegisterExtension;
class OracleUcpInstrumentationTest extends AbstractOracleUcpInstrumentationTest {
@RegisterExtension
static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
@Override
protected InstrumentationExtension testing() {
return testing;
}
@Override
protected void configure(PoolDataSource connectionPool) {}
@Override
protected void shutdown(PoolDataSource connectionPool) {}
}

View File

@ -0,0 +1,47 @@
# Manual Instrumentation for Oracle UCP
Provides OpenTelemetry instrumentation for [Oracle UCP](https://docs.oracle.com/database/121/JJUCP/).
## Quickstart
### Add these dependencies to your project:
Replace `OPENTELEMETRY_VERSION` with the latest stable
[release](https://mvnrepository.com/artifact/io.opentelemetry). `Minimum version: 1.15.0`
For Maven, add to your `pom.xml` dependencies:
```xml
<dependencies>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-oracle-ucp-11.2</artifactId>
<version>OPENTELEMETRY_VERSION</version>
</dependency>
</dependencies>
```
For Gradle, add to your dependencies:
```groovy
implementation("io.opentelemetry.instrumentation:opentelemetry-oracle-ucp-11.2:OPENTELEMETRY_VERSION")
```
### Usage
The instrumentation library allows registering `UniversalConnectionPool` instances for collecting
OpenTelemetry-based metrics.
```java
OracleUcpTelemetry oracleUcpTelemetry;
void configure(OpenTelemetry openTelemetry, UniversalConnectionPool universalConnectionPool) {
oracleUcpTelemetry = OracleUcpTelemetry.create(openTelemetry);
oracleUcpTelemetry.registerMetrics(universalConnectionPool);
}
void destroy(UniversalConnectionPool universalConnectionPool) {
oracleUcpTelemetry.unregisterMetrics(universalConnectionPool);
}
```

View File

@ -0,0 +1,10 @@
plugins {
id("otel.library-instrumentation")
id("otel.nullaway-conventions")
}
dependencies {
library("com.oracle.database.jdbc:ucp:11.2.0.4")
testImplementation(project(":instrumentation:oracle-ucp-11.2:testing"))
}

View File

@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.oracleucp;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.metrics.ObservableLongUpDownCounter;
import io.opentelemetry.instrumentation.api.metrics.db.DbConnectionPoolMetrics;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import oracle.ucp.UniversalConnectionPool;
final class ConnectionPoolMetrics {
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.orcale-ucp-11.2";
// a weak map does not make sense here because each Meter holds a reference to the connection pool
// none of the UniversalConnectionPool implementations contain equals()/hashCode(), so it's safe
// to keep them in a plain ConcurrentHashMap
private static final Map<UniversalConnectionPool, List<ObservableLongUpDownCounter>>
dataSourceMetrics = new ConcurrentHashMap<>();
public static void registerMetrics(
OpenTelemetry openTelemetry, UniversalConnectionPool connectionPool) {
dataSourceMetrics.computeIfAbsent(
connectionPool, (unused) -> createMeters(openTelemetry, connectionPool));
}
private static List<ObservableLongUpDownCounter> createMeters(
OpenTelemetry openTelemetry, UniversalConnectionPool connectionPool) {
DbConnectionPoolMetrics metrics =
DbConnectionPoolMetrics.create(
openTelemetry, INSTRUMENTATION_NAME, connectionPool.getName());
return Arrays.asList(
metrics.usedConnections(connectionPool::getBorrowedConnectionsCount),
metrics.idleConnections(connectionPool::getAvailableConnectionsCount),
metrics.maxConnections(() -> connectionPool.getStatistics().getPeakConnectionsCount()),
metrics.pendingRequestsForConnection(
() -> connectionPool.getStatistics().getPendingRequestsCount()));
}
public static void unregisterMetrics(UniversalConnectionPool connectionPool) {
List<ObservableLongUpDownCounter> observableInstruments =
dataSourceMetrics.remove(connectionPool);
if (observableInstruments != null) {
for (ObservableLongUpDownCounter observable : observableInstruments) {
observable.close();
}
}
}
private ConnectionPoolMetrics() {}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.oracleucp;
import io.opentelemetry.api.OpenTelemetry;
import oracle.ucp.UniversalConnectionPool;
/** Entrypoint for instrumenting Oracle UCP database connection pools. */
public final class OracleUcpTelemetry {
/** Returns a new {@link OracleUcpTelemetry} configured with the given {@link OpenTelemetry}. */
public static OracleUcpTelemetry create(OpenTelemetry openTelemetry) {
return new OracleUcpTelemetry(openTelemetry);
}
private final OpenTelemetry openTelemetry;
private OracleUcpTelemetry(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
/** Start collecting metrics for given connection pool. */
public void registerMetrics(UniversalConnectionPool universalConnectionPool) {
ConnectionPoolMetrics.registerMetrics(openTelemetry, universalConnectionPool);
}
/** Stop collecting metrics for given connection pool. */
public void unregisterMetrics(UniversalConnectionPool universalConnectionPool) {
ConnectionPoolMetrics.unregisterMetrics(universalConnectionPool);
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.oracleucp;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
import oracle.ucp.UniversalConnectionPool;
import oracle.ucp.admin.UniversalConnectionPoolManagerImpl;
import oracle.ucp.jdbc.PoolDataSource;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.RegisterExtension;
public class OracleUcpInstrumentationTest extends AbstractOracleUcpInstrumentationTest {
@RegisterExtension
static final InstrumentationExtension testing = LibraryInstrumentationExtension.create();
private static OracleUcpTelemetry telemetry;
@Override
protected InstrumentationExtension testing() {
return testing;
}
@BeforeAll
static void setup() {
telemetry = OracleUcpTelemetry.create(testing.getOpenTelemetry());
}
@Override
protected void configure(PoolDataSource connectionPool) throws Exception {
UniversalConnectionPool universalConnectionPool =
UniversalConnectionPoolManagerImpl.getUniversalConnectionPoolManager()
.getConnectionPool(connectionPool.getConnectionPoolName());
telemetry.registerMetrics(universalConnectionPool);
}
@Override
protected void shutdown(PoolDataSource connectionPool) throws Exception {
UniversalConnectionPool universalConnectionPool =
UniversalConnectionPoolManagerImpl.getUniversalConnectionPoolManager()
.getConnectionPool(connectionPool.getConnectionPoolName());
telemetry.unregisterMetrics(universalConnectionPool);
}
}

View File

@ -0,0 +1,11 @@
plugins {
id("otel.java-conventions")
}
dependencies {
api(project(":testing-common"))
api("org.mockito:mockito-core")
api("org.mockito:mockito-junit-jupiter")
compileOnly("com.oracle.database.jdbc:ucp:11.2.0.4")
}

View File

@ -0,0 +1,99 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.oracleucp;
import static org.assertj.core.api.Assertions.assertThat;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.db.DbConnectionPoolMetricsAssertions;
import io.opentelemetry.instrumentation.testing.junit.db.MockDriver;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import oracle.ucp.admin.UniversalConnectionPoolManagerImpl;
import oracle.ucp.jdbc.PoolDataSource;
import oracle.ucp.jdbc.PoolDataSourceFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public abstract class AbstractOracleUcpInstrumentationTest {
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.orcale-ucp-11.2";
protected abstract InstrumentationExtension testing();
protected abstract void configure(PoolDataSource connectionPool) throws Exception;
protected abstract void shutdown(PoolDataSource connectionPool) throws Exception;
@BeforeAll
static void setUpMocks() throws SQLException {
MockDriver.register();
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void shouldReportMetrics(boolean setExplicitPoolName) throws Exception {
// given
PoolDataSource connectionPool = PoolDataSourceFactory.getPoolDataSource();
connectionPool.setConnectionFactoryClassName(MockDriver.class.getName());
connectionPool.setURL("jdbc:mock:testDatabase");
if (setExplicitPoolName) {
connectionPool.setConnectionPoolName("testPool");
}
// when
Connection connection = connectionPool.getConnection();
configure(connectionPool);
TimeUnit.MILLISECONDS.sleep(100);
connection.close();
// then
DbConnectionPoolMetricsAssertions.create(
testing(), INSTRUMENTATION_NAME, connectionPool.getConnectionPoolName())
.disableMinIdleConnections()
.disableMaxIdleConnections()
.disableConnectionTimeouts()
.disableCreateTime()
.disableWaitTime()
.disableUseTime()
.assertConnectionPoolEmitsMetrics();
// when
// this one too shouldn't cause any problems when called more than once
connectionPool.getConnection().close();
connectionPool.getConnection().close();
shutdown(connectionPool);
UniversalConnectionPoolManagerImpl.getUniversalConnectionPoolManager()
.destroyConnectionPool(connectionPool.getConnectionPoolName());
// sleep exporter interval
Thread.sleep(100);
testing().clearData();
Thread.sleep(100);
// then
Set<String> metricNames =
new HashSet<>(
Arrays.asList(
"db.client.connections.usage",
"db.client.connections.max",
"db.client.connections.pending_requests"));
assertThat(testing().metrics())
.filteredOn(
metricData ->
metricData.getInstrumentationScopeInfo().getName().equals(INSTRUMENTATION_NAME)
&& metricNames.contains(metricData.getName()))
.isEmpty();
}
}

View File

@ -342,6 +342,9 @@ include(":instrumentation:opentelemetry-api:opentelemetry-api-1.4:javaagent")
include(":instrumentation:opentelemetry-api:opentelemetry-api-1.10:javaagent")
include(":instrumentation:opentelemetry-instrumentation-api:javaagent")
include(":instrumentation:opentelemetry-instrumentation-api:testing")
include(":instrumentation:oracle-ucp-11.2:javaagent")
include(":instrumentation:oracle-ucp-11.2:library")
include(":instrumentation:oracle-ucp-11.2:testing")
include(":instrumentation:oshi:javaagent")
include(":instrumentation:oshi:library")
include(":instrumentation:oshi:testing")

View File

@ -42,11 +42,10 @@ dependencies {
api(project(":instrumentation-api"))
api("org.assertj:assertj-core")
// Needs to be api dependency due to Spock restriction.
api("org.awaitility:awaitility")
api("com.google.guava:guava")
api("org.mockito:mockito-core")
compileOnly(project(":testing:armeria-shaded-for-testing", configuration = "shadow"))

View File

@ -0,0 +1,59 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.testing.junit.db;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Properties;
import java.util.logging.Logger;
import org.mockito.Mockito;
public final class MockDriver implements Driver {
private static final MockDriver INSTANCE = new MockDriver();
public static void register() throws SQLException {
DriverManager.registerDriver(INSTANCE);
}
@Override
public Connection connect(String url, Properties info) {
return Mockito.mock(Connection.class);
}
@Override
public boolean acceptsURL(String url) {
return url.startsWith("jdbc:mock:");
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
return new DriverPropertyInfo[0];
}
@Override
public int getMajorVersion() {
return 0;
}
@Override
public int getMinorVersion() {
return 0;
}
@Override
public boolean jdbcCompliant() {
return false;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new SQLFeatureNotSupportedException("Feature not supported");
}
}