+ * Also, when using non {@link Advice.OnMethodEnter#inline() inline}d advices, + * the signature of the advice method, + * as well as the advice class itself need to be public. + *
*/ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) diff --git a/apm-agent-core/src/test/java/co/elastic/apm/AbstractInstrumentationTest.java b/apm-agent-core/src/test/java/co/elastic/apm/AbstractInstrumentationTest.java new file mode 100644 index 0000000000..4ad2c1bcd7 --- /dev/null +++ b/apm-agent-core/src/test/java/co/elastic/apm/AbstractInstrumentationTest.java @@ -0,0 +1,53 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 the original author or authors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm; + +import co.elastic.apm.bci.ElasticApmAgent; +import co.elastic.apm.configuration.SpyConfiguration; +import co.elastic.apm.impl.ElasticApmTracer; +import net.bytebuddy.agent.ByteBuddyAgent; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; + +public abstract class AbstractInstrumentationTest { + protected static ElasticApmTracer tracer; + protected static MockReporter reporter; + + @BeforeAll + static void beforeAll() { + reporter = new MockReporter(); + tracer = ElasticApmTracer.builder() + .configurationRegistry(SpyConfiguration.createSpyConfig()) + .reporter(reporter) + .build(); + ElasticApmAgent.initInstrumentation(tracer, ByteBuddyAgent.install()); + } + + @AfterAll + static void afterAll() { + ElasticApmAgent.reset(); + } + + @BeforeEach + final void resetReporter() { + reporter.reset(); + } +} diff --git a/apm-agent-plugins/apm-jdbc-plugin/README.md b/apm-agent-plugins/apm-jdbc-plugin/README.md new file mode 100644 index 0000000000..0b145eb942 --- /dev/null +++ b/apm-agent-plugins/apm-jdbc-plugin/README.md @@ -0,0 +1,23 @@ +# JDBC plugin + +This plugin creates spans for JDBC queries. + +## Implementation Notes: + +The JDBC API itself is loaded by the bootstrap class loader. + +The implementations are however mostly loaded by + * The application server class loader (or a module class loader), if the server bundles the implementation + * The web app class loader, if the application bundles the JDBC driver + * The `SystemClassLoader`, when starting a simple `main` method Java program + +As a consequence, +there are not lots of class loader issues, +as the implementations are loaded by a child of the `SystemClassLoader` or the `SystemClassLoader` itself, +which also loads the agent code. +But as we don't need to refer to implementation specific classes, +but only the JDBC API, +which is loaded by the bootstrap class loader, +which is a parent class loader of both the agent and the JDBC implementation, +we can fully reference it even in helper classes. + diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/ApmJdbcEventListener.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/ApmJdbcEventListener.java index 90c002803b..0117f9a9e8 100644 --- a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/ApmJdbcEventListener.java +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/ApmJdbcEventListener.java @@ -28,12 +28,22 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nullable; +import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.SQLException; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +/** + * @deprecated use javaagent + */ +@Deprecated public class ApmJdbcEventListener extends SimpleJdbcEventListener { private static final Logger logger = LoggerFactory.getLogger(ApmJdbcEventListener.class); - + private static final Map+ * Might return {@code null} when the provided {@link PreparedStatement} is a wrapper of the actual statement. + *
+ * + * @return the SQL statement belonging to provided {@link PreparedStatement}, or {@code null} + */ + @Nullable + static String getSqlForStatement(PreparedStatement statement) { + final String sql = statementSqlMap.get(statement); + if (sql != null) { + statementSqlMap.remove(statement); + } + return sql; + } + + @Override + public ElementMatcher super TypeDescription> getTypeMatcher() { + return not(isInterface()) + // pre-select candidates for the more expensive isSubTypeOf matcher + .and(nameContains("Connection")) + .and(isSubTypeOf(Connection.class)); + } + + @Override + public ElementMatcher super MethodDescription> getMethodMatcher() { + return nameStartsWith("prepare") + .and(isPublic()) + .and(returns(isSubTypeOf(PreparedStatement.class))) + .and(takesArgument(0, String.class)); + } + +} diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/PreparedStatementInstrumentation.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/PreparedStatementInstrumentation.java new file mode 100644 index 0000000000..6b3c3661c5 --- /dev/null +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/PreparedStatementInstrumentation.java @@ -0,0 +1,93 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 the original author or authors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.jdbc; + +import co.elastic.apm.bci.ElasticApmInstrumentation; +import co.elastic.apm.bci.VisibleForAdvice; +import co.elastic.apm.impl.ElasticApmTracer; +import co.elastic.apm.impl.transaction.Span; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import javax.annotation.Nullable; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; +import static net.bytebuddy.matcher.ElementMatchers.nameContains; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +public class PreparedStatementInstrumentation extends ElasticApmInstrumentation { + + @Nullable + private static ElasticApmTracer tracer; + @Nullable + private static ApmJdbcEventListener jdbcEventListener; + + // not inlining as we can then set breakpoints into this method + // also, we don't have class loader issues when doing so + // another benefit of not inlining is that the advice methods are included in coverage reports + @Nullable + @VisibleForAdvice + @Advice.OnMethodEnter(inline = false) + public static Span onBeforeExecute(@Advice.This PreparedStatement statement) throws SQLException { + if (tracer != null && jdbcEventListener != null) { + final String sql = ConnectionInstrumentation.getSqlForStatement(statement); + return jdbcEventListener.createJdbcSpan(sql, statement.getConnection(), tracer.currentSpan()); + } + return null; + } + + + @VisibleForAdvice + @Advice.OnMethodExit(inline = false, onThrowable = SQLException.class) + public static void onAfterExecute(@Advice.Enter @Nullable Span span, @Advice.Thrown SQLException e) { + if (span != null) { + span.end(); + } + } + + @Override + public void init(ElasticApmTracer tracer) { + PreparedStatementInstrumentation.tracer = tracer; + PreparedStatementInstrumentation.jdbcEventListener = new ApmJdbcEventListener(tracer); + } + + @Override + public ElementMatcher super TypeDescription> getTypeMatcher() { + return not(isInterface()) + // pre-select candidates for the more expensive isSubTypeOf matcher + .and(nameContains("Statement")) + .and(isSubTypeOf(PreparedStatement.class)); + } + + @Override + public ElementMatcher super MethodDescription> getMethodMatcher() { + return nameStartsWith("execute") + .and(isPublic()) + .and(takesArguments(0)); + } +} diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/StatementInstrumentation.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/StatementInstrumentation.java new file mode 100644 index 0000000000..2733e0c229 --- /dev/null +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/StatementInstrumentation.java @@ -0,0 +1,95 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 the original author or authors + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package co.elastic.apm.jdbc; + +import co.elastic.apm.bci.ElasticApmInstrumentation; +import co.elastic.apm.bci.VisibleForAdvice; +import co.elastic.apm.impl.ElasticApmTracer; +import co.elastic.apm.impl.transaction.Span; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import javax.annotation.Nullable; +import java.sql.SQLException; +import java.sql.Statement; + +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.isSubTypeOf; +import static net.bytebuddy.matcher.ElementMatchers.nameContains; +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +/** + * Creates spans for JDBC calls + */ +public class StatementInstrumentation extends ElasticApmInstrumentation { + + @Nullable + private static ElasticApmTracer tracer; + @Nullable + private static ApmJdbcEventListener jdbcEventListener; + + // not inlining as we can then set breakpoints into this method + // also, we don't have class loader issues when doing so + // another benefit of not inlining is that the advice methods are included in coverage reports + @Nullable + @VisibleForAdvice + @Advice.OnMethodEnter(inline = false) + public static Span onBeforeExecute(@Advice.This Statement statement, @Advice.Argument(0) String sql) throws SQLException { + if (tracer != null && jdbcEventListener != null) { + return jdbcEventListener.createJdbcSpan(sql, statement.getConnection(), tracer.currentSpan()); + } + return null; + } + + + @VisibleForAdvice + @Advice.OnMethodExit(inline = false, onThrowable = SQLException.class) + public static void onAfterExecute(@Advice.Enter @Nullable Span span, @Advice.Thrown SQLException e) { + if (span != null) { + span.end(); + } + } + + @Override + public void init(ElasticApmTracer tracer) { + StatementInstrumentation.tracer = tracer; + StatementInstrumentation.jdbcEventListener = new ApmJdbcEventListener(tracer); + } + + @Override + public ElementMatcher super TypeDescription> getTypeMatcher() { + return not(isInterface()) + // pre-select candidates for the more expensive isSubTypeOf matcher + .and(nameContains("Statement")) + .and(isSubTypeOf(Statement.class)); + } + + @Override + public ElementMatcher super MethodDescription> getMethodMatcher() { + return nameStartsWith("execute") + .and(isPublic()) + .and(takesArgument(0, String.class)); + } +} diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/package-info.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/package-info.java index f0134188a3..21fada2651 100644 --- a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/package-info.java +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/package-info.java @@ -7,9 +7,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -17,4 +17,7 @@ * limitations under the License. * #L% */ +@NonnullApi package co.elastic.apm.jdbc; + +import co.elastic.apm.annotation.NonnullApi; diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/resources/META-INF/services/co.elastic.apm.bci.ElasticApmInstrumentation b/apm-agent-plugins/apm-jdbc-plugin/src/main/resources/META-INF/services/co.elastic.apm.bci.ElasticApmInstrumentation new file mode 100644 index 0000000000..fd31c2eb32 --- /dev/null +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/resources/META-INF/services/co.elastic.apm.bci.ElasticApmInstrumentation @@ -0,0 +1,3 @@ +co.elastic.apm.jdbc.ConnectionInstrumentation +co.elastic.apm.jdbc.StatementInstrumentation +co.elastic.apm.jdbc.PreparedStatementInstrumentation diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/jdbc/ApmJdbcEventListenerTest.java b/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/jdbc/ApmJdbcEventListenerTest.java index d5b2ae902a..6043134ba3 100644 --- a/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/jdbc/ApmJdbcEventListenerTest.java +++ b/apm-agent-plugins/apm-jdbc-plugin/src/test/java/co/elastic/apm/jdbc/ApmJdbcEventListenerTest.java @@ -19,17 +19,15 @@ */ package co.elastic.apm.jdbc; -import co.elastic.apm.MockReporter; -import co.elastic.apm.configuration.SpyConfiguration; -import co.elastic.apm.impl.ElasticApmTracer; +import co.elastic.apm.AbstractInstrumentationTest; import co.elastic.apm.impl.transaction.Db; import co.elastic.apm.impl.transaction.Span; import co.elastic.apm.impl.transaction.Transaction; -import com.p6spy.engine.spy.P6SpyDriver; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; @@ -38,19 +36,14 @@ import static org.assertj.core.api.Assertions.assertThat; -class ApmJdbcEventListenerTest { +class ApmJdbcEventListenerTest extends AbstractInstrumentationTest { private Connection connection; private Transaction transaction; @BeforeEach void setUp() throws SQLException { - ElasticApmTracer tracer = ElasticApmTracer.builder() - .configurationRegistry(SpyConfiguration.createSpyConfig()) - .reporter(new MockReporter()) - .build() - .register(); - connection = DriverManager.getConnection("jdbc:p6spy:h2:mem:test", "user", ""); + connection = DriverManager.getConnection("jdbc:h2:mem:test", "user", ""); connection.createStatement().execute("CREATE TABLE IF NOT EXISTS ELASTIC_APM (FOO INT, BAR VARCHAR(255))"); connection.createStatement().execute("INSERT INTO ELASTIC_APM (FOO, BAR) VALUES (1, 'APM')"); transaction = tracer.startTransaction(); @@ -63,14 +56,34 @@ void setUp() throws SQLException { void tearDown() throws SQLException { connection.close(); transaction.end(); - ElasticApmTracer.unregister(); } @Test - void testJdbcSpan() throws SQLException { - PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM ELASTIC_APM WHERE FOO=$1"); + void testStatement() throws SQLException { + final String sql = "SELECT * FROM ELASTIC_APM WHERE FOO=1"; + ResultSet resultSet = connection.createStatement().executeQuery(sql); + assertSpanRecorded(resultSet, sql); + } + + @Test + void testPreparedStatement() throws SQLException { + final String sql = "SELECT * FROM ELASTIC_APM WHERE FOO=$1"; + PreparedStatement preparedStatement = connection.prepareStatement(sql); + preparedStatement.setInt(1, 1); + ResultSet resultSet = preparedStatement.executeQuery(); + assertSpanRecorded(resultSet, sql); + } + + @Test + void testCallableStatement() throws SQLException { + final String sql = "SELECT * FROM ELASTIC_APM WHERE FOO=$1"; + CallableStatement preparedStatement = connection.prepareCall(sql); preparedStatement.setInt(1, 1); ResultSet resultSet = preparedStatement.executeQuery(); + assertSpanRecorded(resultSet, sql); + } + + private void assertSpanRecorded(ResultSet resultSet, String sql) throws SQLException { assertThat(resultSet.next()).isTrue(); assertThat(resultSet.getInt("foo")).isEqualTo(1); assertThat(resultSet.getString("BAR")).isEqualTo("APM"); @@ -79,7 +92,7 @@ void testJdbcSpan() throws SQLException { assertThat(jdbcSpan.getName()).isEqualTo("SELECT"); assertThat(jdbcSpan.getType()).isEqualToIgnoringCase("db.h2.sql"); Db db = jdbcSpan.getContext().getDb(); - assertThat(db.getStatement()).isEqualTo("SELECT * FROM ELASTIC_APM WHERE FOO=$1"); + assertThat(db.getStatement()).isEqualTo(sql); assertThat(db.getUser()).isEqualToIgnoringCase("user"); assertThat(db.getType()).isEqualToIgnoringCase("sql"); } diff --git a/pom.xml b/pom.xml index b3b0a0e58c..faa6eb8223 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,7 @@