瀏覽代碼

Implement Apache DBCP2 datasource metrics (#6175)

* Apache DBCP2 datasource metrics

* Fix issues from PR

* Add readme for autoinstrumentation
Ago Allikmaa 2 年之前
父節點
當前提交
fae88de680
共有 14 個文件被更改,包括 462 次插入0 次删除
  1. 9 0
      instrumentation/apache-dbcp-2.0/javaagent/README.md
  2. 20 0
      instrumentation/apache-dbcp-2.0/javaagent/build.gradle.kts
  3. 25 0
      instrumentation/apache-dbcp-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/ApacheDbcpInstrumentationModule.java
  4. 21 0
      instrumentation/apache-dbcp-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/ApacheDbcpSingletons.java
  5. 64 0
      instrumentation/apache-dbcp-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/BasicDataSourceInstrumentation.java
  6. 37 0
      instrumentation/apache-dbcp-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/ApacheDbcpInstrumentationTest.java
  7. 47 0
      instrumentation/apache-dbcp-2.0/library/README.md
  8. 10 0
      instrumentation/apache-dbcp-2.0/library/build.gradle.kts
  9. 34 0
      instrumentation/apache-dbcp-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachedbcp/ApacheDbcpTelemetry.java
  10. 52 0
      instrumentation/apache-dbcp-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachedbcp/DataSourceMetrics.java
  11. 40 0
      instrumentation/apache-dbcp-2.0/library/src/test/java/io/opentelemetry/instrumentation/apachedbcp/ApacheDbcpInstrumentationTest.java
  12. 11 0
      instrumentation/apache-dbcp-2.0/testing/build.gradle.kts
  13. 89 0
      instrumentation/apache-dbcp-2.0/testing/src/main/java/io/opentelemetry/instrumentation/apachedbcp/AbstractApacheDbcpInstrumentationTest.java
  14. 3 0
      settings.gradle.kts

+ 9 - 0
instrumentation/apache-dbcp-2.0/javaagent/README.md

@@ -0,0 +1,9 @@
+# Auto-instrumentation for Apache DBCP
+
+Provides OpenTelemetry auto-instrumentation
+for [Apache DBCP](https://commons.apache.org/proper/commons-dbcp/).
+
+This auto-instrumentation uses `MBeanRegistration` methods for lifecycle detection, therefore it
+only activates if the `BasicDataSource` is registered to an `MBeanServer`. If using Spring Boot,
+this happens automatically as all Spring beans that support JMX registration are automatically
+registered by default.

+ 20 - 0
instrumentation/apache-dbcp-2.0/javaagent/build.gradle.kts

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

+ 25 - 0
instrumentation/apache-dbcp-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/ApacheDbcpInstrumentationModule.java

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

+ 21 - 0
instrumentation/apache-dbcp-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/ApacheDbcpSingletons.java

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

+ 64 - 0
instrumentation/apache-dbcp-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/BasicDataSourceInstrumentation.java

@@ -0,0 +1,64 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.apachedbcp;
+
+import static io.opentelemetry.javaagent.instrumentation.apachedbcp.ApacheDbcpSingletons.telemetry;
+import static net.bytebuddy.matcher.ElementMatchers.isPublic;
+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 javax.management.ObjectName;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.apache.commons.dbcp2.BasicDataSource;
+
+class BasicDataSourceInstrumentation implements TypeInstrumentation {
+  @Override
+  public ElementMatcher<TypeDescription> typeMatcher() {
+    return named("org.apache.commons.dbcp2.BasicDataSource");
+  }
+
+  @Override
+  public void transform(TypeTransformer typeTransformer) {
+    typeTransformer.applyAdviceToMethod(
+        isPublic().and(named("preRegister")).and(takesArguments(2)),
+        this.getClass().getName() + "$PreRegisterAdvice");
+
+    typeTransformer.applyAdviceToMethod(
+        isPublic().and(named("postDeregister")),
+        this.getClass().getName() + "$PostDeregisterAdvice");
+  }
+
+  @SuppressWarnings("unused")
+  public static class PreRegisterAdvice {
+    @Advice.OnMethodExit(suppress = Throwable.class)
+    public static void onExit(
+        @Advice.This BasicDataSource dataSource, @Advice.Return ObjectName objectName) {
+      String dataSourceName;
+      if (objectName != null) {
+        dataSourceName = objectName.getKeyProperty("name");
+        if (dataSourceName == null) {
+          dataSourceName = objectName.toString();
+        }
+      } else {
+        // fallback just in case it is somehow registered without a name
+        dataSourceName = "dbcp2-" + System.identityHashCode(dataSource);
+      }
+      telemetry().registerMetrics(dataSource, dataSourceName);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  public static class PostDeregisterAdvice {
+    @Advice.OnMethodExit(suppress = Throwable.class)
+    public static void onExit(@Advice.This BasicDataSource dataSource) {
+      telemetry().unregisterMetrics(dataSource);
+    }
+  }
+}

+ 37 - 0
instrumentation/apache-dbcp-2.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachedbcp/ApacheDbcpInstrumentationTest.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.javaagent.instrumentation.apachedbcp;
+
+import io.opentelemetry.instrumentation.apachedbcp.AbstractApacheDbcpInstrumentationTest;
+import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import java.lang.management.ManagementFactory;
+import javax.management.ObjectName;
+import org.apache.commons.dbcp2.BasicDataSource;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class ApacheDbcpInstrumentationTest extends AbstractApacheDbcpInstrumentationTest {
+
+  @RegisterExtension
+  static final InstrumentationExtension testing = AgentInstrumentationExtension.create();
+
+  @Override
+  protected InstrumentationExtension testing() {
+    return testing;
+  }
+
+  @Override
+  protected void configure(BasicDataSource dataSource, String dataSourceName) throws Exception {
+    dataSource.preRegister(
+        ManagementFactory.getPlatformMBeanServer(),
+        new ObjectName("io.opentelemetry.db:name=" + dataSourceName));
+  }
+
+  @Override
+  protected void shutdown(BasicDataSource dataSource) {
+    dataSource.postDeregister();
+  }
+}

+ 47 - 0
instrumentation/apache-dbcp-2.0/library/README.md

@@ -0,0 +1,47 @@
+# Manual Instrumentation for Apache DBCP
+
+Provides OpenTelemetry instrumentation for [Apache DBCP](https://commons.apache.org/proper/commons-dbcp/).
+
+## 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-apache-dbcp-2.0</artifactId>
+    <version>OPENTELEMETRY_VERSION</version>
+  </dependency>
+</dependencies>
+```
+
+For Gradle, add to your dependencies:
+
+```groovy
+implementation("io.opentelemetry.instrumentation:opentelemetry-apache-dbcp-2.0:OPENTELEMETRY_VERSION")
+```
+
+### Usage
+
+The instrumentation library allows registering `BasicDataSourceMXBean` instances for collecting
+OpenTelemetry-based metrics. A non-null name of the data source must be explicitly provided.
+
+```java
+ApacheDbcpTelemetry apacheDbcpTelemetry;
+
+void configure(OpenTelemetry openTelemetry, BasicDataSourceMXBean dataSource, String dataSourceName) {
+  apacheDbcpTelemetry = ApacheDbcpTelemetry.create(openTelemetry);
+  apacheDbcpTelemetry.registerMetrics(dataSource, dataSourceName);
+}
+
+void destroy(BasicDataSourceMXBean dataSource) {
+  apacheDbcpTelemetry.unregisterMetrics(dataSource);
+}
+```

+ 10 - 0
instrumentation/apache-dbcp-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-dbcp2:2.0")
+
+  testImplementation(project(":instrumentation:apache-dbcp-2.0:testing"))
+}

+ 34 - 0
instrumentation/apache-dbcp-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachedbcp/ApacheDbcpTelemetry.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.apachedbcp;
+
+import io.opentelemetry.api.OpenTelemetry;
+import org.apache.commons.dbcp2.BasicDataSourceMXBean;
+
+/** Entrypoint for instrumenting Apache DBCP database connection pools. */
+public final class ApacheDbcpTelemetry {
+
+  /** Returns a new {@link ApacheDbcpTelemetry} configured with the given {@link OpenTelemetry}. */
+  public static ApacheDbcpTelemetry create(OpenTelemetry openTelemetry) {
+    return new ApacheDbcpTelemetry(openTelemetry);
+  }
+
+  private final OpenTelemetry openTelemetry;
+
+  private ApacheDbcpTelemetry(OpenTelemetry openTelemetry) {
+    this.openTelemetry = openTelemetry;
+  }
+
+  /** Start collecting metrics for given connection pool. */
+  public void registerMetrics(BasicDataSourceMXBean dataSource, String dataSourceName) {
+    DataSourceMetrics.registerMetrics(openTelemetry, dataSource, dataSourceName);
+  }
+
+  /** Stop collecting metrics for given connection pool. */
+  public void unregisterMetrics(BasicDataSourceMXBean dataSource) {
+    DataSourceMetrics.unregisterMetrics(dataSource);
+  }
+}

+ 52 - 0
instrumentation/apache-dbcp-2.0/library/src/main/java/io/opentelemetry/instrumentation/apachedbcp/DataSourceMetrics.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.apachedbcp;
+
+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.apache.commons.dbcp2.BasicDataSourceMXBean;
+
+final class DataSourceMetrics {
+  private static final String INSTRUMENTATION_NAME = "io.opentelemetry.apache-dbcp-2.0";
+
+  // a weak map does not make sense here because each Meter holds a reference to the dataSource
+  // all instrumented/known implementations of BasicDataSourceMXBean do not implement
+  // equals()/hashCode(), so it's safe to keep them in a plain ConcurrentHashMap
+  private static final Map<BasicDataSourceMXBean, List<ObservableLongUpDownCounter>>
+      dataSourceMetrics = new ConcurrentHashMap<>();
+
+  public static void registerMetrics(
+      OpenTelemetry openTelemetry, BasicDataSourceMXBean dataSource, String dataSourceName) {
+    DbConnectionPoolMetrics metrics =
+        DbConnectionPoolMetrics.create(openTelemetry, INSTRUMENTATION_NAME, dataSourceName);
+
+    List<ObservableLongUpDownCounter> meters =
+        Arrays.asList(
+            metrics.usedConnections(dataSource::getNumActive),
+            metrics.idleConnections(dataSource::getNumIdle),
+            metrics.minIdleConnections(dataSource::getMinIdle),
+            metrics.maxIdleConnections(dataSource::getMaxIdle),
+            metrics.maxConnections(dataSource::getMaxTotal));
+
+    dataSourceMetrics.put(dataSource, meters);
+  }
+
+  public static void unregisterMetrics(BasicDataSourceMXBean dataSource) {
+    List<ObservableLongUpDownCounter> observableInstruments = dataSourceMetrics.remove(dataSource);
+    if (observableInstruments != null) {
+      for (ObservableLongUpDownCounter observable : observableInstruments) {
+        observable.close();
+      }
+    }
+  }
+
+  private DataSourceMetrics() {}
+}

+ 40 - 0
instrumentation/apache-dbcp-2.0/library/src/test/java/io/opentelemetry/instrumentation/apachedbcp/ApacheDbcpInstrumentationTest.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.apachedbcp;
+
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
+import org.apache.commons.dbcp2.BasicDataSource;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class ApacheDbcpInstrumentationTest extends AbstractApacheDbcpInstrumentationTest {
+
+  @RegisterExtension
+  static final InstrumentationExtension testing = LibraryInstrumentationExtension.create();
+
+  private static ApacheDbcpTelemetry telemetry;
+
+  @Override
+  protected InstrumentationExtension testing() {
+    return testing;
+  }
+
+  @BeforeAll
+  static void setup() {
+    telemetry = ApacheDbcpTelemetry.create(testing.getOpenTelemetry());
+  }
+
+  @Override
+  protected void configure(BasicDataSource dataSource, String dataSourceName) {
+    telemetry.registerMetrics(dataSource, dataSourceName);
+  }
+
+  @Override
+  protected void shutdown(BasicDataSource dataSource) {
+    telemetry.unregisterMetrics(dataSource);
+  }
+}

+ 11 - 0
instrumentation/apache-dbcp-2.0/testing/build.gradle.kts

@@ -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.apache.commons:commons-dbcp2:2.0")
+}

+ 89 - 0
instrumentation/apache-dbcp-2.0/testing/src/main/java/io/opentelemetry/instrumentation/apachedbcp/AbstractApacheDbcpInstrumentationTest.java

@@ -0,0 +1,89 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.apachedbcp;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
+import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
+import io.opentelemetry.instrumentation.testing.junit.db.DbConnectionPoolMetricsAssertions;
+import java.sql.Connection;
+import java.sql.Driver;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.commons.dbcp2.BasicDataSource;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public abstract class AbstractApacheDbcpInstrumentationTest {
+  private static final String INSTRUMENTATION_NAME = "io.opentelemetry.apache-dbcp-2.0";
+
+  @Mock Driver driverMock;
+  @Mock Connection connectionMock;
+
+  protected abstract InstrumentationExtension testing();
+
+  protected abstract void configure(BasicDataSource dataSource, String dataSourceName)
+      throws Exception;
+
+  protected abstract void shutdown(BasicDataSource dataSource) throws Exception;
+
+  @Test
+  void shouldReportMetrics() throws Exception {
+    // given
+    when(driverMock.connect(any(), any())).thenReturn(connectionMock);
+    when(connectionMock.isValid(anyInt())).thenReturn(true);
+
+    String dataSourceName = "dataSourceName";
+    BasicDataSource dataSource = new BasicDataSource();
+    dataSource.setDriver(driverMock);
+    dataSource.setUrl("db:///url");
+    dataSource.postDeregister();
+    configure(dataSource, dataSourceName);
+
+    // when
+    dataSource.getConnection().close();
+
+    // then
+    DbConnectionPoolMetricsAssertions.create(testing(), INSTRUMENTATION_NAME, dataSourceName)
+        .disableConnectionTimeouts()
+        .disableCreateTime()
+        .disableWaitTime()
+        .disableUseTime()
+        .disablePendingRequests()
+        .assertConnectionPoolEmitsMetrics();
+
+    // when
+    dataSource.close();
+    shutdown(dataSource);
+
+    // 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.idle.min",
+                "db.client.connections.idle.max",
+                "db.client.connections.max"));
+    assertThat(testing().metrics())
+        .filteredOn(
+            metricData ->
+                metricData.getInstrumentationScopeInfo().getName().equals(INSTRUMENTATION_NAME)
+                    && metricNames.contains(metricData.getName()))
+        .isEmpty();
+  }
+}

+ 3 - 0
settings.gradle.kts

@@ -137,6 +137,9 @@ include(":instrumentation:akka:akka-actor-fork-join-2.5:javaagent")
 include(":instrumentation:akka:akka-http-10.0:javaagent")
 include(":instrumentation:apache-camel-2.20:javaagent")
 include(":instrumentation:apache-camel-2.20:javaagent-unit-tests")
+include(":instrumentation:apache-dbcp-2.0:javaagent")
+include(":instrumentation:apache-dbcp-2.0:library")
+include(":instrumentation:apache-dbcp-2.0:testing")
 include(":instrumentation:apache-dubbo-2.7:javaagent")
 include(":instrumentation:apache-dubbo-2.7:library-autoconfigure")
 include(":instrumentation:apache-dubbo-2.7:testing")