Implement Vibur DBCP connection pool metrics (#6092)

* Implement Vibur DBCP connection pool metrics

* Apply suggestions from code review

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>

* address review comments

* don't check for metircs that aren't reported

* rework library test setup

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
This commit is contained in:
Lauri Tulmin 2022-05-26 11:26:02 +03:00 committed by GitHub
parent 4586db491d
commit b95b64ba88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 557 additions and 82 deletions

View File

@ -31,6 +31,7 @@ that can be used if you prefer that over using the Java agent:
* [Spring RestTemplate](../instrumentation/spring/spring-web-3.1/library)
* [Spring Web MVC](../instrumentation/spring/spring-webmvc-3.1/library)
* [Spring WebFlux Client](../instrumentation/spring/spring-webflux-5.0/library)
* [Vibur DBCP](../instrumentation/vibur-dbcp-11.0/library)
And some libraries are publishing their own OpenTelemetry instrumentation (yay!), e.g.

View File

@ -117,6 +117,7 @@ These are the supported libraries and frameworks:
| [Vert.x HttpClient](https://vertx.io/docs/apidocs/io/vertx/core/http/HttpClient.html) | 3.0+ |
| [Vert.x Kafka Client](https://vertx.io/docs/vertx-kafka-client/java/) | 3.6+ |
| [Vert.x RxJava2](https://vertx.io/docs/vertx-rx/java2/) | 3.5+ |
| [Vibur DBCP](https://www.vibur.org/) | 11.0+ |
## Application Servers

View File

@ -0,0 +1,20 @@
plugins {
id("otel.javaagent-instrumentation")
}
muzzle {
pass {
group.set("org.vibur")
module.set("vibur-dbcp")
versions.set("[11.0,)")
assertInverse.set(true)
}
}
dependencies {
library("org.vibur:vibur-dbcp:11.0")
implementation(project(":instrumentation:vibur-dbcp-11.0:library"))
testImplementation(project(":instrumentation:vibur-dbcp-11.0:testing"))
}

View File

@ -0,0 +1,51 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.viburdbcp;
import static io.opentelemetry.javaagent.instrumentation.viburdbcp.ViburSingletons.telemetry;
import static net.bytebuddy.matcher.ElementMatchers.named;
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 org.vibur.dbcp.ViburDBCPDataSource;
final class ViburDbcpDataSourceInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("org.vibur.dbcp.ViburDBCPDataSource");
}
@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
named("start").and(takesArguments(0)), this.getClass().getName() + "$StartAdvice");
transformer.applyAdviceToMethod(
named("close").and(takesArguments(0)), this.getClass().getName() + "$CloseAdvice");
}
@SuppressWarnings("unused")
public static class StartAdvice {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onExit(@Advice.This ViburDBCPDataSource dataSource) {
telemetry().registerMetrics(dataSource);
}
}
@SuppressWarnings("unused")
public static class CloseAdvice {
@Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class)
public static void onExit(@Advice.This ViburDBCPDataSource dataSource) {
telemetry().unregisterMetrics(dataSource);
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.viburdbcp;
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 ViburDbcpInstrumentationModule extends InstrumentationModule {
public ViburDbcpInstrumentationModule() {
super("vibur-dbcp", "vibur-dbcp-11.0");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new ViburDbcpDataSourceInstrumentation());
}
}

View File

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

View File

@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.instrumentation.viburdbcp;
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.viburdbcp.AbstractViburInstrumentationTest;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.vibur.dbcp.ViburDBCPDataSource;
class ViburInstrumentationTest extends AbstractViburInstrumentationTest {
@RegisterExtension
static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
@Override
protected InstrumentationExtension testing() {
return testing;
}
@Override
protected void configure(ViburDBCPDataSource viburDataSource) {}
@Override
protected void shutdown(ViburDBCPDataSource viburDataSource) {}
}

View File

@ -0,0 +1,47 @@
# Manual Instrumentation for Vibur DBCP
Provides OpenTelemetry instrumentation for [Vibur DBCP](https://www.vibur.org/).
## 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-viburdbcp-11.0</artifactId>
<version>OPENTELEMETRY_VERSION</version>
</dependency>
</dependencies>
```
For Gradle, add to your dependencies:
```groovy
implementation("io.opentelemetry.instrumentation:opentelemetry-viburdbcp-11.0:OPENTELEMETRY_VERSION")
```
### Usage
The instrumentation library allows registering `ViburDBCPDataSource` instances for collecting
OpenTelemetry-based metrics.
```java
ViburTelemetry viburTelemetry;
void configure(OpenTelemetry openTelemetry, ViburDBCPDataSource viburDataSource) {
viburTelemetry = ViburTelemetry.create(openTelemetry);
viburTelemetry.registerMetrics(viburDataSource);
}
void destroy(ViburDBCPDataSource viburDataSource) {
viburTelemetry.unregisterMetrics(viburDataSource);
}
```

View File

@ -0,0 +1,10 @@
plugins {
id("otel.library-instrumentation")
id("otel.nullaway-conventions")
}
dependencies {
library("org.vibur:vibur-dbcp:11.0")
testImplementation(project(":instrumentation:vibur-dbcp-11.0:testing"))
}

View File

@ -0,0 +1,52 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.viburdbcp;
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 org.vibur.dbcp.ViburDBCPDataSource;
final class ConnectionPoolMetrics {
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.viburdbcp-11.0";
// a weak map does not make sense here because each Meter holds a reference to the dataSource
// ViburDBCPDataSource does not implement equals()/hashCode(), so it's safe to keep them in a
// plain ConcurrentHashMap
private static final Map<ViburDBCPDataSource, List<ObservableLongUpDownCounter>>
dataSourceMetrics = new ConcurrentHashMap<>();
public static void registerMetrics(OpenTelemetry openTelemetry, ViburDBCPDataSource dataSource) {
dataSourceMetrics.computeIfAbsent(
dataSource, (unused) -> createMeters(openTelemetry, dataSource));
}
private static List<ObservableLongUpDownCounter> createMeters(
OpenTelemetry openTelemetry, ViburDBCPDataSource dataSource) {
DbConnectionPoolMetrics metrics =
DbConnectionPoolMetrics.create(openTelemetry, INSTRUMENTATION_NAME, dataSource.getName());
return Arrays.asList(
metrics.usedConnections(() -> dataSource.getPool().taken()),
metrics.idleConnections(() -> dataSource.getPool().remainingCreated()),
metrics.maxConnections(dataSource::getPoolMaxSize));
}
public static void unregisterMetrics(ViburDBCPDataSource dataSource) {
List<ObservableLongUpDownCounter> observableInstruments = dataSourceMetrics.remove(dataSource);
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.viburdbcp;
import io.opentelemetry.api.OpenTelemetry;
import org.vibur.dbcp.ViburDBCPDataSource;
/** Entrypoint for instrumenting Vibur database connection pools. */
public final class ViburTelemetry {
/** Returns a new {@link ViburTelemetry} configured with the given {@link OpenTelemetry}. */
public static ViburTelemetry create(OpenTelemetry openTelemetry) {
return new ViburTelemetry(openTelemetry);
}
private final OpenTelemetry openTelemetry;
private ViburTelemetry(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
/** Start collecting metrics for given data source. */
public void registerMetrics(ViburDBCPDataSource dataSource) {
ConnectionPoolMetrics.registerMetrics(openTelemetry, dataSource);
}
/** Stop collecting metrics for given data source. */
public void unregisterMetrics(ViburDBCPDataSource dataSource) {
ConnectionPoolMetrics.unregisterMetrics(dataSource);
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.viburdbcp;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.vibur.dbcp.ViburDBCPDataSource;
class ViburInstrumentationTest extends AbstractViburInstrumentationTest {
@RegisterExtension
static final InstrumentationExtension testing = LibraryInstrumentationExtension.create();
private static ViburTelemetry telemetry;
@Override
protected InstrumentationExtension testing() {
return testing;
}
@BeforeAll
static void setup() {
telemetry = ViburTelemetry.create(testing.getOpenTelemetry());
}
@Override
protected void configure(ViburDBCPDataSource viburDataSource) {
telemetry.registerMetrics(viburDataSource);
}
@Override
protected void shutdown(ViburDBCPDataSource viburDataSource) {
telemetry.unregisterMetrics(viburDataSource);
}
}

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("org.vibur:vibur-dbcp:11.0")
}

View File

@ -0,0 +1,90 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.instrumentation.viburdbcp;
import static org.mockito.Mockito.when;
import io.opentelemetry.instrumentation.testing.internal.AutoCleanupExtension;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.db.DbConnectionPoolMetricsAssertions;
import java.sql.Connection;
import java.sql.SQLException;
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;
import org.vibur.dbcp.ViburDBCPDataSource;
@ExtendWith(MockitoExtension.class)
public abstract class AbstractViburInstrumentationTest {
@RegisterExtension static final AutoCleanupExtension cleanup = AutoCleanupExtension.create();
@Mock DataSource dataSourceMock;
@Mock Connection connectionMock;
protected abstract InstrumentationExtension testing();
protected abstract void configure(ViburDBCPDataSource viburDataSource);
protected abstract void shutdown(ViburDBCPDataSource viburDataSource);
@Test
void shouldReportMetrics() throws SQLException, InterruptedException {
// given
when(dataSourceMock.getConnection()).thenReturn(connectionMock);
ViburDBCPDataSource viburDataSource = new ViburDBCPDataSource();
viburDataSource.setExternalDataSource(dataSourceMock);
viburDataSource.setName("testPool");
configure(viburDataSource);
viburDataSource.start();
// when
Connection viburConnection = viburDataSource.getConnection();
TimeUnit.MILLISECONDS.sleep(100);
viburConnection.close();
// then
DbConnectionPoolMetricsAssertions.create(
testing(), "io.opentelemetry.viburdbcp-11.0", "testPool")
.disableMinIdleConnections()
.disableMaxIdleConnections()
.disablePendingRequests()
.disableConnectionTimeouts()
.disableCreateTime()
.disableWaitTime()
.disableUseTime()
.assertConnectionPoolEmitsMetrics();
// when
// this one too shouldn't cause any problems when called more than once
viburDataSource.close();
viburDataSource.close();
shutdown(viburDataSource);
// sleep exporter interval
Thread.sleep(100);
testing().clearData();
Thread.sleep(100);
// then
testing()
.waitAndAssertMetrics(
"io.opentelemetry.viburdbcp-11.0",
"db.client.connections.usage",
AbstractIterableAssert::isEmpty);
testing()
.waitAndAssertMetrics(
"io.opentelemetry.viburdbcp-11.0",
"db.client.connections.max",
AbstractIterableAssert::isEmpty);
}
}

View File

@ -442,6 +442,9 @@ include(":instrumentation:vertx:vertx-kafka-client-3.6:testing")
include(":instrumentation:vertx:vertx-rx-java-3.5:javaagent")
include(":instrumentation:vertx:vertx-web-3.0:javaagent")
include(":instrumentation:vertx:vertx-web-3.0:testing")
include(":instrumentation:vibur-dbcp-11.0:javaagent")
include(":instrumentation:vibur-dbcp-11.0:library")
include(":instrumentation:vibur-dbcp-11.0:testing")
include(":instrumentation:wicket-8.0:javaagent")
// benchmark

View File

@ -22,8 +22,13 @@ public final class DbConnectionPoolMetricsAssertions {
private final String instrumentationName;
private final String poolName;
private boolean testMinIdleConnections = true;
private boolean testMaxIdleConnections = true;
private boolean testPendingRequests = true;
private boolean testConnectionTimeouts = true;
private boolean testCreateTime = true;
private boolean testWaitTime = true;
private boolean testUseTime = true;
DbConnectionPoolMetricsAssertions(
InstrumentationExtension testing, String instrumentationName, String poolName) {
@ -32,16 +37,41 @@ public final class DbConnectionPoolMetricsAssertions {
this.poolName = poolName;
}
public DbConnectionPoolMetricsAssertions disableMinIdleConnections() {
testMinIdleConnections = false;
return this;
}
public DbConnectionPoolMetricsAssertions disableMaxIdleConnections() {
testMaxIdleConnections = false;
return this;
}
public DbConnectionPoolMetricsAssertions disablePendingRequests() {
testPendingRequests = false;
return this;
}
public DbConnectionPoolMetricsAssertions disableConnectionTimeouts() {
testConnectionTimeouts = false;
return this;
}
public DbConnectionPoolMetricsAssertions disableCreateTime() {
testCreateTime = false;
return this;
}
public DbConnectionPoolMetricsAssertions disableWaitTime() {
testWaitTime = false;
return this;
}
public DbConnectionPoolMetricsAssertions disableUseTime() {
testUseTime = false;
return this;
}
public void assertConnectionPoolEmitsMetrics() {
testing.waitAndAssertMetrics(
instrumentationName,
@ -71,23 +101,25 @@ public final class DbConnectionPoolMetricsAssertions {
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 (testMinIdleConnections) {
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,
@ -124,24 +156,26 @@ public final class DbConnectionPoolMetricsAssertions {
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 (testPendingRequests) {
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,
@ -162,52 +196,58 @@ public final class DbConnectionPoolMetricsAssertions {
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))))));
if (testCreateTime) {
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))))));
}
if (testWaitTime) {
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))))));
}
if (testUseTime) {
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))))));
}
}
}