diff --git a/apm-agent-benchmarks/pom.xml b/apm-agent-benchmarks/pom.xml index bbd74dfa9e..791e36337e 100644 --- a/apm-agent-benchmarks/pom.xml +++ b/apm-agent-benchmarks/pom.xml @@ -96,6 +96,11 @@ jackson-dataformat-protobuf ${version.jackson} + + net.bytebuddy + byte-buddy-agent + ${version.byte-buddy} + diff --git a/apm-agent-benchmarks/src/main/java/co/elastic/apm/benchmark/ElasticApmActiveContinuousBenchmark.java b/apm-agent-benchmarks/src/main/java/co/elastic/apm/benchmark/ElasticApmActiveContinuousBenchmark.java new file mode 100644 index 0000000000..8923aeee60 --- /dev/null +++ b/apm-agent-benchmarks/src/main/java/co/elastic/apm/benchmark/ElasticApmActiveContinuousBenchmark.java @@ -0,0 +1,43 @@ +/*- + * #%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.benchmark; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.RunnerException; + +import javax.servlet.ServletException; +import java.io.IOException; + +public class ElasticApmActiveContinuousBenchmark extends ElasticApmContinuousBenchmark { + + public ElasticApmActiveContinuousBenchmark() { + super(true); + } + + public static void main(String[] args) throws RunnerException { + run(ElasticApmActiveContinuousBenchmark.class); + } + + @Benchmark + public int benchmarkWithApm() throws IOException, ServletException { + httpServlet.service(request, response); + return response.getStatus(); + } +} diff --git a/apm-agent-benchmarks/src/main/java/co/elastic/apm/benchmark/ElasticApmContinuousBenchmark.java b/apm-agent-benchmarks/src/main/java/co/elastic/apm/benchmark/ElasticApmContinuousBenchmark.java index e72857e1f5..81db800d02 100644 --- a/apm-agent-benchmarks/src/main/java/co/elastic/apm/benchmark/ElasticApmContinuousBenchmark.java +++ b/apm-agent-benchmarks/src/main/java/co/elastic/apm/benchmark/ElasticApmContinuousBenchmark.java @@ -19,12 +19,13 @@ */ package co.elastic.apm.benchmark; +import co.elastic.apm.bci.ElasticApmAgent; import co.elastic.apm.configuration.CoreConfiguration; import co.elastic.apm.impl.ElasticApmTracer; import co.elastic.apm.report.Reporter; import co.elastic.apm.servlet.ApmFilter; import io.undertow.Undertow; -import org.openjdk.jmh.annotations.Benchmark; +import net.bytebuddy.agent.ByteBuddyAgent; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; @@ -32,20 +33,16 @@ import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.TearDown; -import org.openjdk.jmh.runner.RunnerException; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.stagemonitor.configuration.ConfigurationOptionProvider; import org.stagemonitor.configuration.ConfigurationRegistry; import org.stagemonitor.configuration.source.SimpleSource; -import javax.servlet.FilterChain; import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; import java.net.InetSocketAddress; import java.sql.Connection; import java.sql.DriverManager; @@ -97,20 +94,17 @@ @BenchmarkMode(Mode.SampleTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @State(Scope.Thread) -public class ElasticApmContinuousBenchmark extends AbstractBenchmark { +public abstract class ElasticApmContinuousBenchmark extends AbstractBenchmark { - private ApmFilter apmFilter; - private MockHttpServletRequest request; - private MockHttpServletResponse response; - private FilterChain filterChainWithApm; + private final boolean apmEnabled; + protected MockHttpServletRequest request; + protected MockHttpServletResponse response; + protected HttpServlet httpServlet; private Undertow server; private ElasticApmTracer tracer; - private Connection connectionWithApm; - private Connection connectionWithoutApm; - private FilterChain filterChainWithoutApm; - public static void main(String[] args) throws RunnerException { - run(ElasticApmContinuousBenchmark.class); + public ElasticApmContinuousBenchmark(boolean apmEnabled) { + this.apmEnabled = apmEnabled; } @Setup @@ -124,42 +118,28 @@ public void setUp() throws SQLException { .configurationRegistry(ConfigurationRegistry.builder() .addConfigSource(new SimpleSource() .add(CoreConfiguration.SERVICE_NAME, "benchmark") + .add(CoreConfiguration.INSTRUMENT, Boolean.toString(apmEnabled)) + .add(CoreConfiguration.ACTIVE, Boolean.toString(apmEnabled)) .add("server_url", "http://localhost:" + port)) .optionProviders(ServiceLoader.load(ConfigurationOptionProvider.class)) .build()) .build() .register(); - apmFilter = new ApmFilter(tracer); + ElasticApmAgent.initInstrumentation(tracer, ByteBuddyAgent.install()); request = createRequest(); response = createResponse(); - connectionWithApm = DriverManager.getConnection("jdbc:p6spy:h2:mem:test", "user", ""); - connectionWithApm.createStatement().execute("CREATE TABLE IF NOT EXISTS ELASTIC_APM (FOO INT, BAR VARCHAR(255))"); - connectionWithApm.createStatement().execute("INSERT INTO ELASTIC_APM (FOO, BAR) VALUES (1, 'APM')"); - filterChainWithApm = new BenchmarkingFilterChain(connectionWithApm, tracer); - - connectionWithoutApm = DriverManager.getConnection("jdbc:h2:mem:test", "user", ""); - connectionWithoutApm.createStatement().execute("CREATE TABLE IF NOT EXISTS ELASTIC_APM (FOO INT, BAR VARCHAR(255))"); - connectionWithoutApm.createStatement().execute("INSERT INTO ELASTIC_APM (FOO, BAR) VALUES (1, 'APM')"); - filterChainWithoutApm = new BenchmarkingFilterChain(connectionWithoutApm, tracer); + Connection 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')"); + httpServlet = new BenchmarkingServlet(connection, tracer); } @TearDown public void tearDown() { server.stop(); tracer.stop(); - } - - @Benchmark - public int benchmarkWithApm() throws IOException, ServletException { - apmFilter.doFilter(request, response, filterChainWithApm); - return response.getStatus(); - } - - @Benchmark - public int benchmarkWithoutApm() throws IOException, ServletException { - filterChainWithoutApm.doFilter(request, response); - return response.getStatus(); + ElasticApmAgent.reset(); } private MockHttpServletRequest createRequest() { @@ -231,18 +211,18 @@ private MockHttpServletResponse createResponse() { return new MockHttpServletResponse(); } - private static class BenchmarkingFilterChain implements FilterChain { + private static class BenchmarkingServlet extends HttpServlet { private final Connection connection; private final Reporter reporter; - private BenchmarkingFilterChain(Connection connection, ElasticApmTracer tracer) { + private BenchmarkingServlet(Connection connection, ElasticApmTracer tracer) { this.connection = connection; reporter = tracer.getReporter(); } @Override - public void doFilter(ServletRequest request, ServletResponse response) throws ServletException { + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException { try { final PreparedStatement preparedStatement = connection .prepareStatement("SELECT * FROM ELASTIC_APM WHERE foo=?"); @@ -254,7 +234,7 @@ public void doFilter(ServletRequest request, ServletResponse response) throws Se } // makes sure the jdbc query and the reporting can't be eliminated by JIT // setting it as the http status code so that there are no allocations necessary - ((HttpServletResponse) response).setStatus(count + reporter.getDropped()); + response.setStatus(count + reporter.getDropped()); } catch (Exception e) { throw new ServletException(e); } diff --git a/apm-agent-benchmarks/src/main/java/co/elastic/apm/benchmark/ElasticApmNotActiveContinuousBenchmark.java b/apm-agent-benchmarks/src/main/java/co/elastic/apm/benchmark/ElasticApmNotActiveContinuousBenchmark.java new file mode 100644 index 0000000000..a37a60f62d --- /dev/null +++ b/apm-agent-benchmarks/src/main/java/co/elastic/apm/benchmark/ElasticApmNotActiveContinuousBenchmark.java @@ -0,0 +1,42 @@ +/*- + * #%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.benchmark; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.runner.RunnerException; + +import javax.servlet.ServletException; +import java.io.IOException; + +public class ElasticApmNotActiveContinuousBenchmark extends ElasticApmContinuousBenchmark { + public ElasticApmNotActiveContinuousBenchmark() { + super(false); + } + + public static void main(String[] args) throws RunnerException { + run(ElasticApmNotActiveContinuousBenchmark.class); + } + + @Benchmark + public int benchmarkWithoutApm() throws IOException, ServletException { + httpServlet.service(request, response); + return response.getStatus(); + } +} diff --git a/apm-agent-core/pom.xml b/apm-agent-core/pom.xml index d320f98adb..27cb030bfa 100644 --- a/apm-agent-core/pom.xml +++ b/apm-agent-core/pom.xml @@ -88,7 +88,7 @@ net.bytebuddy byte-buddy - 1.8.11 + ${version.byte-buddy} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/bci/VisibleForAdvice.java b/apm-agent-core/src/main/java/co/elastic/apm/bci/VisibleForAdvice.java index 9d802905aa..14fd5d758f 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/bci/VisibleForAdvice.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/bci/VisibleForAdvice.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. @@ -19,6 +19,8 @@ */ package co.elastic.apm.bci; +import net.bytebuddy.asm.Advice; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -27,6 +29,11 @@ /** * A marker annotation which indicates that the annotated field or method has to be public because it is called by advice methods, * which are inlined into other classes. + *

+ * 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 metaDataMap = + Collections.synchronizedMap(new WeakHashMap()); private final ElasticApmTracer elasticApmTracer; public ApmJdbcEventListener() { @@ -45,7 +55,7 @@ public ApmJdbcEventListener(ElasticApmTracer elasticApmTracer) { } @Nullable - static String getMethod(String sql) { + static String getMethod(@Nullable String sql) { if (sql == null) { return null; } @@ -63,27 +73,64 @@ static String getMethod(String sql) { } } + /* + * This makes sure that even when there are wrappers for the statement, + * we only record each JDBC call once. + */ + private static boolean isAlreadyMonitored(@Nullable Span parent) { + // a db span can't be the child of another db span + // this means the span has already been created for this db call + return parent != null && parent.getType() != null && parent.getType().startsWith("db."); + } + @Override public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { } @Override public void onBeforeAnyExecute(StatementInformation statementInformation) { + createJdbcSpan(statementInformation.getStatementQuery(), statementInformation.getConnectionInformation().getConnection(), + elasticApmTracer.currentSpan()); + } + + @Nullable + Span createJdbcSpan(@Nullable String sql, Connection connection, @Nullable Span parentSpan) { + if (sql == null || isAlreadyMonitored(parentSpan)) { + return null; + } Span span = elasticApmTracer.startSpan(); if (span == null) { - return; + return null; } - span.setName(getMethod(statementInformation.getStatementQuery())); + span.setName(getMethod(sql)); + // temporarily setting the type here is important + // getting the meta data can result in another jdbc call + // if that is traced as well -> StackOverflowError + // to work around that, isAlreadyMonitored checks if the parent span is a db span and ignores them + span.setType("db.unknown.sql"); try { - String dbVendor = getDbVendor(statementInformation.getConnectionInformation().getConnection().getMetaData().getURL()); - span.setType("db." + dbVendor + ".sql"); + final ConnectionMetaData connectionMetaData = getConnectionMetaData(connection); + span.setType(connectionMetaData.type); span.getContext().getDb() - .withUser(statementInformation.getConnectionInformation().getConnection().getMetaData().getUserName()) - .withStatement(statementInformation.getStatementQuery()) + .withUser(connectionMetaData.user) + .withStatement(sql) .withType("sql"); } catch (SQLException e) { logger.warn("Ignored exception", e); } + return span; + } + + private ConnectionMetaData getConnectionMetaData(Connection connection) throws SQLException { + ConnectionMetaData connectionMetaData = metaDataMap.get(connection); + if (connectionMetaData == null) { + final DatabaseMetaData metaData = connection.getMetaData(); + String dbVendor = getDbVendor(metaData.getURL()); + connectionMetaData = new ConnectionMetaData("db." + dbVendor + ".sql", metaData.getUserName()); + metaDataMap.put(connection, connectionMetaData); + } + return connectionMetaData; + } String getDbVendor(String url) { @@ -119,4 +166,14 @@ public void onBeforeAnyAddBatch(StatementInformation statementInformation) { public void onAfterAnyAddBatch(StatementInformation statementInformation, long timeElapsedNanos, SQLException e) { super.onAfterAnyAddBatch(statementInformation, timeElapsedNanos, e); } + + private static class ConnectionMetaData { + final String type; + final String user; + + private ConnectionMetaData(String type, String user) { + this.type = type; + this.user = user; + } + } } diff --git a/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/ConnectionInstrumentation.java b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/ConnectionInstrumentation.java new file mode 100644 index 0000000000..6ccea467ef --- /dev/null +++ b/apm-agent-plugins/apm-jdbc-plugin/src/main/java/co/elastic/apm/jdbc/ConnectionInstrumentation.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 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.Connection; +import java.sql.PreparedStatement; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; + +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.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +/** + * Matches the various {@link Connection#prepareCall} and {@link Connection#prepareStatement} methods + * and keeps a reference to from the resulting {@link java.sql.CallableStatement} or {@link PreparedStatement} to the sql. + */ +public class ConnectionInstrumentation extends ElasticApmInstrumentation { + + private static final Map statementSqlMap = + Collections.synchronizedMap(new WeakHashMap()); + + @VisibleForAdvice + @Advice.OnMethodExit(inline = false) + public static void storeSql(@Advice.Return final PreparedStatement statement, @Advice.Argument(0) String sql) { + statementSqlMap.put(statement, sql); + } + + /** + * Returns the SQL statement belonging to provided {@link PreparedStatement}. + *

+ * 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 getTypeMatcher() { + return not(isInterface()) + // pre-select candidates for the more expensive isSubTypeOf matcher + .and(nameContains("Connection")) + .and(isSubTypeOf(Connection.class)); + } + + @Override + public ElementMatcher 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 getTypeMatcher() { + return not(isInterface()) + // pre-select candidates for the more expensive isSubTypeOf matcher + .and(nameContains("Statement")) + .and(isSubTypeOf(PreparedStatement.class)); + } + + @Override + public ElementMatcher 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 getTypeMatcher() { + return not(isInterface()) + // pre-select candidates for the more expensive isSubTypeOf matcher + .and(nameContains("Statement")) + .and(isSubTypeOf(Statement.class)); + } + + @Override + public ElementMatcher 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 @@ 3.1.0 3.0.0 2.19.1 + 1.8.11