diff --git a/.fossa.yml b/.fossa.yml index 82a27cd4b33a..c4a49ba7b8b8 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -61,6 +61,12 @@ targets: - type: gradle path: ./ target: ':instrumentation:alibaba-druid-1.0:library' + - type: gradle + path: ./ + target: ':instrumentation:apache-commons-pool-2.0:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:apache-commons-pool-2.0:library' - type: gradle path: ./ target: ':instrumentation:apache-dbcp-2.0:javaagent' diff --git a/.github/config/latest-dep-versions.json b/.github/config/latest-dep-versions.json index 996187ab66b1..dc898faecfee 100644 --- a/.github/config/latest-dep-versions.json +++ b/.github/config/latest-dep-versions.json @@ -280,6 +280,7 @@ "org.apache.camel:camel-undertow#2.+": "2.25.4", "org.apache.cassandra:java-driver-core#+": "4.19.3", "org.apache.commons:commons-dbcp2#+": "2.14.0", + "org.apache.commons:commons-pool2#+": "2.13.1", "org.apache.cxf:cxf-rt-frontend-jaxrs#+": "4.2.2", "org.apache.cxf:cxf-rt-frontend-jaxrs#3.+": "3.6.11", "org.apache.cxf:cxf-rt-frontend-jaxws#+": "4.2.2", diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 0d4feee7800f..1ab70df3d255 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -25,6 +25,7 @@ These are the supported libraries and frameworks: | [Alibaba Druid](https://github.com/alibaba/druid) | 1.0+ | [opentelemetry-alibaba-druid-1.0](../instrumentation/alibaba-druid-1.0/library) | [Database Pool Metrics] | | [Apache Axis2](https://axis.apache.org/axis2/java/core/) | 1.6+ | N/A | Provides `http.route` [2], Controller Spans [3] | | [Apache Camel](https://camel.apache.org/) | 2.20+ (not including 3.0+ yet) | N/A | Dependent on components in use | +| [Apache Commons Pool](https://commons.apache.org/proper/commons-pool/) | 2.0+ (disabled by default) | [opentelemetry-apache-commons-pool-2.0](../instrumentation/apache-commons-pool-2.0/library) | none | | [Apache CXF JAX-RS](https://cxf.apache.org/) | 3.2+ (not including 4.0+ yet) | N/A | Provides `http.route` [2], Controller Spans [3] | | [Apache CXF JAX-WS](https://cxf.apache.org/) | 3.0+ (not including 4.0+ yet) | N/A | Provides `http.route` [2], Controller Spans [3] | | [Apache DBCP](https://commons.apache.org/proper/commons-dbcp/) | 2.0+ | [opentelemetry-apache-dbcp-2.0](../instrumentation/apache-dbcp-2.0/library) | [Database Pool Metrics] | @@ -251,6 +252,8 @@ Some instrumentations can produce too many spans and metrics and thus create a l For this reason, the following instrumentations are disabled by default: - `jdbc-datasource` which creates spans whenever the `java.sql.DataSource#getConnection` method is called. +- `apache-commons-pool` which emits Apache Commons Pool object pool metrics that do not currently follow semantic + conventions. - `dropwizard-metrics` which might create a very low quality metrics data, because of lack of label/attribute support in the Dropwizard metrics API. - `micrometer` which might create high number of metrics due to being broadly used in libraries. @@ -258,5 +261,6 @@ For this reason, the following instrumentations are disabled by default: To enable them, add the `otel.instrumentation..enabled` system property: - `-Dotel.instrumentation.jdbc-datasource.enabled=true` +- `-Dotel.instrumentation.apache-commons-pool.enabled=true` - `-Dotel.instrumentation.dropwizard-metrics.enabled=true` - `-Dotel.instrumentation.micrometer.enabled=true` diff --git a/instrumentation/apache-commons-pool-2.0/javaagent/build.gradle.kts b/instrumentation/apache-commons-pool-2.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..d8babad9f2d2 --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/javaagent/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("org.apache.commons") + module.set("commons-pool2") + versions.set("[2.0,)") + assertInverse.set(true) + } +} + +dependencies { + library("org.apache.commons:commons-pool2:2.0") + + implementation(project(":instrumentation:apache-commons-pool-2.0:library")) + + testImplementation(project(":instrumentation:apache-commons-pool-2.0:testing")) +} + +tasks { + withType().configureEach { + systemProperty("collectMetadata", otelProps.collectMetadata) + jvmArgs("-Dotel.instrumentation.apache-commons-pool.enabled=true") + systemProperty("metadataConfig", "otel.instrumentation.apache-commons-pool.enabled=true") + } +} diff --git a/instrumentation/apache-commons-pool-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/BaseGenericObjectPoolInstrumentation.java b/instrumentation/apache-commons-pool-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/BaseGenericObjectPoolInstrumentation.java new file mode 100644 index 000000000000..ca99966706e9 --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/BaseGenericObjectPoolInstrumentation.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecommonspool.v2_0; + +import static io.opentelemetry.javaagent.instrumentation.apachecommonspool.v2_0.CommonsPoolSingletons.telemetry; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; +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 javax.management.ObjectName; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import org.apache.commons.pool2.impl.BaseGenericObjectPool; +import org.apache.commons.pool2.impl.GenericKeyedObjectPool; +import org.apache.commons.pool2.impl.GenericObjectPool; + +class BaseGenericObjectPoolInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("org.apache.commons.pool2.impl.BaseGenericObjectPool"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor() + .and(takesArguments(3)) + .and(takesArgument(0, named("org.apache.commons.pool2.impl.BaseObjectPoolConfig"))) + .and(takesArgument(1, String.class)) + .and(takesArgument(2, String.class)), + getClass().getName() + "$ConstructorAdvice"); + + transformer.applyAdviceToMethod( + named("jmxUnregister").and(takesArguments(0)), + getClass().getName() + "$JmxUnregisterAdvice"); + } + + @SuppressWarnings("unused") + public static class ConstructorAdvice { + @Advice.OnMethodExit(suppress = Throwable.class) + public static void onExit( + @Advice.This BaseGenericObjectPool pool, @Advice.Argument(2) String jmxNamePrefix) { + ObjectName objectName = pool.getJmxName(); + + String type = objectName == null ? null : objectName.getKeyProperty("type"); + if (type == null) { + if (pool instanceof GenericKeyedObjectPool) { + type = "GenericKeyedObjectPool"; + } else if (pool instanceof GenericObjectPool) { + type = "GenericObjectPool"; + } else { + type = pool.getClass().getSimpleName(); + } + } + + String name = objectName == null ? null : objectName.getKeyProperty("name"); + if (name == null) { + name = jmxNamePrefix == null ? "unknown" : jmxNamePrefix; + } + + telemetry().registerMetrics(pool, type + "-" + name); + } + } + + @SuppressWarnings("unused") + public static class JmxUnregisterAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void onExit(@Advice.This BaseGenericObjectPool pool) { + telemetry().unregisterMetrics(pool); + } + } +} diff --git a/instrumentation/apache-commons-pool-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/CommonsPoolInstrumentationModule.java b/instrumentation/apache-commons-pool-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/CommonsPoolInstrumentationModule.java new file mode 100644 index 000000000000..a8e2dc27ed66 --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/CommonsPoolInstrumentationModule.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecommonspool.v2_0; + +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 CommonsPoolInstrumentationModule extends InstrumentationModule { + public CommonsPoolInstrumentationModule() { + super("apache-commons-pool", "apache-commons-pool-2.0"); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @Override + public List typeInstrumentations() { + return singletonList(new BaseGenericObjectPoolInstrumentation()); + } +} diff --git a/instrumentation/apache-commons-pool-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/CommonsPoolSingletons.java b/instrumentation/apache-commons-pool-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/CommonsPoolSingletons.java new file mode 100644 index 000000000000..25d54e573889 --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/CommonsPoolSingletons.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecommonspool.v2_0; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.apachecommonspool.v2_0.CommonsPoolTelemetry; + +public class CommonsPoolSingletons { + + private static final CommonsPoolTelemetry telemetry = + CommonsPoolTelemetry.create(GlobalOpenTelemetry.get()); + + public static CommonsPoolTelemetry telemetry() { + return telemetry; + } + + private CommonsPoolSingletons() {} +} diff --git a/instrumentation/apache-commons-pool-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/CommonsPoolInstrumentationTest.java b/instrumentation/apache-commons-pool-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/CommonsPoolInstrumentationTest.java new file mode 100644 index 000000000000..6858f3789292 --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecommonspool/v2_0/CommonsPoolInstrumentationTest.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.apachecommonspool.v2_0; + +import io.opentelemetry.instrumentation.apachecommonspool.AbstractCommonsPoolInstrumentationTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.apache.commons.pool2.impl.GenericKeyedObjectPool; +import org.apache.commons.pool2.impl.GenericObjectPool; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class CommonsPoolInstrumentationTest extends AbstractCommonsPoolInstrumentationTest { + + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected void configure(GenericObjectPool pool, String poolName) {} + + @Override + protected void configure(GenericKeyedObjectPool pool, String poolName) {} + + @Override + protected void shutdown(GenericObjectPool pool) {} + + @Override + protected void shutdown(GenericKeyedObjectPool pool) {} + + @Test + void shouldUseJmxNamePrefixWhenJmxNameIsUnavailable() throws Exception { + GenericObjectPool pool = createGenericObjectPool("customPool", false); + Object borrowed = null; + try { + borrowed = pool.borrowObject(); + + assertObjectCountPoolNames("GenericObjectPool-customPool"); + } finally { + if (borrowed != null) { + pool.returnObject(borrowed); + } + pool.close(); + } + + assertNoMetrics(); + } + + @Test + void shouldUseUnknownWhenJmxNameAndPrefixAreUnavailable() throws Exception { + GenericObjectPool pool = createGenericObjectPool(null, false); + Object borrowed = null; + try { + borrowed = pool.borrowObject(); + + assertObjectCountPoolNames("GenericObjectPool-unknown"); + } finally { + if (borrowed != null) { + pool.returnObject(borrowed); + } + pool.close(); + } + + assertNoMetrics(); + } +} diff --git a/instrumentation/apache-commons-pool-2.0/library/README.md b/instrumentation/apache-commons-pool-2.0/library/README.md new file mode 100644 index 000000000000..39d554028f23 --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/library/README.md @@ -0,0 +1,51 @@ +# Library Instrumentation for Apache Commons Pool version 2.0 and higher + +Provides OpenTelemetry instrumentation for [Apache Commons Pool](https://commons.apache.org/proper/commons-pool/). + +## Quickstart + +### Add these dependencies to your project + +Replace `OPENTELEMETRY_VERSION` with the latest release. + +For Maven, add to your `pom.xml` dependencies: + +```xml + + io.opentelemetry.instrumentation + opentelemetry-apache-commons-pool-2.0 + OPENTELEMETRY_VERSION + +``` + +For Gradle, add to your dependencies: + +```groovy +implementation("io.opentelemetry.instrumentation:opentelemetry-apache-commons-pool-2.0:OPENTELEMETRY_VERSION") +``` + +### Usage + +Register supported `GenericObjectPool` or `GenericKeyedObjectPool` instances for metrics collection. +The pool name must be stable and unique. + +```java +CommonsPoolTelemetry telemetry = CommonsPoolTelemetry.create(openTelemetry); + +telemetry.registerMetrics(pool, "my-pool"); + +// When done: +telemetry.unregisterMetrics(pool); +``` + +## Metrics + +These metrics are Apache Commons Pool specific and do not currently follow OpenTelemetry semantic conventions. + +| Metric | Description | Unit | +| --------------------------------------------- | ------------------------------------------------------------------------------ | ----------- | +| `apache.commons_pool.object.count` | The number of objects currently in the state described by the state attribute. | `{object}` | +| `apache.commons_pool.object.idle.min` | The minimum number of idle objects allowed in the pool. | `{object}` | +| `apache.commons_pool.object.idle.max` | The maximum number of idle objects allowed in the pool. | `{object}` | +| `apache.commons_pool.object.max` | The maximum number of objects allowed in the pool. | `{object}` | +| `apache.commons_pool.object.pending_requests` | The number of requests currently waiting for an object from the pool. | `{request}` | diff --git a/instrumentation/apache-commons-pool-2.0/library/build.gradle.kts b/instrumentation/apache-commons-pool-2.0/library/build.gradle.kts new file mode 100644 index 000000000000..443fb48c7bed --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/library/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("otel.library-instrumentation") + id("otel.nullaway-conventions") +} + +dependencies { + library("org.apache.commons:commons-pool2:2.0") + + testImplementation(project(":instrumentation:apache-commons-pool-2.0:testing")) +} diff --git a/instrumentation/apache-commons-pool-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/CommonsPoolMetrics.java b/instrumentation/apache-commons-pool-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/CommonsPoolMetrics.java new file mode 100644 index 000000000000..013a9726619a --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/CommonsPoolMetrics.java @@ -0,0 +1,134 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachecommonspool.v2_0; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.BatchCallback; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.IntSupplier; +import javax.annotation.Nullable; +import org.apache.commons.pool2.impl.GenericKeyedObjectPoolMXBean; +import org.apache.commons.pool2.impl.GenericObjectPoolMXBean; + +final class CommonsPoolMetrics { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.apache-commons-pool-2.0"; + + // a weak map does not make sense here because each Meter holds a reference to the pool + // use identity comparison because pools are mutable lifecycle objects + private static final Map poolMetrics = new ConcurrentHashMap<>(); + + static void registerMetrics(OpenTelemetry openTelemetry, Object pool, String poolName) { + if (pool instanceof GenericObjectPoolMXBean) { + GenericObjectPoolMXBean objectPool = (GenericObjectPoolMXBean) pool; + registerMetrics( + openTelemetry, + objectPool, + poolName, + objectPool::getNumActive, + objectPool::getNumIdle, + objectPool::getMinIdle, + objectPool::getMaxIdle, + objectPool::getMaxTotal, + objectPool::getNumWaiters); + } else if (pool instanceof GenericKeyedObjectPoolMXBean) { + GenericKeyedObjectPoolMXBean keyedPool = (GenericKeyedObjectPoolMXBean) pool; + registerMetrics( + openTelemetry, + keyedPool, + poolName, + keyedPool::getNumActive, + keyedPool::getNumIdle, + keyedPool::getMinIdlePerKey, + keyedPool::getMaxIdlePerKey, + keyedPool::getMaxTotal, + keyedPool::getNumWaiters); + } + } + + private static void registerMetrics( + OpenTelemetry openTelemetry, + Object pool, + String poolName, + IntSupplier active, + IntSupplier idle, + IntSupplier minIdle, + IntSupplier maxIdle, + IntSupplier maxTotal, + IntSupplier waiters) { + poolMetrics.computeIfAbsent( + new IdentityPoolKey(pool), + unused -> + createCallback( + openTelemetry, poolName, active, idle, minIdle, maxIdle, maxTotal, waiters)); + } + + private static BatchCallback createCallback( + OpenTelemetry openTelemetry, + String poolName, + IntSupplier active, + IntSupplier idle, + IntSupplier minIdle, + IntSupplier maxIdle, + IntSupplier maxTotal, + IntSupplier waiters) { + ObjectPoolMetrics metrics = + ObjectPoolMetrics.create(openTelemetry, INSTRUMENTATION_NAME, poolName); + + ObservableLongMeasurement objects = metrics.objects(); + ObservableLongMeasurement minIdleObjects = metrics.minIdleObjects(); + ObservableLongMeasurement maxIdleObjects = metrics.maxIdleObjects(); + ObservableLongMeasurement maxObjects = metrics.maxObjects(); + ObservableLongMeasurement pendingRequests = metrics.pendingRequestsForObject(); + + Attributes attributes = metrics.getAttributes(); + + return metrics.batchCallback( + () -> { + objects.record(active.getAsInt(), metrics.getUsedObjectsAttributes()); + objects.record(idle.getAsInt(), metrics.getIdleObjectsAttributes()); + minIdleObjects.record(minIdle.getAsInt(), attributes); + maxIdleObjects.record(maxIdle.getAsInt(), attributes); + maxObjects.record(maxTotal.getAsInt(), attributes); + pendingRequests.record(waiters.getAsInt(), attributes); + }, + objects, + minIdleObjects, + maxIdleObjects, + maxObjects, + pendingRequests); + } + + static void unregisterMetrics(Object pool) { + BatchCallback callback = poolMetrics.remove(new IdentityPoolKey(pool)); + if (callback != null) { + callback.close(); + } + } + + private static final class IdentityPoolKey { + private final Object pool; + + private IdentityPoolKey(Object pool) { + this.pool = pool; + } + + @Override + @SuppressWarnings("ReferenceEquality") + public boolean equals(@Nullable Object other) { + return other instanceof IdentityPoolKey && pool == ((IdentityPoolKey) other).pool; + } + + @Override + public int hashCode() { + return System.identityHashCode(pool); + } + } + + private CommonsPoolMetrics() {} +} diff --git a/instrumentation/apache-commons-pool-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/CommonsPoolTelemetry.java b/instrumentation/apache-commons-pool-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/CommonsPoolTelemetry.java new file mode 100644 index 000000000000..9fb70d6f2746 --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/CommonsPoolTelemetry.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachecommonspool.v2_0; + +import io.opentelemetry.api.OpenTelemetry; + +/** Entrypoint for instrumenting Apache Commons Pool 2 object pools. */ +public final class CommonsPoolTelemetry { + private final OpenTelemetry openTelemetry; + + /** Returns a new {@link CommonsPoolTelemetry} configured with the given {@link OpenTelemetry}. */ + public static CommonsPoolTelemetry create(OpenTelemetry openTelemetry) { + return new CommonsPoolTelemetry(openTelemetry); + } + + private CommonsPoolTelemetry(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + /** Start collecting metrics for the given supported object pool. */ + public void registerMetrics(Object pool, String poolName) { + CommonsPoolMetrics.registerMetrics(openTelemetry, pool, poolName); + } + + /** Stop collecting metrics for the given supported object pool. */ + public void unregisterMetrics(Object pool) { + CommonsPoolMetrics.unregisterMetrics(pool); + } +} diff --git a/instrumentation/apache-commons-pool-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/ObjectPoolMetrics.java b/instrumentation/apache-commons-pool-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/ObjectPoolMetrics.java new file mode 100644 index 000000000000..9865b6b668b0 --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/ObjectPoolMetrics.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachecommonspool.v2_0; + +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.BatchCallback; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterBuilder; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.api.metrics.ObservableMeasurement; +import io.opentelemetry.instrumentation.api.internal.EmbeddedInstrumentationProperties; + +final class ObjectPoolMetrics { + private static final AttributeKey POOL_NAME = stringKey("apache.commons_pool.pool.name"); + private static final AttributeKey OBJECT_STATE = + stringKey("apache.commons_pool.object.state"); + + private static final String STATE_IDLE = "idle"; + private static final String STATE_USED = "used"; + + static ObjectPoolMetrics 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 ObjectPoolMetrics(meterBuilder.build(), Attributes.of(POOL_NAME, poolName)); + } + + private final Meter meter; + private final Attributes attributes; + private final Attributes usedObjectsAttributes; + private final Attributes idleObjectsAttributes; + + private ObjectPoolMetrics(Meter meter, Attributes attributes) { + this.meter = meter; + this.attributes = attributes; + usedObjectsAttributes = attributes.toBuilder().put(OBJECT_STATE, STATE_USED).build(); + idleObjectsAttributes = attributes.toBuilder().put(OBJECT_STATE, STATE_IDLE).build(); + } + + ObservableLongMeasurement objects() { + return meter + .upDownCounterBuilder("apache.commons_pool.object.count") + .setUnit("{object}") + .setDescription( + "The number of objects currently in the state described by the state attribute.") + .buildObserver(); + } + + ObservableLongMeasurement minIdleObjects() { + return meter + .upDownCounterBuilder("apache.commons_pool.object.idle.min") + .setUnit("{object}") + .setDescription("The minimum number of idle objects allowed in the pool.") + .buildObserver(); + } + + ObservableLongMeasurement maxIdleObjects() { + return meter + .upDownCounterBuilder("apache.commons_pool.object.idle.max") + .setUnit("{object}") + .setDescription("The maximum number of idle objects allowed in the pool.") + .buildObserver(); + } + + ObservableLongMeasurement maxObjects() { + return meter + .upDownCounterBuilder("apache.commons_pool.object.max") + .setUnit("{object}") + .setDescription("The maximum number of objects allowed in the pool.") + .buildObserver(); + } + + ObservableLongMeasurement pendingRequestsForObject() { + return meter + .upDownCounterBuilder("apache.commons_pool.object.pending_requests") + .setUnit("{request}") + .setDescription("The number of requests currently waiting for an object from the pool.") + .buildObserver(); + } + + BatchCallback batchCallback( + Runnable callback, + ObservableMeasurement observableMeasurement, + ObservableMeasurement... additionalMeasurements) { + return meter.batchCallback(callback, observableMeasurement, additionalMeasurements); + } + + Attributes getAttributes() { + return attributes; + } + + Attributes getUsedObjectsAttributes() { + return usedObjectsAttributes; + } + + Attributes getIdleObjectsAttributes() { + return idleObjectsAttributes; + } +} diff --git a/instrumentation/apache-commons-pool-2.0/library/src/test/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/CommonsPoolInstrumentationTest.java b/instrumentation/apache-commons-pool-2.0/library/src/test/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/CommonsPoolInstrumentationTest.java new file mode 100644 index 000000000000..6e897a946367 --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/library/src/test/java/io/opentelemetry/instrumentation/apachecommonspool/v2_0/CommonsPoolInstrumentationTest.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachecommonspool.v2_0; + +import io.opentelemetry.instrumentation.apachecommonspool.AbstractCommonsPoolInstrumentationTest; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.apache.commons.pool2.impl.GenericKeyedObjectPool; +import org.apache.commons.pool2.impl.GenericObjectPool; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; + +class CommonsPoolInstrumentationTest extends AbstractCommonsPoolInstrumentationTest { + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + private static CommonsPoolTelemetry telemetry; + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @BeforeAll + static void setup() { + telemetry = CommonsPoolTelemetry.create(testing.getOpenTelemetry()); + } + + @Override + protected void configure(GenericObjectPool pool, String poolName) { + telemetry.registerMetrics(pool, poolName); + } + + @Override + protected void configure(GenericKeyedObjectPool pool, String poolName) { + telemetry.registerMetrics(pool, poolName); + } + + @Override + protected void shutdown(GenericObjectPool pool) { + telemetry.unregisterMetrics(pool); + } + + @Override + protected void shutdown(GenericKeyedObjectPool pool) { + telemetry.unregisterMetrics(pool); + } +} diff --git a/instrumentation/apache-commons-pool-2.0/metadata.yaml b/instrumentation/apache-commons-pool-2.0/metadata.yaml new file mode 100644 index 000000000000..93be5335a3db --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/metadata.yaml @@ -0,0 +1,12 @@ +display_name: Apache Commons Pool +description: > + This instrumentation enables object pool metrics for Apache Commons Pool + GenericObjectPool and GenericKeyedObjectPool instances. +library_link: https://commons.apache.org/proper/commons-pool/ +disabled_by_default: true +configurations: + - name: otel.instrumentation.apache-commons-pool.enabled + declarative_name: java.apache_commons_pool.enabled + description: Enables the Apache Commons Pool instrumentation. + type: boolean + default: false diff --git a/instrumentation/apache-commons-pool-2.0/testing/build.gradle.kts b/instrumentation/apache-commons-pool-2.0/testing/build.gradle.kts new file mode 100644 index 000000000000..daea0538aebc --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/testing/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api("io.opentelemetry.javaagent:opentelemetry-testing-common") + + compileOnly("org.apache.commons:commons-pool2:2.0") +} diff --git a/instrumentation/apache-commons-pool-2.0/testing/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/AbstractCommonsPoolInstrumentationTest.java b/instrumentation/apache-commons-pool-2.0/testing/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/AbstractCommonsPoolInstrumentationTest.java new file mode 100644 index 000000000000..a01ce4dd7989 --- /dev/null +++ b/instrumentation/apache-commons-pool-2.0/testing/src/main/java/io/opentelemetry/instrumentation/apachecommonspool/AbstractCommonsPoolInstrumentationTest.java @@ -0,0 +1,344 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.apachecommonspool; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.awaitility.Awaitility.await; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.assertj.LongPointAssert; +import io.opentelemetry.sdk.testing.assertj.LongSumAssert; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import org.apache.commons.pool2.BaseKeyedPooledObjectFactory; +import org.apache.commons.pool2.BasePooledObjectFactory; +import org.apache.commons.pool2.PooledObject; +import org.apache.commons.pool2.impl.DefaultPooledObject; +import org.apache.commons.pool2.impl.GenericKeyedObjectPool; +import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig; +import org.apache.commons.pool2.impl.GenericObjectPool; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.junit.jupiter.api.Test; + +public abstract class AbstractCommonsPoolInstrumentationTest { + + protected static final String INSTRUMENTATION_NAME = "io.opentelemetry.apache-commons-pool-2.0"; + private static final AttributeKey POOL_NAME = stringKey("apache.commons_pool.pool.name"); + private static final AttributeKey OBJECT_STATE = + stringKey("apache.commons_pool.object.state"); + + protected abstract InstrumentationExtension testing(); + + protected abstract void configure(GenericObjectPool pool, String poolName) throws Exception; + + protected abstract void configure(GenericKeyedObjectPool pool, String poolName) + throws Exception; + + protected abstract void shutdown(GenericObjectPool pool) throws Exception; + + protected abstract void shutdown(GenericKeyedObjectPool pool) throws Exception; + + @Test + void shouldReportGenericObjectPoolMetrics() throws Exception { + testGenericObjectPoolMetrics(true); + } + + @Test + void shouldReportGenericObjectPoolMetricsWhenJmxDisabled() throws Exception { + testGenericObjectPoolMetrics(false); + } + + @Test + void shouldReusePoolNameAfterShutdown() throws Exception { + String poolName = "GenericObjectPool-pool"; + GenericObjectPool first = createGenericObjectPool("pool", false); + Object firstBorrowed = null; + try { + configure(first, poolName); + + firstBorrowed = first.borrowObject(); + + assertPoolMetrics(poolName); + } finally { + if (firstBorrowed != null) { + first.returnObject(firstBorrowed); + } + shutdown(first); + first.close(); + } + + assertNoMetrics(); + + GenericObjectPool second = createGenericObjectPool("pool", false); + Object secondBorrowed = null; + try { + configure(second, poolName); + + secondBorrowed = second.borrowObject(); + + assertPoolMetrics(poolName); + } finally { + if (secondBorrowed != null) { + second.returnObject(secondBorrowed); + } + shutdown(second); + second.close(); + } + + assertNoMetrics(); + } + + private void testGenericObjectPoolMetrics(boolean jmxEnabled) throws Exception { + GenericObjectPool pool = + createGenericObjectPool(jmxEnabled ? "objectPool" : "pool", jmxEnabled); + Object borrowed = null; + try { + String poolName = + "GenericObjectPool-" + (jmxEnabled ? pool.getJmxName().getKeyProperty("name") : "pool"); + configure(pool, poolName); + + borrowed = pool.borrowObject(); + + assertPoolMetrics(poolName); + } finally { + if (borrowed != null) { + pool.returnObject(borrowed); + } + shutdown(pool); + pool.close(); + } + + assertNoMetrics(); + } + + @Test + void shouldReportGenericKeyedObjectPoolMetrics() throws Exception { + testGenericKeyedObjectPoolMetrics(true); + } + + @Test + void shouldReportGenericKeyedObjectPoolMetricsWhenJmxDisabled() throws Exception { + testGenericKeyedObjectPoolMetrics(false); + } + + private void testGenericKeyedObjectPoolMetrics(boolean jmxEnabled) throws Exception { + GenericKeyedObjectPool pool = + createGenericKeyedObjectPool(jmxEnabled ? "keyedObjectPool" : "pool", jmxEnabled); + Object borrowed = null; + try { + String poolName = + "GenericKeyedObjectPool-" + + (jmxEnabled ? pool.getJmxName().getKeyProperty("name") : "pool"); + configure(pool, poolName); + + borrowed = pool.borrowObject("key"); + + assertPoolMetrics(poolName); + } finally { + if (borrowed != null) { + pool.returnObject("key", borrowed); + } + shutdown(pool); + pool.close(); + } + + assertNoMetrics(); + } + + protected static GenericObjectPool createGenericObjectPool( + String poolName, boolean jmxEnabled) { + GenericObjectPoolConfig config = new GenericObjectPoolConfig(); + config.setJmxEnabled(jmxEnabled); + config.setJmxNamePrefix(poolName); + config.setMaxTotal(10); + config.setMaxIdle(5); + config.setMinIdle(1); + return new GenericObjectPool<>(new TestObjectFactory(), config); + } + + private static GenericKeyedObjectPool createGenericKeyedObjectPool( + String poolName, boolean jmxEnabled) { + GenericKeyedObjectPoolConfig config = new GenericKeyedObjectPoolConfig(); + config.setJmxEnabled(jmxEnabled); + config.setJmxNamePrefix(poolName); + config.setMaxTotal(10); + config.setMaxTotalPerKey(5); + config.setMaxIdlePerKey(3); + config.setMinIdlePerKey(1); + return new GenericKeyedObjectPool<>(new TestKeyedObjectFactory(), config); + } + + private void assertPoolMetrics(String poolName) { + verifyObjectCount(poolName); + verifyMinIdleObjects(poolName); + verifyMaxIdleObjects(poolName); + verifyMaxObjects(poolName); + verifyPendingRequests(poolName); + } + + private void verifyObjectCount(String poolName) { + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "apache.commons_pool.object.count", + metrics -> metrics.anySatisfy(metric -> verifyObjectCountMetric(metric, poolName))); + } + + protected void assertObjectCountPoolNames(String... poolNames) { + List> assertions = new ArrayList<>(); + for (String poolName : poolNames) { + assertions.add( + point -> + point.hasAttributesSatisfying( + equalTo(POOL_NAME, poolName), equalTo(OBJECT_STATE, "idle"))); + assertions.add( + point -> + point.hasAttributesSatisfying( + equalTo(POOL_NAME, poolName), equalTo(OBJECT_STATE, "used"))); + } + + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "apache.commons_pool.object.count", + metrics -> + metrics.anySatisfy( + metric -> + assertThat(metric) + .hasLongSumSatisfying( + sum -> sum.isNotMonotonic().hasPointsSatisfying(assertions)))); + } + + private static void verifyObjectCountMetric(MetricData metric, String poolName) { + assertThat(metric) + .hasUnit("{object}") + .hasDescription( + "The number of objects currently in the state described by the state attribute.") + .hasLongSumSatisfying( + sum -> + sum.isNotMonotonic() + .hasPointsSatisfying( + point -> + point.hasAttributesSatisfying( + equalTo(POOL_NAME, poolName), equalTo(OBJECT_STATE, "idle")), + point -> + point.hasAttributesSatisfying( + equalTo(POOL_NAME, poolName), equalTo(OBJECT_STATE, "used")))); + } + + private void verifyMinIdleObjects(String poolName) { + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "apache.commons_pool.object.idle.min", + metrics -> metrics.anySatisfy(metric -> verifyMinIdleObjectsMetric(metric, poolName))); + } + + private static void verifyMinIdleObjectsMetric(MetricData metric, String poolName) { + assertThat(metric) + .hasUnit("{object}") + .hasDescription("The minimum number of idle objects allowed in the pool.") + .hasLongSumSatisfying(sum -> verifyPoolName(sum, poolName)); + } + + private void verifyMaxIdleObjects(String poolName) { + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "apache.commons_pool.object.idle.max", + metrics -> metrics.anySatisfy(metric -> verifyMaxIdleObjectsMetric(metric, poolName))); + } + + private static void verifyMaxIdleObjectsMetric(MetricData metric, String poolName) { + assertThat(metric) + .hasUnit("{object}") + .hasDescription("The maximum number of idle objects allowed in the pool.") + .hasLongSumSatisfying(sum -> verifyPoolName(sum, poolName)); + } + + private void verifyMaxObjects(String poolName) { + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "apache.commons_pool.object.max", + metrics -> metrics.anySatisfy(metric -> verifyMaxObjectsMetric(metric, poolName))); + } + + private static void verifyMaxObjectsMetric(MetricData metric, String poolName) { + assertThat(metric) + .hasUnit("{object}") + .hasDescription("The maximum number of objects allowed in the pool.") + .hasLongSumSatisfying(sum -> verifyPoolName(sum, poolName)); + } + + private void verifyPendingRequests(String poolName) { + testing() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + "apache.commons_pool.object.pending_requests", + metrics -> metrics.anySatisfy(metric -> verifyPendingRequestsMetric(metric, poolName))); + } + + private static void verifyPendingRequestsMetric(MetricData metric, String poolName) { + assertThat(metric) + .hasUnit("{request}") + .hasDescription("The number of requests currently waiting for an object from the pool.") + .hasLongSumSatisfying(sum -> verifyPoolName(sum, poolName)); + } + + private static void verifyPoolName(LongSumAssert sum, String poolName) { + sum.isNotMonotonic() + .hasPointsSatisfying(point -> point.hasAttributes(Attributes.of(POOL_NAME, poolName))); + } + + protected void assertNoMetrics() { + testing().clearData(); + + await() + .untilAsserted( + () -> + assertThat(testing().metrics()) + .filteredOn( + metricData -> + metricData + .getInstrumentationScopeInfo() + .getName() + .equals(INSTRUMENTATION_NAME)) + .isEmpty()); + } + + private static class TestObjectFactory extends BasePooledObjectFactory { + + @Override + public Object create() { + return new Object(); + } + + @Override + public PooledObject wrap(Object testObject) { + return new DefaultPooledObject<>(testObject); + } + } + + private static class TestKeyedObjectFactory extends BaseKeyedPooledObjectFactory { + + @Override + public Object create(String key) { + return new Object(); + } + + @Override + public PooledObject wrap(Object value) { + return new DefaultPooledObject<>(value); + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 8b34f4ee2dd8..8bef2084eb21 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -188,6 +188,9 @@ include(":instrumentation:akka:akka-http-10.0:javaagent") include(":instrumentation:alibaba-druid-1.0:javaagent") include(":instrumentation:alibaba-druid-1.0:library") include(":instrumentation:alibaba-druid-1.0:testing") +include(":instrumentation:apache-commons-pool-2.0:javaagent") +include(":instrumentation:apache-commons-pool-2.0:library") +include(":instrumentation:apache-commons-pool-2.0:testing") include(":instrumentation:apache-dbcp-2.0:javaagent") include(":instrumentation:apache-dbcp-2.0:library") include(":instrumentation:apache-dbcp-2.0:testing")