From 51df3c4827b8c5f15d0fc1cd9deec9c9404929f7 Mon Sep 17 00:00:00 2001 From: Felix Barnsteiner Date: Fri, 11 Jan 2019 17:58:12 +0100 Subject: [PATCH] WIP: ExecutorService instrumentation closes #145 --- .../apm/agent/bci/ElasticApmAgent.java | 3 +- .../impl/ContextInScopeRunnableWrapper.java | 90 +++++++++++++++++++ .../apm/agent/impl/ElasticApmTracer.java | 47 ++++++++-- ...r.java => SpanInScopeRunnableWrapper.java} | 45 +++++----- .../agent/impl/transaction/AbstractSpan.java | 12 +++ .../impl/transaction/TraceContextHolder.java | 58 ++++++------ .../apm-java-concurrent-plugin/pom.xml | 14 +++ .../concurrent/ExecutorInstrumentation.java | 66 ++++++++++++++ .../apm/agent/concurrent/package-info.java | 23 +++++ ...ic.apm.agent.bci.ElasticApmInstrumentation | 1 + .../agent/servlet/AsyncInstrumentation.java | 6 +- apm-agent-plugins/pom.xml | 1 + elastic-apm-agent/pom.xml | 5 ++ integration-tests/concurrent-testbed/pom.xml | 22 +++++ .../apm/concurrent/ExecutorServiceIT.java | 73 +++++++++++++++ integration-tests/pom.xml | 1 + 16 files changed, 403 insertions(+), 64 deletions(-) create mode 100644 apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ContextInScopeRunnableWrapper.java rename apm-agent-core/src/main/java/co/elastic/apm/agent/impl/{InScopeRunnableWrapper.java => SpanInScopeRunnableWrapper.java} (54%) create mode 100644 apm-agent-plugins/apm-java-concurrent-plugin/pom.xml create mode 100644 apm-agent-plugins/apm-java-concurrent-plugin/src/main/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentation.java create mode 100644 apm-agent-plugins/apm-java-concurrent-plugin/src/main/java/co/elastic/apm/agent/concurrent/package-info.java create mode 100644 apm-agent-plugins/apm-java-concurrent-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation create mode 100644 integration-tests/concurrent-testbed/pom.xml create mode 100644 integration-tests/concurrent-testbed/src/test/java/co/elastic/apm/concurrent/ExecutorServiceIT.java diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java index e6538b2fd6e..42a4d69f32f 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmAgent.java @@ -64,6 +64,7 @@ import static net.bytebuddy.matcher.ElementMatchers.any; import static net.bytebuddy.matcher.ElementMatchers.nameContains; import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; +import static net.bytebuddy.matcher.ElementMatchers.not; public class ElasticApmAgent { @@ -298,7 +299,7 @@ private static AgentBuilder getAgentBuilder(final ByteBuddy byteBuddy, final Cor : AgentBuilder.PoolStrategy.Default.FAST) .ignore(any(), isReflectionClassLoader()) .or(any(), classLoaderWithName("org.codehaus.groovy.runtime.callsite.CallSiteClassLoader")) - .or(nameStartsWith("java.")) + .or(nameStartsWith("java.").and(not(nameStartsWith("java.util.concurrent.")))) .or(nameStartsWith("com.sun.")) .or(nameStartsWith("sun")) .or(nameStartsWith("org.aspectj.")) diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ContextInScopeRunnableWrapper.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ContextInScopeRunnableWrapper.java new file mode 100644 index 00000000000..acb8749ddaa --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ContextInScopeRunnableWrapper.java @@ -0,0 +1,90 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018-2019 Elastic and contributors + * %% + * 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.agent.impl; + + +import co.elastic.apm.agent.bci.VisibleForAdvice; +import co.elastic.apm.agent.impl.transaction.TraceContext; +import co.elastic.apm.agent.objectpool.Recyclable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +@VisibleForAdvice +public class ContextInScopeRunnableWrapper implements Runnable, Recyclable { + private static final Logger logger = LoggerFactory.getLogger(ContextInScopeRunnableWrapper.class); + private final ElasticApmTracer tracer; + private final TraceContext context; + @Nullable + private volatile Runnable delegate; + + ContextInScopeRunnableWrapper(ElasticApmTracer tracer) { + this.tracer = tracer; + context = TraceContext.with64BitId(tracer); + } + + ContextInScopeRunnableWrapper wrap(Runnable delegate, TraceContext context) { + this.context.copyFrom(context); + // ordering is important: volatile write has to be after copying the TraceContext to ensure visibility in #run + this.delegate = delegate; + return this; + } + + // Exceptions in the agent may never affect the monitored application + // normally, advices act as the boundary of user and agent code and exceptions are handled via @Advice.OnMethodEnter(suppress = Throwable.class) + // In this case, this class acts as the boundary of user and agent code so we have to do the tedious exception handling here + @Override + public void run() { + try { + context.activate(); + } catch (Throwable t) { + try { + logger.error("Unexpected error while activating span", t); + } catch (Throwable ignore) { + } + } + try { + //noinspection ConstantConditions + delegate.run(); + } catch (Exception e) { + // although the corresponding span may be ended at this point, + // we still have a copy of it's TraceContext so we can track errors here + // (in contrast to SpanInScopeRunnableWrapper) + context.captureException(e); + } finally { + try { + context.deactivate(); + tracer.recycle(this); + } catch (Throwable t) { + try { + logger.error("Unexpected error while activating span", t); + } catch (Throwable ignore) { + } + } + } + } + + @Override + public void resetState() { + context.resetState(); + delegate = null; + } +} diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java index f15ed357877..2645c3ac121 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java @@ -59,13 +59,21 @@ public class ElasticApmTracer { private static final Logger logger = LoggerFactory.getLogger(ElasticApmTracer.class); + /** + * The number of required {@link Runnable} wrappers does not depend on the size of the disruptor + * but rather on the amount of application threads. + * The requirement increases if the application tends to wrap multiple {@link Runnable}s. + */ + private static final int MAX_POOLED_RUNNABLES = 256; + private final ConfigurationRegistry configurationRegistry; private final StacktraceConfiguration stacktraceConfiguration; private final Iterable lifecycleListeners; private final ObjectPool transactionPool; private final ObjectPool spanPool; private final ObjectPool errorPool; - private final ObjectPool runnableWrapperObjectPool; + private final ObjectPool runnableSpanWrapperObjectPool; + private final ObjectPool runnableContextWrapperObjectPool; private final Reporter reporter; // Maintains a stack of all the activated spans // This way its easy to retrieve the bottom of the stack (the transaction) @@ -111,11 +119,20 @@ public ErrorCapture createInstance() { return new ErrorCapture(ElasticApmTracer.this); } }); - runnableWrapperObjectPool = QueueBasedObjectPool.ofRecyclable(AtomicQueueFactory.newQueue(createBoundedMpmc(maxPooledElements)), false, - new Allocator() { + // consider specialized object pools which return the objects to the thread-local pool of their originating thread + // with a combination of DetachedThreadLocal and org.jctools.queues.MpscRelaxedArrayQueue + runnableSpanWrapperObjectPool = QueueBasedObjectPool.ofRecyclable(AtomicQueueFactory.newQueue(createBoundedMpmc(MAX_POOLED_RUNNABLES)), false, + new Allocator() { + @Override + public SpanInScopeRunnableWrapper createInstance() { + return new SpanInScopeRunnableWrapper(ElasticApmTracer.this); + } + }); + runnableContextWrapperObjectPool = QueueBasedObjectPool.ofRecyclable(AtomicQueueFactory.newQueue(createBoundedMpmc(MAX_POOLED_RUNNABLES)), false, + new Allocator() { @Override - public InScopeRunnableWrapper createInstance() { - return new InScopeRunnableWrapper(ElasticApmTracer.this); + public ContextInScopeRunnableWrapper createInstance() { + return new ContextInScopeRunnableWrapper(ElasticApmTracer.this); } }); sampler = ProbabilitySampler.of(coreConfiguration.getSampleRate().get()); @@ -301,11 +318,25 @@ public void recycle(ErrorCapture error) { } public Runnable wrapRunnable(Runnable delegate, AbstractSpan span) { - return runnableWrapperObjectPool.createInstance().wrap(delegate, span); + if (delegate instanceof SpanInScopeRunnableWrapper) { + return delegate; + } + return runnableSpanWrapperObjectPool.createInstance().wrap(delegate, span); + } + + public void recycle(SpanInScopeRunnableWrapper wrapper) { + runnableSpanWrapperObjectPool.recycle(wrapper); + } + + public Runnable wrapRunnable(Runnable delegate, TraceContext traceContext) { + if (delegate instanceof SpanInScopeRunnableWrapper) { + return delegate; + } + return runnableContextWrapperObjectPool.createInstance().wrap(delegate, traceContext); } - public void recycle(InScopeRunnableWrapper wrapper) { - runnableWrapperObjectPool.recycle(wrapper); + public void recycle(ContextInScopeRunnableWrapper wrapper) { + runnableContextWrapperObjectPool.recycle(wrapper); } /** diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/InScopeRunnableWrapper.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/SpanInScopeRunnableWrapper.java similarity index 54% rename from apm-agent-core/src/main/java/co/elastic/apm/agent/impl/InScopeRunnableWrapper.java rename to apm-agent-core/src/main/java/co/elastic/apm/agent/impl/SpanInScopeRunnableWrapper.java index 740be403279..9750214e430 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/InScopeRunnableWrapper.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/SpanInScopeRunnableWrapper.java @@ -29,54 +29,55 @@ import javax.annotation.Nullable; @VisibleForAdvice -public class InScopeRunnableWrapper implements Runnable, Recyclable { - private final Logger logger = LoggerFactory.getLogger(InScopeRunnableWrapper.class); - +public class SpanInScopeRunnableWrapper implements Runnable, Recyclable { + private static final Logger logger = LoggerFactory.getLogger(SpanInScopeRunnableWrapper.class); + private final ElasticApmTracer tracer; @Nullable - private Runnable delegate; + private volatile Runnable delegate; @Nullable - private AbstractSpan span; + private volatile AbstractSpan span; - private final ElasticApmTracer tracer; - - InScopeRunnableWrapper(ElasticApmTracer tracer) { + SpanInScopeRunnableWrapper(ElasticApmTracer tracer) { this.tracer = tracer; } - public InScopeRunnableWrapper wrap(Runnable delegate, AbstractSpan span) { + SpanInScopeRunnableWrapper wrap(Runnable delegate, AbstractSpan span) { this.delegate = delegate; this.span = span; return this; } + // Exceptions in the agent may never affect the monitored application + // normally, advices act as the boundary of user and agent code and exceptions are handled via @Advice.OnMethodEnter(suppress = Throwable.class) + // In this case, this class acts as the boundary of user and agent code so we have to do the tedious exception handling here @Override public void run() { - if (span != null) { + // minimize volatile reads + AbstractSpan localSpan = span; + if (localSpan != null) { try { - span.activate(); - } catch (Throwable throwable) { + localSpan.activate(); + } catch (Throwable t) { try { - logger.warn("Failed to activate span"); - } catch (Throwable t) { - // do nothing, just never fail + logger.error("Unexpected error while activating span", t); + } catch (Throwable ignore) { } } } - try { //noinspection ConstantConditions delegate.run(); + // the span may be ended at this point } finally { try { - if (span != null) { - span.deactivate(); + if (localSpan != null) { + localSpan.deactivate(); } tracer.recycle(this); - } catch (Throwable throwable) { + } catch (Throwable t) { try { - logger.warn("Failed to deactivate span or recycle"); - } catch (Throwable t) { - // do nothing, just never fail + logger.error("Unexpected error while activating span", t); + } catch (Throwable ignore) { } } } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java index 1452bed6b25..a9bea0d824e 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java @@ -201,4 +201,16 @@ public boolean isChildOf(TraceContextHolder other) { return getTraceContext().isChildOf(other); } + /** + * Wraps the provided runnable and makes this {@link AbstractSpan} active in the {@link Runnable#run()} method. + * + *

