diff --git a/CHANGELOG.md b/CHANGELOG.md index 16ddd60160..feebab0c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Features * Added support for sync calls of OkHttp client + * Added support for context propagation for `java.util.concurrent.ExecutorService`s * The `trace_methods` configuration now allows to omit the method matcher. Example: `com.example.*` traces all classes and methods within the `com.example` package and sub-packages. * Added support for JSF. Tested on WildFly, WebSphere Liberty and Payara with embedded JSF implementation and on Tomcat and Jetty with 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 e66e9d2742..19ebac7a1f 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 @@ -299,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.").and(not(nameStartsWith("com.sun.faces.")))) .or(nameStartsWith("sun")) .or(nameStartsWith("org.aspectj.")) diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmInstrumentation.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmInstrumentation.java index 388635de64..9c10485fde 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmInstrumentation.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/bci/ElasticApmInstrumentation.java @@ -20,6 +20,7 @@ package co.elastic.apm.agent.bci; import co.elastic.apm.agent.impl.ElasticApmTracer; +import co.elastic.apm.agent.impl.transaction.TraceContextHolder; import net.bytebuddy.description.NamedElement; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; @@ -59,6 +60,14 @@ static void staticInit(ElasticApmTracer tracer) { ElasticApmInstrumentation.tracer = tracer; } + @Nullable + public static TraceContextHolder getActive() { + if (tracer != null) { + return tracer.getActive(); + } + return null; + } + public void init(ElasticApmTracer tracer) { } diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ContextInScopeCallableWrapper.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ContextInScopeCallableWrapper.java new file mode 100644 index 0000000000..1161df50a3 --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ContextInScopeCallableWrapper.java @@ -0,0 +1,86 @@ +/*- + * #%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; +import java.util.concurrent.Callable; + +@VisibleForAdvice +public class ContextInScopeCallableWrapper implements Callable, Recyclable { + private static final Logger logger = LoggerFactory.getLogger(ContextInScopeCallableWrapper.class); + private final ElasticApmTracer tracer; + private final TraceContext context; + @Nullable + private volatile Callable delegate; + + ContextInScopeCallableWrapper(ElasticApmTracer tracer) { + this.tracer = tracer; + context = TraceContext.with64BitId(tracer); + } + + ContextInScopeCallableWrapper wrap(Callable 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 V call() throws Exception { + try { + context.activate(); + } catch (Throwable t) { + try { + logger.error("Unexpected error while activating span", t); + } catch (Throwable ignore) { + } + } + try { + //noinspection ConstantConditions + return delegate.call(); + } finally { + try { + context.deactivate(); + tracer.recycle(this); + } catch (Throwable t) { + try { + logger.error("Unexpected error while deactivating or recycling 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/ContextInScopeRunnableWrapper.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ContextInScopeRunnableWrapper.java new file mode 100644 index 0000000000..2eef58617b --- /dev/null +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ContextInScopeRunnableWrapper.java @@ -0,0 +1,85 @@ +/*- + * #%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(); + } finally { + try { + context.deactivate(); + tracer.recycle(this); + } catch (Throwable t) { + try { + logger.error("Unexpected error while deactivating or recycling 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 e06a46b4fa..2e9a340f36 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 @@ -47,6 +47,7 @@ import java.util.ArrayDeque; import java.util.Deque; import java.util.List; +import java.util.concurrent.Callable; import static org.jctools.queues.spec.ConcurrentQueueSpec.createBoundedMpmc; @@ -59,13 +60,22 @@ 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 ObjectPool> callableContextWrapperObjectPool; 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) @@ -112,11 +122,27 @@ 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); + } + }); + callableContextWrapperObjectPool = QueueBasedObjectPool.ofRecyclable(AtomicQueueFactory.>newQueue(createBoundedMpmc(MAX_POOLED_RUNNABLES)), false, + new Allocator>() { + @Override + public ContextInScopeCallableWrapper createInstance() { + return new ContextInScopeCallableWrapper<>(ElasticApmTracer.this); } }); sampler = ProbabilitySampler.of(coreConfiguration.getSampleRate().get()); @@ -302,11 +328,36 @@ 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 ContextInScopeRunnableWrapper) { + return delegate; + } + return runnableContextWrapperObjectPool.createInstance().wrap(delegate, traceContext); + } + + public void recycle(ContextInScopeRunnableWrapper wrapper) { + runnableContextWrapperObjectPool.recycle(wrapper); + } + + public Callable wrapCallable(Callable delegate, TraceContext traceContext) { + if (delegate instanceof ContextInScopeCallableWrapper) { + return delegate; + } + return ((ContextInScopeCallableWrapper) callableContextWrapperObjectPool.createInstance()).wrap(delegate, traceContext); } - public void recycle(InScopeRunnableWrapper wrapper) { - runnableWrapperObjectPool.recycle(wrapper); + public void recycle(ContextInScopeCallableWrapper callableWrapper) { + callableContextWrapperObjectPool.recycle(callableWrapper); } /** 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 1cd8f2c257..bc33419e8b 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 deactivating or recycling 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 5c1ae88404..e095caf6eb 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 @@ -186,4 +186,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 f8744861b9..81ae7d9b69 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 @@ -28,6 +28,7 @@ import javax.annotation.Nullable; import java.util.List; +import java.util.concurrent.Callable; /** * An abstraction of both {@link TraceContext} and {@link AbstractSpan}. @@ -62,44 +63,30 @@ protected TraceContextHolder(ElasticApmTracer tracer) { public abstract boolean isChildOf(TraceContextHolder other); public T activate() { - try { - tracer.activate(this); - List activationListeners = tracer.getActivationListeners(); - for (int i = 0; i < activationListeners.size(); i++) { - try { - activationListeners.get(i).onActivate(this); - } catch (Error e) { - throw e; - } catch (Throwable t) { - logger.warn("Exception while calling {}#onActivate", activationListeners.get(i).getClass().getSimpleName(), t); - } - } - } catch (Throwable t) { + tracer.activate(this); + List activationListeners = tracer.getActivationListeners(); + for (int i = 0; i < activationListeners.size(); i++) { try { - logger.error("Unexpected error while activating context", t); - } catch (Throwable ignore) { + activationListeners.get(i).onActivate(this); + } catch (Error e) { + throw e; + } catch (Throwable t) { + logger.warn("Exception while calling {}#onActivate", activationListeners.get(i).getClass().getSimpleName(), t); } } return (T) this; } public T deactivate() { - try { - tracer.deactivate(this); - List activationListeners = tracer.getActivationListeners(); - for (int i = 0; i < activationListeners.size(); i++) { - try { - activationListeners.get(i).onDeactivate(); - } catch (Error e) { - throw e; - } catch (Throwable t) { - logger.warn("Exception while calling {}#onDeactivate", activationListeners.get(i).getClass().getSimpleName(), t); - } - } - } catch (Throwable t) { + tracer.deactivate(this); + List activationListeners = tracer.getActivationListeners(); + for (int i = 0; i < activationListeners.size(); i++) { try { - logger.error("Unexpected error while activating context", t); - } catch (Throwable ignore) { + activationListeners.get(i).onDeactivate(); + } catch (Error e) { + throw e; + } catch (Throwable t) { + logger.warn("Exception while calling {}#onDeactivate", activationListeners.get(i).getClass().getSimpleName(), t); } } return (T) this; @@ -134,4 +121,28 @@ public T captureException(@Nullable Throwable t) { return (T) this; } + /** + * Wraps the provided {@link 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()); + } + + /** + * Wraps the provided {@link Callable} and makes this {@link TraceContext} active in the {@link Callable#call()} 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 java.util.concurrent.Callable} is executed in. + *

+ */ + public Callable withActiveContext(Callable runnable) { + return tracer.wrapCallable(runnable, getTraceContext()); + } + } diff --git a/apm-agent-core/src/main/resources/META-INF/NOTICE b/apm-agent-core/src/main/resources/META-INF/NOTICE index 7fde0822d1..c2c6489c47 100644 --- a/apm-agent-core/src/main/resources/META-INF/NOTICE +++ b/apm-agent-core/src/main/resources/META-INF/NOTICE @@ -8,3 +8,7 @@ under the Apache License 2.0. See: This product includes software derived from micrometer, under the Apache License 2.0. See: - co.elastic.apm.agent.metrics.ProcessorMetrics + +This product includes software derived from https://github.com/raphw/weak-lock-free, +under the Apache License 2.0. See: + - co.elastic.apm.agent.util.weaklockfree.WeakConcurrentMap diff --git a/apm-agent-core/src/test/java/co/elastic/apm/agent/MockReporter.java b/apm-agent-core/src/test/java/co/elastic/apm/agent/MockReporter.java index 161eae5948..4e4ddddf10 100644 --- a/apm-agent-core/src/test/java/co/elastic/apm/agent/MockReporter.java +++ b/apm-agent-core/src/test/java/co/elastic/apm/agent/MockReporter.java @@ -80,13 +80,13 @@ private static JsonSchema getSchema(String resource) { } @Override - public void report(Transaction transaction) { + public synchronized void report(Transaction transaction) { verifyTransactionSchema(asJson(dslJsonSerializer.toJsonString(transaction))); transactions.add(transaction); } @Override - public void report(Span span) { + public synchronized void report(Span span) { verifySpanSchema(asJson(dslJsonSerializer.toJsonString(span))); spans.add(span); } @@ -119,27 +119,42 @@ private JsonNode asJson(String jsonContent) { } } - public List getTransactions() { + public synchronized List getTransactions() { return Collections.unmodifiableList(transactions); } - public Transaction getFirstTransaction() { + public synchronized Transaction getFirstTransaction() { return transactions.iterator().next(); } public Transaction getFirstTransaction(long timeoutMs) throws InterruptedException { final long end = System.currentTimeMillis() + timeoutMs; do { - if (!transactions.isEmpty()) { - return getFirstTransaction(); + synchronized (this) { + if (!transactions.isEmpty()) { + return getFirstTransaction(); + } } Thread.sleep(1); } while (System.currentTimeMillis() < end); return getFirstTransaction(); } + public Span getFirstSpan(long timeoutMs) throws InterruptedException { + final long end = System.currentTimeMillis() + timeoutMs; + do { + synchronized (this) { + if (!spans.isEmpty()) { + return getFirstSpan(); + } + } + Thread.sleep(1); + } while (System.currentTimeMillis() < end); + return getFirstSpan(); + } + @Override - public void report(ErrorCapture error) { + public synchronized void report(ErrorCapture error) { verifyErrorSchema(asJson(dslJsonSerializer.toJsonString(error))); errors.add(error); } @@ -149,19 +164,19 @@ public void scheduleMetricReporting(MetricRegistry metricRegistry, long interval // noop } - public Span getFirstSpan() { + public synchronized Span getFirstSpan() { return spans.get(0); } - public List getSpans() { + public synchronized List getSpans() { return spans; } - public List getErrors() { + public synchronized List getErrors() { return Collections.unmodifiableList(errors); } - public ErrorCapture getFirstError() { + public synchronized ErrorCapture getFirstError() { return errors.iterator().next(); } diff --git a/apm-agent-core/src/test/resources/elasticapm.properties b/apm-agent-core/src/test/resources/elasticapm.properties index 28a8dbb8e7..7bbb36c7f2 100644 --- a/apm-agent-core/src/test/resources/elasticapm.properties +++ b/apm-agent-core/src/test/resources/elasticapm.properties @@ -2,4 +2,5 @@ log_level=DEBUG log_file=System.out # incubating instrumentations should be active in tests disable_instrumentations= +excluded_from_instrumentation=java.util.concurrent.* application_packages=co.elastic.apm 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 0000000000..d44a10e1c0 --- /dev/null +++ b/apm-agent-plugins/apm-java-concurrent-plugin/pom.xml @@ -0,0 +1,29 @@ + + + + apm-agent-plugins + co.elastic.apm + 1.3.1-SNAPSHOT + + 4.0.0 + + apm-java-concurrent-plugin + + + + ${project.groupId} + apm-agent-api + ${project.version} + test + + + io.netty + netty-transport + 4.1.29.Final + test + + + + 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 0000000000..aad5b28afd --- /dev/null +++ b/apm-agent-plugins/apm-java-concurrent-plugin/src/main/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentation.java @@ -0,0 +1,149 @@ +/*- + * #%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.bci.VisibleForAdvice; +import co.elastic.apm.agent.impl.transaction.TraceContextHolder; +import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentSet; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.NamedElement; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; +import static net.bytebuddy.matcher.ElementMatchers.nameContains; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +public abstract class ExecutorInstrumentation extends ElasticApmInstrumentation { + + @VisibleForAdvice + public static final WeakConcurrentSet excluded = new WeakConcurrentSet<>(WeakConcurrentSet.Cleaner.THREAD); + + @Override + public ElementMatcher getTypeMatcherPreFilter() { + return nameContains("Execut") + .or(nameContains("Loop")) + .or(nameContains("Pool")) + .or(nameContains("Dispatch")); + } + + @Override + public ElementMatcher getTypeMatcher() { + return hasSuperType(named("java.util.concurrent.Executor")); + } + + @Override + public Collection getInstrumentationGroupNames() { + return Arrays.asList("concurrent", "executor"); + } + + public static class ExecutorRunnableInstrumentation extends ExecutorInstrumentation { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onExecute(@Advice.This Executor thiz, + @Advice.Argument(value = 0, readOnly = false) @Nullable Runnable runnable, + @Advice.Local("original") Runnable original) { + final TraceContextHolder active = ExecutorInstrumentation.getActive(); + if (active != null && runnable != null && !excluded.contains(thiz)) { + original = runnable; + runnable = active.withActiveContext(runnable); + } + } + + // This advice detects if the Executor can't cope with our wrappers + // If so, it retries without the wrapper and adds it to a list of excluded Executor instances + // which disables context propagation for those + // There is a slight risk that retrying causes a side effect but the more likely scenario is that adding the task to the queue + // fails and noting has been executed yet. + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Exception.class, repeatOn = Advice.OnNonDefaultValue.class) + public static boolean onError(@Advice.This Executor thiz, + @Nullable @Advice.Thrown Exception exception, + @Nullable @Advice.Argument(value = 0, readOnly = false) Runnable runnable, + @Advice.Local("original") @Nullable Runnable original) { + + if (original != null && (exception instanceof ClassCastException || exception instanceof IllegalArgumentException)) { + // seems like this executor expects a specific subtype of Callable + runnable = original; + // repeat only if submitting a task fails for the first time + return excluded.add(thiz); + } else { + // don't repeat on exceptions which don't seem to be caused by wrapping the runnable + return false; + } + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("execute").and(returns(void.class)).and(takesArguments(Runnable.class)) + .or(named("submit").and(returns(Future.class)).and(takesArguments(Runnable.class))) + .or(named("submit").and(returns(Future.class)).and(takesArguments(Runnable.class, Object.class))); + } + } + + public static class ExecutorCallableInstrumentation extends ExecutorInstrumentation { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void onSubmit(@Advice.This ExecutorService thiz, + @Advice.Argument(value = 0, readOnly = false) @Nullable Callable callable, + @Advice.Local("original") Callable original) { + final TraceContextHolder active = ExecutorInstrumentation.getActive(); + if (active != null && callable != null && !excluded.contains(thiz)) { + original = callable; + callable = active.withActiveContext(callable); + } + } + + // This advice detects if the Executor can't cope with our wrappers + // If so, it retries without the wrapper and adds it to a list of excluded Executor instances + // which disables context propagation for those + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Exception.class, repeatOn = Advice.OnNonDefaultValue.class) + public static boolean onError(@Advice.This Executor thiz, + @Nullable @Advice.Thrown Exception exception, + @Nullable @Advice.Argument(value = 0, readOnly = false) Callable callable, + @Advice.Local("original") Callable original) { + if (exception instanceof ClassCastException || exception instanceof IllegalArgumentException) { + // seems like this executor expects a specific subtype of Callable + callable = original; + // repeat only if submitting a task fails for the first time + return excluded.add(thiz); + } else { + // don't repeat on exceptions which don't seem to be caused by wrapping the runnable + return false; + } + } + + @Override + public ElementMatcher getMethodMatcher() { + return named("submit").and(returns(Future.class)).and(takesArguments(Callable.class)); + } + + } + +} 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 0000000000..4244aae127 --- /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 0000000000..f516f6dddf --- /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,2 @@ +co.elastic.apm.agent.concurrent.ExecutorInstrumentation$ExecutorRunnableInstrumentation +co.elastic.apm.agent.concurrent.ExecutorInstrumentation$ExecutorCallableInstrumentation diff --git a/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentationTest.java b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentationTest.java new file mode 100644 index 0000000000..589775a0a9 --- /dev/null +++ b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorInstrumentationTest.java @@ -0,0 +1,123 @@ +/*- + * #%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.AbstractInstrumentationTest; +import co.elastic.apm.agent.impl.transaction.Transaction; +import io.netty.util.concurrent.GlobalEventExecutor; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(Parameterized.class) +public class ExecutorInstrumentationTest extends AbstractInstrumentationTest { + + private final ExecutorService executor; + private Transaction transaction; + + public ExecutorInstrumentationTest(Supplier supplier) { + executor = supplier.get(); + } + + @Parameterized.Parameters() + public static Iterable> data() { + return Arrays.asList(() -> ExecutorServiceWrapper.wrap(Executors.newSingleThreadExecutor()), + () -> ExecutorServiceWrapper.wrap(Executors.newSingleThreadScheduledExecutor()), + () -> ExecutorServiceWrapper.wrap(new ForkJoinPool()), + () -> GlobalEventExecutor.INSTANCE); + } + + @Before + public void setUp() { + transaction = tracer.startTransaction().withName("Transaction").activate(); + } + + @After + public void tearDown() { + assertThat(tracer.getActive()).isNull(); + } + + @Test + public void testExecutorSubmitRunnableAnonymousInnerClass() throws Exception { + executor.submit(new Runnable() { + @Override + public void run() { + createAsyncSpan(); + } + }).get(); + + assertOnlySpanIsChildOfOnlyTransaction(); + } + + @Test + public void testExecutorSubmitRunnableLambda() throws Exception { + executor.submit(() -> createAsyncSpan()).get(1, TimeUnit.SECONDS); + assertOnlySpanIsChildOfOnlyTransaction(); + } + + @Test + public void testExecutorExecute() throws Exception { + executor.execute(this::createAsyncSpan); + assertOnlySpanIsChildOfOnlyTransaction(); + } + + @Test + public void testExecutorSubmitRunnableWithResult() throws Exception { + executor.submit(this::createAsyncSpan, null); + assertOnlySpanIsChildOfOnlyTransaction(); + } + + @Test + public void testExecutorSubmitCallableMethodReference() throws Exception { + executor.submit(() -> { + createAsyncSpan(); + return null; + }).get(1, TimeUnit.SECONDS); + assertOnlySpanIsChildOfOnlyTransaction(); + } + + private void assertOnlySpanIsChildOfOnlyTransaction() throws InterruptedException { + try { + // wait for the async operation to end + assertThat(reporter.getFirstSpan(1000)).isNotNull(); + } finally { + transaction.deactivate().end(); + } + assertThat(reporter.getTransactions()).hasSize(1); + assertThat(reporter.getSpans()).hasSize(1); + assertThat(reporter.getFirstSpan().isChildOf(reporter.getFirstTransaction())).isTrue(); + } + + private void createAsyncSpan() { + assertThat(tracer.getActive().getTraceContext().getId()).isEqualTo(transaction.getTraceContext().getId()); + tracer.getActive().createSpan().withName("Async").end(); + } +} diff --git a/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorServiceWrapper.java b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorServiceWrapper.java new file mode 100644 index 0000000000..60edff9688 --- /dev/null +++ b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/ExecutorServiceWrapper.java @@ -0,0 +1,115 @@ +/*- + * #%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 javax.annotation.Nonnull; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class ExecutorServiceWrapper implements ExecutorService { + + private final ExecutorService delegate; + + public static ExecutorService wrap(ExecutorService delegate) { + return new ExecutorServiceWrapper(delegate); + } + + public ExecutorServiceWrapper(ExecutorService delegate) { + this.delegate = delegate; + } + + @Override + public void shutdown() { + delegate.shutdown(); + } + + @Override + public List shutdownNow() { + return delegate.shutdownNow(); + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, @Nonnull TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + + // Runnable + + @Override + public void execute(@Nonnull final Runnable command) { + delegate.execute(command); + } + + @Override + public Future submit(@Nonnull final Runnable task) { + return delegate.submit(task); + } + + @Override + public Future submit(@Nonnull final Runnable task, T result) { + return delegate.submit(task, result); + } + + // Callable + + @Override + public Future submit(@Nonnull final Callable task) { + return delegate.submit(task); + } + + // Collection + + @Override + public List> invokeAll(@Nonnull Collection> tasks) throws InterruptedException { + return delegate.invokeAll(tasks); + } + + @Override + public List> invokeAll(@Nonnull Collection> tasks, long timeout, @Nonnull TimeUnit unit) throws InterruptedException { + return delegate.invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(@Nonnull Collection> tasks) throws InterruptedException, ExecutionException { + return delegate.invokeAny(tasks); + } + + @Override + public T invokeAny(@Nonnull Collection> tasks, long timeout, @Nonnull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return delegate.invokeAny(tasks, timeout, unit); + } +} diff --git a/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/FailingExecutorInstrumentationTest.java b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/FailingExecutorInstrumentationTest.java new file mode 100644 index 0000000000..9d73f13adf --- /dev/null +++ b/apm-agent-plugins/apm-java-concurrent-plugin/src/test/java/co/elastic/apm/agent/concurrent/FailingExecutorInstrumentationTest.java @@ -0,0 +1,128 @@ +/*- + * #%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.AbstractInstrumentationTest; +import co.elastic.apm.agent.impl.ContextInScopeCallableWrapper; +import co.elastic.apm.agent.impl.ContextInScopeRunnableWrapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FailingExecutorInstrumentationTest extends AbstractInstrumentationTest { + + private ExecutorService executor; + private AtomicInteger runCounter; + private AtomicInteger submitWithWrapperCounter; + + @BeforeEach + void setUp() { + executor = ExecutorServiceWrapper.wrap(new ForkJoinPool() { + @Override + public ForkJoinTask submit(Runnable task) { + if (task instanceof ContextInScopeRunnableWrapper) { + submitWithWrapperCounter.incrementAndGet(); + throw new ClassCastException(); + } + return super.submit(task); + } + + @Override + public ForkJoinTask submit(Callable task) { + if (task instanceof ContextInScopeCallableWrapper) { + submitWithWrapperCounter.incrementAndGet(); + throw new IllegalArgumentException(); + } + return super.submit(task); + } + + @Override + public void execute(Runnable task) { + throw new IllegalArgumentException(); + } + + @Override + public ForkJoinTask submit(Runnable task, T result) { + throw new UnsupportedOperationException(); + } + }); + tracer.startTransaction().activate(); + runCounter = new AtomicInteger(); + submitWithWrapperCounter = new AtomicInteger(); + } + + @AfterEach + void tearDown() { + tracer.currentTransaction().deactivate().end(); + } + + @Test + void testRunnableWrappersNotSupported() throws Exception { + executor.submit(() -> { + assertThat(runCounter.incrementAndGet()).isEqualTo(1); + }).get(); + assertThat(submitWithWrapperCounter.get()).isEqualTo(1); + + assertThat(ExecutorInstrumentation.excluded.contains(executor)).isTrue(); + executor.submit(() -> { + assertThat(runCounter.incrementAndGet()).isEqualTo(2); + }).get(); + assertThat(submitWithWrapperCounter.get()).isEqualTo(1); + } + + @Test + void testCallableWrappersNotSupported() throws Exception { + executor.submit(() -> { + assertThat(runCounter.incrementAndGet()).isEqualTo(1); + return null; + }).get(); + assertThat(submitWithWrapperCounter.get()).isEqualTo(1); + + assertThat(ExecutorInstrumentation.excluded.contains(executor)).isTrue(); + executor.submit(() -> { + assertThat(runCounter.incrementAndGet()).isEqualTo(2); + }).get(); + assertThat(submitWithWrapperCounter.get()).isEqualTo(1); + } + + @Test + void testOnlyRetryOnce() { + assertThatThrownBy(() -> executor.execute(() -> { + })).isInstanceOf(IllegalArgumentException.class); + assertThat(ExecutorInstrumentation.excluded.contains(executor)).isTrue(); + } + + @Test + void testUnrelatedException() { + assertThatThrownBy(() -> executor.submit(() -> { + }, null)).isInstanceOf(UnsupportedOperationException.class); + assertThat(ExecutorInstrumentation.excluded.contains(executor)).isFalse(); + } + +} 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 44ba9afb99..a249067ea7 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 @@ -164,10 +164,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 db569419ce..67d34c5d37 100644 --- a/apm-agent-plugins/pom.xml +++ b/apm-agent-plugins/pom.xml @@ -22,6 +22,7 @@ apm-slf4j-plugin apm-es-restclient-plugin apm-okhttp-plugin + apm-java-concurrent-plugin diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index 6aedda3caa..10552baf6e 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -252,7 +252,7 @@ you should add an additional entry to this list (make sure to also include the d ==== `disable_instrumentations` A list of instrumentations which should be disabled. -Valid options are `annotations`, `apache-httpclient`, `dispatcher-servlet`, `elasticsearch-restclient`, `http-client`, `incubating`, `jax-rs`, `jax-rs-annotations`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `spring-resttemplate`. +Valid options are `annotations`, `apache-httpclient`, `concurrent`, `dispatcher-servlet`, `elasticsearch-restclient`, `executor`, `http-client`, `incubating`, `jax-rs`, `jax-rs-annotations`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `spring-resttemplate`. If you want to try out incubating features, set the value to an empty string. @@ -1056,7 +1056,7 @@ The default unit for this option is `ms` # sanitize_field_names=password,passwd,pwd,secret,*key,*token*,*session*,*credit*,*card*,authorization,set-cookie # A list of instrumentations which should be disabled. -# Valid options are `annotations`, `apache-httpclient`, `dispatcher-servlet`, `elasticsearch-restclient`, `http-client`, `incubating`, `jax-rs`, `jax-rs-annotations`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `spring-resttemplate`. +# Valid options are `annotations`, `apache-httpclient`, `concurrent`, `dispatcher-servlet`, `elasticsearch-restclient`, `executor`, `http-client`, `incubating`, `jax-rs`, `jax-rs-annotations`, `jdbc`, `jsf`, `okhttp`, `opentracing`, `public-api`, `render`, `servlet-api`, `servlet-api-async`, `spring-mvc`, `spring-resttemplate`. # If you want to try out incubating features, # set the value to an empty string. # diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index 31b0233539..e8028e3671 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -141,7 +141,7 @@ Distributed tracing will only work if you are using one of the supported network |=== |Framework |Supported versions | Description | Since -|Apache HttpClient (<>) +|Apache HttpClient |4.3+ |The agent automatically creates spans for outgoing HTTP requests and propagates tracing headers. The spans are named after the schema ` `. @@ -163,6 +163,26 @@ Distributed tracing will only work if you are using one of the supported network |=== + +[float] +[[supported-async-frameworks]] +=== Asynchronous frameworks +When a Span is created in a different Thread than its parent, +the trace context has to be propagated onto this thread. + +This section lists all supported asynchronous frameworks. + +|=== +|Framework |Supported versions | Description | Since + +|ExecutorService +| +|The agent propagates the context when using the `java.util.concurrent.ExecutorService` methods of any `ExecutorService` implementation. +|1.4.0 + +|=== + + [float] [[supported-technologies-caveats]] === Caveats diff --git a/elastic-apm-agent/pom.xml b/elastic-apm-agent/pom.xml index 9eac697a92..85b12cb6a1 100644 --- a/elastic-apm-agent/pom.xml +++ b/elastic-apm-agent/pom.xml @@ -152,6 +152,11 @@ apm-httpclient-core ${project.version} + + ${project.groupId} + apm-java-concurrent-plugin + ${project.version} + ${project.groupId} apm-jaxrs-plugin diff --git a/integration-tests/simple-webapp-integration-test/src/test/java/co/elastic/apm/servlet/AbstractServletContainerIntegrationTest.java b/integration-tests/simple-webapp-integration-test/src/test/java/co/elastic/apm/servlet/AbstractServletContainerIntegrationTest.java index f8ba88d59b..b47a222af6 100644 --- a/integration-tests/simple-webapp-integration-test/src/test/java/co/elastic/apm/servlet/AbstractServletContainerIntegrationTest.java +++ b/integration-tests/simple-webapp-integration-test/src/test/java/co/elastic/apm/servlet/AbstractServletContainerIntegrationTest.java @@ -196,11 +196,21 @@ public void testAllScenarios() throws Exception { testTransactionReporting(); testTransactionErrorReporting(); testSpanErrorReporting(); + testExecutorService(); for (TestApp testApp : getTestApps()) { testApp.testMethod.accept(this); } } + private void testExecutorService() throws Exception { + mockServerContainer.getClient().clear(HttpRequest.request(), ClearType.LOG); + final String pathToTest = contextPath + "/executor-service-servlet"; + executeAndValidateRequest(pathToTest, null, 200); + String transactionId = assertTransactionReported(pathToTest, 200).get("id").textValue(); + final List spans = assertSpansTransactionId(500, this::getReportedSpans, transactionId); + assertThat(spans).hasSize(1); + } + private void testTransactionReporting() throws Exception { for (String pathToTest : getPathsToTest()) { pathToTest = contextPath + pathToTest; @@ -253,7 +263,9 @@ void executeAndValidateRequest(String pathToTest, String expectedContent, int ex assertThat(response.code()).withFailMessage(response.toString() + getServerLogs()).isEqualTo(expectedResponseCode); final ResponseBody responseBody = response.body(); assertThat(responseBody).isNotNull(); - assertThat(responseBody.string()).contains(expectedContent); + if (expectedContent != null) { + assertThat(responseBody.string()).contains(expectedContent); + } } Response executeRequest(String pathToTest) throws IOException { diff --git a/integration-tests/simple-webapp/src/main/java/co/elastic/webapp/ExecutorServiceTestServlet.java b/integration-tests/simple-webapp/src/main/java/co/elastic/webapp/ExecutorServiceTestServlet.java new file mode 100644 index 0000000000..bb99686afc --- /dev/null +++ b/integration-tests/simple-webapp/src/main/java/co/elastic/webapp/ExecutorServiceTestServlet.java @@ -0,0 +1,64 @@ +/*- + * #%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.webapp; + +import co.elastic.apm.api.ElasticApm; +import co.elastic.apm.api.Transaction; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class ExecutorServiceTestServlet extends HttpServlet { + + private ExecutorService executor; + + @Override + public void init() { + executor = Executors.newSingleThreadExecutor(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + final Transaction transaction = ElasticApm.currentTransaction(); + if (!transaction.isSampled()) { + throw new IllegalStateException("Transaction is not sampled"); + } + + try { + executor.submit(new Runnable() { + @Override + public void run() { + if (!ElasticApm.currentSpan().getId().equals(transaction.getId())) { + throw new IllegalStateException("Context not propagated"); + } + ElasticApm.currentSpan().createSpan().setName("Async").end(); + } + }).get(); + } catch (Exception e) { + throw new ServletException(e); + } + } + +} diff --git a/integration-tests/simple-webapp/src/main/webapp/WEB-INF/web.xml b/integration-tests/simple-webapp/src/main/webapp/WEB-INF/web.xml index 96aa3dc775..c7b5bfdc26 100644 --- a/integration-tests/simple-webapp/src/main/webapp/WEB-INF/web.xml +++ b/integration-tests/simple-webapp/src/main/webapp/WEB-INF/web.xml @@ -24,6 +24,12 @@ 1 true + + ExecutorServiceTestServlet + co.elastic.webapp.ExecutorServiceTestServlet + 1 + true + TestServlet @@ -37,4 +43,8 @@ AsyncStartServlet /async-start-servlet + + ExecutorServiceTestServlet + /executor-service-servlet + diff --git a/pom.xml b/pom.xml index c64932d647..4abbed8bec 100644 --- a/pom.xml +++ b/pom.xml @@ -58,14 +58,14 @@ 2.2.0 1.4.196 2.9.8 - 5.1.1 + 5.3.2 4.12 1.2.3 3.9.1 1.7.25 5.0.4.RELEASE 9.4.11.v20180605 - 5.1.1 + 5.3.2 0.1.19 3.7.0