+ * Note: does activates the {@link AbstractSpan} and not only the {@link TraceContext}. + * This should only be used span is closed in thread the provided {@link Runnable} is executed in. + *

+ */ + public Runnable withActiveSpan(Runnable runnable) { + return tracer.wrapRunnable(runnable, this); + } + } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContextHolder.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContextHolder.java index 8a18590ea43..d0841652b53 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContextHolder.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContextHolder.java @@ -61,44 +61,30 @@ protected TraceContextHolder(ElasticApmTracer tracer) { public abstract boolean isChildOf(TraceContextHolder other); public T activate() { - try { - tracer.activate(this); - List spanListeners = tracer.getSpanListeners(); - for (int i = 0; i < spanListeners.size(); i++) { - try { - spanListeners.get(i).onActivate(this); - } catch (Error e) { - throw e; - } catch (Throwable t) { - logger.warn("Exception while calling {}#onActivate", spanListeners.get(i).getClass().getSimpleName(), t); - } - } - } catch (Throwable t) { + tracer.activate(this); + List spanListeners = tracer.getSpanListeners(); + for (int i = 0; i < spanListeners.size(); i++) { try { - logger.error("Unexpected error while activating context", t); - } catch (Throwable ignore) { + spanListeners.get(i).onActivate(this); + } catch (Error e) { + throw e; + } catch (Throwable t) { + logger.warn("Exception while calling {}#onActivate", spanListeners.get(i).getClass().getSimpleName(), t); } } return (T) this; } public T deactivate() { - try { - tracer.deactivate(this); - List spanListeners = tracer.getSpanListeners(); - for (int i = 0; i < spanListeners.size(); i++) { - try { - spanListeners.get(i).onDeactivate(this); - } catch (Error e) { - throw e; - } catch (Throwable t) { - logger.warn("Exception while calling {}#onDeactivate", spanListeners.get(i).getClass().getSimpleName(), t); - } - } - } catch (Throwable t) { + tracer.deactivate(this); + List spanListeners = tracer.getSpanListeners(); + for (int i = 0; i < spanListeners.size(); i++) { try { - logger.error("Unexpected error while activating context", t); - } catch (Throwable ignore) { + spanListeners.get(i).onDeactivate(this); + } catch (Error e) { + throw e; + } catch (Throwable t) { + logger.warn("Exception while calling {}#onDeactivate", spanListeners.get(i).getClass().getSimpleName(), t); } } return (T) this; @@ -131,4 +117,16 @@ public T captureException(@Nullable Throwable t) { return (T) this; } + /** + * Wraps the provided runnable and makes this {@link TraceContext} active in the {@link Runnable#run()} method. + * + *

+ * Note: does not activate the {@link AbstractSpan} but only the {@link TraceContext}. + * This is useful if this span is closed in a different thread than the provided {@link Runnable} is executed in. + *

+ */ + public Runnable withActiveContext(Runnable runnable) { + return tracer.wrapRunnable(runnable, getTraceContext()); + } + } diff --git a/apm-agent-plugins/apm-java-concurrent-plugin/pom.xml b/apm-agent-plugins/apm-java-concurrent-plugin/pom.xml new file mode 100644 index 00000000000..df566af3cea --- /dev/null +++ b/apm-agent-plugins/apm-java-concurrent-plugin/pom.xml @@ -0,0 +1,14 @@ + + + + apm-agent-plugins + co.elastic.apm + 1.3.1-SNAPSHOT + + 4.0.0 + + apm-java-concurrent-plugin + + diff --git a/apm-agent-plugins/apm-java-concurrent-plugin/src/main/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentation.java b/apm-agent-plugins/apm-java-concurrent-plugin/src/main/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentation.java new file mode 100644 index 00000000000..868d1ffe947 --- /dev/null +++ b/apm-agent-plugins/apm-java-concurrent-plugin/src/main/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentation.java @@ -0,0 +1,66 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2019 Elastic and contributors + * %% + * 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.agent.concurrent; + +import co.elastic.apm.agent.bci.ElasticApmInstrumentation; +import co.elastic.apm.agent.impl.transaction.TraceContextHolder; +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.util.Collection; +import java.util.Collections; +import java.util.concurrent.Future; + +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +public class ExecutorInstrumentation extends ElasticApmInstrumentation { + + @Advice.OnMethodEnter + public static void onExecute(@Advice.Argument(value = 0, readOnly = false) @Nullable Runnable runnable) { + if (tracer != null && runnable != null) { + final TraceContextHolder active = tracer.getActive(); + if (active != null) { + runnable = active.withActiveContext(runnable); + } + } + } + + @Override + public ElementMatcher getTypeMatcher() { + return named("java.util.concurrent.ThreadPoolExecutor"); + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("execute").and(returns(void.class)).and(takesArguments(Runnable.class)).and(isPublic()) + .or(named("submit").and(returns(Future.class)).and(takesArguments(Runnable.class)).and(isPublic())); + } + + @Override + public Collection getInstrumentationGroupNames() { + return Collections.singletonList("executor"); + } +} diff --git a/apm-agent-plugins/apm-java-concurrent-plugin/src/main/java/co/elastic/apm/agent/concurrent/package-info.java b/apm-agent-plugins/apm-java-concurrent-plugin/src/main/java/co/elastic/apm/agent/concurrent/package-info.java new file mode 100644 index 00000000000..4244aae127b --- /dev/null +++ b/apm-agent-plugins/apm-java-concurrent-plugin/src/main/java/co/elastic/apm/agent/concurrent/package-info.java @@ -0,0 +1,23 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2019 Elastic and contributors + * %% + * 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% + */ +@NonnullApi +package co.elastic.apm.agent.concurrent; + +import co.elastic.apm.agent.annotation.NonnullApi; diff --git a/apm-agent-plugins/apm-java-concurrent-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation b/apm-agent-plugins/apm-java-concurrent-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation new file mode 100644 index 00000000000..bacae55304b --- /dev/null +++ b/apm-agent-plugins/apm-java-concurrent-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.ElasticApmInstrumentation @@ -0,0 +1 @@ +co.elastic.apm.agent.concurrent.ExecutorInstrumentation diff --git a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AsyncInstrumentation.java b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AsyncInstrumentation.java index 02692a01847..909d86ce3b1 100644 --- a/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AsyncInstrumentation.java +++ b/apm-agent-plugins/apm-servlet-plugin/src/main/java/co/elastic/apm/agent/servlet/AsyncInstrumentation.java @@ -165,10 +165,10 @@ public static class AsyncContextStartAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) private static void onEnterAsyncContextStart(@Advice.Argument(value = 0, readOnly = false) @Nullable Runnable runnable) { - if (tracer != null) { + if (tracer != null && runnable != null) { final Transaction transaction = tracer.currentTransaction(); - if (transaction != null && runnable != null && asyncHelper != null) { - runnable = tracer.wrapRunnable(runnable, transaction); + if (transaction != null) { + runnable = transaction.withActiveSpan(runnable); } } } diff --git a/apm-agent-plugins/pom.xml b/apm-agent-plugins/pom.xml index f916b5a4a1d..ffee029b9bf 100644 --- a/apm-agent-plugins/pom.xml +++ b/apm-agent-plugins/pom.xml @@ -20,6 +20,7 @@ apm-httpclient-core apm-slf4j-plugin apm-es-restclient-plugin + apm-java-concurrent-plugin diff --git a/elastic-apm-agent/pom.xml b/elastic-apm-agent/pom.xml index 9b70622856a..44f62736c0f 100644 --- a/elastic-apm-agent/pom.xml +++ b/elastic-apm-agent/pom.xml @@ -161,6 +161,11 @@ apm-httpclient-core ${project.version} + + ${project.groupId} + apm-java-concurrent-plugin + ${project.version} + ${project.groupId} apm-jaxrs-plugin diff --git a/integration-tests/concurrent-testbed/pom.xml b/integration-tests/concurrent-testbed/pom.xml new file mode 100644 index 00000000000..733b20fa4e2 --- /dev/null +++ b/integration-tests/concurrent-testbed/pom.xml @@ -0,0 +1,22 @@ + + + + integration-tests + co.elastic.apm + 1.3.1-SNAPSHOT + + 4.0.0 + + concurrent-testbed + + + + ${project.groupId} + apm-agent-api + ${project.version} + test + + + diff --git a/integration-tests/concurrent-testbed/src/test/java/co/elastic/apm/concurrent/ExecutorServiceIT.java b/integration-tests/concurrent-testbed/src/test/java/co/elastic/apm/concurrent/ExecutorServiceIT.java new file mode 100644 index 00000000000..9122045244c --- /dev/null +++ b/integration-tests/concurrent-testbed/src/test/java/co/elastic/apm/concurrent/ExecutorServiceIT.java @@ -0,0 +1,73 @@ +/*- + * #%L + * Elastic APM Java agent + * %% + * Copyright (C) 2018 - 2019 Elastic and contributors + * %% + * 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.concurrent; + +import co.elastic.apm.api.ElasticApm; +import co.elastic.apm.api.Scope; +import co.elastic.apm.api.Transaction; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +class ExecutorServiceIT { + + private ExecutorService executor; + private Transaction transaction; + private Scope scope; + + + @BeforeEach + void setUp() { + executor = Executors.newSingleThreadExecutor(); + transaction = ElasticApm.startTransaction(); + scope = transaction.activate(); + assertThat(ElasticApm.currentSpan().isSampled()).isTrue(); + assertThat(ElasticApm.currentSpan().getId()).isEqualTo(transaction.getId()); + } + + @AfterEach + void tearDown() { + scope.close(); + assertThat(ElasticApm.currentSpan().isSampled()).isFalse(); + } + + @Test + void testExecutorExecuteAnonymousInnerClass() throws Exception { + executor.submit(new Runnable() { + @Override + public void run() { + assertThat(ElasticApm.currentSpan().getId()).isEqualTo(transaction.getId()); + } + }).get(); + } + + @Test + void testExecutorExecuteLambda() throws Exception { + executor.submit(() -> { + assertThat(ElasticApm.currentSpan().getId()).isEqualTo(transaction.getId()); + }).get(1, TimeUnit.SECONDS); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 35a1c9e4027..938266fa835 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -15,6 +15,7 @@ simple-webapp-integration-test spring-boot-1-5 spring-boot-2 + concurrent-testbed true