diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 9d18b00606..99f60db695 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -54,6 +54,7 @@ endif::[]
* Added support for setting the service name on Log4j2's EcsLayout - {pull}2296[#2296]
* Print the used instrumentation groups when the application stops - {pull}2448[#2448]
* Add `elastic.apm.start_async` property that makes the agent start on a non-premain/main thread - {pull}2454[#2454]
+* Added support for setting the application class loader for a transaction via the public api - {pull}2444[#2444]
[float]
===== Bug fixes
diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/NoopTransaction.java b/apm-agent-api/src/main/java/co/elastic/apm/api/NoopTransaction.java
index fe66caf0da..9869cbe31c 100644
--- a/apm-agent-api/src/main/java/co/elastic/apm/api/NoopTransaction.java
+++ b/apm-agent-api/src/main/java/co/elastic/apm/api/NoopTransaction.java
@@ -226,4 +226,10 @@ public Span setDestinationAddress(@Nullable String address, int port) {
public Span setDestinationService(@Nullable String resource) {
return this;
}
+
+ @Nonnull
+ @Override
+ public Transaction setApplicationClassLoader(@Nullable ClassLoader classLoader) {
+ return this;
+ }
}
diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/Transaction.java b/apm-agent-api/src/main/java/co/elastic/apm/api/Transaction.java
index a4a4f1d4ee..f929f26067 100644
--- a/apm-agent-api/src/main/java/co/elastic/apm/api/Transaction.java
+++ b/apm-agent-api/src/main/java/co/elastic/apm/api/Transaction.java
@@ -331,4 +331,16 @@ public interface Transaction extends Span {
@Override
Scope activate();
+ /**
+ * Sets the class loader of the application that started the transaction.
+ *
+ * The class loader is used to determine the service name and version of the transaction. If this method is called
+ * after child spans are already created, they may be associated with the wrong service name and version.
+ *
+ *
+ * @param classLoader the class loader of the application that started the transaction
+ * @return this
+ */
+ @Nonnull
+ Transaction setApplicationClassLoader(ClassLoader classLoader);
}
diff --git a/apm-agent-api/src/main/java/co/elastic/apm/api/TransactionImpl.java b/apm-agent-api/src/main/java/co/elastic/apm/api/TransactionImpl.java
index da0ce5bfe5..5cfe35cd62 100644
--- a/apm-agent-api/src/main/java/co/elastic/apm/api/TransactionImpl.java
+++ b/apm-agent-api/src/main/java/co/elastic/apm/api/TransactionImpl.java
@@ -186,4 +186,10 @@ public Transaction setDestinationAddress(@Nullable String address, int port) {
public Transaction setDestinationService(@Nullable String resource) {
throw new UnsupportedOperationException();
}
+
+ @Nonnull
+ public Transaction setApplicationClassLoader(ClassLoader classLoader) {
+ // co.elastic.apm.agent.pluginapi.TransactionInstrumentation.SetClassLoaderInstrumentation
+ return this;
+ }
}
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 e7da0791ea..260de07089 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
@@ -174,7 +174,7 @@ public Transaction startRootTransaction(Sampler sampler, long epochMicros, @Null
Transaction transaction = null;
if (isRunning()) {
transaction = createTransaction().start(TraceContext.asRoot(), null, epochMicros, sampler, initiatingClassLoader);
- afterTransactionStart(initiatingClassLoader, transaction);
+ afterTransactionStart(transaction);
}
return transaction;
}
@@ -199,7 +199,7 @@ public Transaction startChildTransaction(@Nullable C headerCarrier, TextHead
if (isRunning()) {
transaction = createTransaction().start(TraceContext.getFromTraceContextTextHeaders(), headerCarrier,
textHeadersGetter, epochMicros, sampler, initiatingClassLoader);
- afterTransactionStart(initiatingClassLoader, transaction);
+ afterTransactionStart(transaction);
}
return transaction;
}
@@ -218,12 +218,12 @@ public Transaction startChildTransaction(@Nullable C headerCarrier, BinaryHe
if (isRunning()) {
transaction = createTransaction().start(TraceContext.getFromTraceContextBinaryHeaders(), headerCarrier,
binaryHeadersGetter, epochMicros, sampler, initiatingClassLoader);
- afterTransactionStart(initiatingClassLoader, transaction);
+ afterTransactionStart(transaction);
}
return transaction;
}
- private void afterTransactionStart(@Nullable ClassLoader initiatingClassLoader, Transaction transaction) {
+ private void afterTransactionStart(Transaction transaction) {
if (logger.isDebugEnabled()) {
logger.debug("startTransaction {}", transaction);
if (logger.isTraceEnabled()) {
@@ -231,11 +231,6 @@ private void afterTransactionStart(@Nullable ClassLoader initiatingClassLoader,
new RuntimeException("this exception is just used to record where the transaction has been started from"));
}
}
- final ServiceInfo serviceInfo = getServiceInfo(initiatingClassLoader);
- if (serviceInfo != null) {
- transaction.getTraceContext().setServiceName(serviceInfo.getServiceName());
- transaction.getTraceContext().setServiceVersion(serviceInfo.getServiceVersion());
- }
}
public Transaction noopTransaction() {
@@ -344,11 +339,7 @@ private ErrorCapture captureException(long epochMicros, @Nullable Throwable e, @
parent.setNonDiscardable();
} else {
error.getTraceContext().getId().setToRandomValue();
- ServiceInfo serviceInfo = getServiceInfo(initiatingClassLoader);
- if (serviceInfo != null) {
- error.getTraceContext().setServiceName(serviceInfo.getServiceName());
- error.getTraceContext().setServiceVersion(serviceInfo.getServiceVersion());
- }
+ error.getTraceContext().setApplicationClassLoader(initiatingClassLoader);
}
return error;
}
diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java
index 455e6272b6..4f5b98a9c1 100644
--- a/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java
+++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java
@@ -19,17 +19,18 @@
package co.elastic.apm.agent.impl.transaction;
import co.elastic.apm.agent.configuration.CoreConfiguration;
+import co.elastic.apm.agent.configuration.ServiceInfo;
import co.elastic.apm.agent.impl.ElasticApmTracer;
import co.elastic.apm.agent.impl.Tracer;
import co.elastic.apm.agent.impl.sampling.Sampler;
import co.elastic.apm.agent.objectpool.Recyclable;
+import co.elastic.apm.agent.sdk.logging.Logger;
+import co.elastic.apm.agent.sdk.logging.LoggerFactory;
import co.elastic.apm.agent.sdk.weakconcurrent.WeakConcurrent;
import co.elastic.apm.agent.sdk.weakconcurrent.WeakMap;
import co.elastic.apm.agent.util.ByteUtils;
import co.elastic.apm.agent.util.ClassLoaderUtils;
import co.elastic.apm.agent.util.HexUtils;
-import co.elastic.apm.agent.sdk.logging.Logger;
-import co.elastic.apm.agent.sdk.logging.LoggerFactory;
import javax.annotation.Nullable;
import java.lang.ref.WeakReference;
@@ -732,8 +733,8 @@ public int hashCode() {
return Objects.hash(traceId, id, parentId, flags);
}
- void setApplicationClassLoader(@Nullable ClassLoader classLoader) {
- if (ClassLoaderUtils.isBootstrapClassLoader(classLoader) || ClassLoaderUtils.isAgentClassLoader(classLoader)) {
+ public void setApplicationClassLoader(@Nullable ClassLoader classLoader) {
+ if (ClassLoaderUtils.isBootstrapClassLoader(classLoader)) {
return;
}
WeakReference local = classLoaderWeakReferenceCache.get(classLoader);
@@ -742,6 +743,11 @@ void setApplicationClassLoader(@Nullable ClassLoader classLoader) {
classLoaderWeakReferenceCache.putIfAbsent(classLoader, local);
}
applicationClassLoader = local;
+ final ServiceInfo serviceInfo = tracer.getServiceInfo(classLoader);
+ if (serviceInfo != null) {
+ setServiceName(serviceInfo.getServiceName());
+ setServiceVersion(serviceInfo.getServiceVersion());
+ }
}
@Nullable
diff --git a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentation.java b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentation.java
index d96638fabd..6c5bae4839 100644
--- a/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentation.java
+++ b/apm-agent-plugins/apm-api-plugin/src/main/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentation.java
@@ -147,4 +147,22 @@ public static void addCustomContext(@Advice.FieldValue(value = "span", typing =
}
}
}
+
+ public static class SetClassLoaderInstrumentation extends TransactionInstrumentation {
+ public SetClassLoaderInstrumentation() {
+ super(named("setApplicationClassLoader"));
+ }
+
+ public static class AdviceClass {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class, inline = false)
+ public static void setClassLoader(@Advice.FieldValue(value = "span", typing = Assigner.Typing.DYNAMIC) Object transaction,
+ @Advice.Argument(0) ClassLoader classLoader) {
+ if (transaction instanceof Transaction) {
+ TraceContext traceContext = ((Transaction) transaction).getTraceContext();
+ traceContext.setApplicationClassLoader(classLoader);
+ }
+ }
+ }
+ }
}
diff --git a/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation b/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation
index 161e9bceca..031ba28588 100644
--- a/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation
+++ b/apm-agent-plugins/apm-api-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation
@@ -7,6 +7,7 @@ co.elastic.apm.agent.pluginapi.TransactionInstrumentation$SetFrameworkNameInstru
co.elastic.apm.agent.pluginapi.TransactionInstrumentation$SetUserInstrumentation
co.elastic.apm.agent.pluginapi.TransactionInstrumentation$EnsureParentIdInstrumentation
co.elastic.apm.agent.pluginapi.TransactionInstrumentation$SetResultInstrumentation
+co.elastic.apm.agent.pluginapi.TransactionInstrumentation$SetClassLoaderInstrumentation
co.elastic.apm.agent.pluginapi.TransactionInstrumentation$AddCustomContextInstrumentation
co.elastic.apm.agent.pluginapi.AbstractSpanInstrumentation$SetNameInstrumentation
co.elastic.apm.agent.pluginapi.AbstractSpanInstrumentation$SetTypeInstrumentation
diff --git a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentationTest.java b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentationTest.java
index cd84e97cba..e3263b959e 100644
--- a/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentationTest.java
+++ b/apm-agent-plugins/apm-api-plugin/src/test/java/co/elastic/apm/agent/pluginapi/TransactionInstrumentationTest.java
@@ -19,10 +19,12 @@
package co.elastic.apm.agent.pluginapi;
import co.elastic.apm.AbstractApiTest;
+import co.elastic.apm.agent.configuration.ServiceInfo;
import co.elastic.apm.agent.impl.TracerInternalApiUtils;
import co.elastic.apm.api.AbstractSpanImplAccessor;
import co.elastic.apm.api.ElasticApm;
import co.elastic.apm.api.Outcome;
+import co.elastic.apm.api.Scope;
import co.elastic.apm.api.Span;
import co.elastic.apm.api.Transaction;
import org.apache.commons.lang3.RandomStringUtils;
@@ -32,6 +34,8 @@
import org.junit.jupiter.params.provider.MethodSource;
import javax.annotation.Nullable;
+import java.net.URL;
+import java.net.URLClassLoader;
import java.security.SecureRandom;
import static org.assertj.core.api.Assertions.assertThat;
@@ -296,6 +300,24 @@ void setOutcome_success() {
testSetOutcome(Outcome.SUCCESS);
}
+ @Test
+ void setClassLoader() {
+ ClassLoader classLoader = new URLClassLoader(new URL[0]);
+
+ tracer.overrideServiceInfoForClassLoader(classLoader, ServiceInfo.of("My Service", "My Version"));
+
+ transaction.setApplicationClassLoader(classLoader);
+ try (Scope scope = transaction.activate()) {
+ assertThat(tracer.getActive().getTraceContext().getApplicationClassLoader()).isSameAs(classLoader);
+ } finally {
+ endTransaction();
+ }
+
+ co.elastic.apm.agent.impl.transaction.Transaction reportedTransaction = reporter.getFirstTransaction();
+ assertThat(reportedTransaction.getTraceContext().getServiceName()).isEqualTo("My Service");
+ assertThat(reportedTransaction.getTraceContext().getServiceVersion()).isEqualTo("My Version");
+ }
+
private void testSetOutcome(Outcome outcome) {
// set it first to a different value than the expected one
Outcome[] values = Outcome.values();
diff --git a/apm-agent-plugins/apm-log-correlation-plugin/src/main/java/co/elastic/apm/agent/mdc/MdcActivationListener.java b/apm-agent-plugins/apm-log-correlation-plugin/src/main/java/co/elastic/apm/agent/mdc/MdcActivationListener.java
index 769e94a800..7e3786f8c7 100644
--- a/apm-agent-plugins/apm-log-correlation-plugin/src/main/java/co/elastic/apm/agent/mdc/MdcActivationListener.java
+++ b/apm-agent-plugins/apm-log-correlation-plugin/src/main/java/co/elastic/apm/agent/mdc/MdcActivationListener.java
@@ -27,6 +27,7 @@
import co.elastic.apm.agent.logging.LoggingConfiguration;
import co.elastic.apm.agent.sdk.logging.Logger;
import co.elastic.apm.agent.sdk.logging.LoggerFactory;
+import co.elastic.apm.agent.util.ClassLoaderUtils;
import javax.annotation.Nullable;
import java.lang.invoke.MethodHandle;
@@ -227,10 +228,10 @@ public void after(TraceContext deactivatedContext, boolean isError) throws Throw
*/
private ClassLoader getApplicationClassLoader(TraceContext context) {
ClassLoader applicationClassLoader = context.getApplicationClassLoader();
- if (applicationClassLoader != null) {
- return applicationClassLoader;
- } else {
+ if (applicationClassLoader == null || ClassLoaderUtils.isAgentClassLoader(applicationClassLoader)) {
return getFallbackClassLoader();
+ } else {
+ return applicationClassLoader;
}
}
private ClassLoader getFallbackClassLoader() {
diff --git a/docs/public-api.asciidoc b/docs/public-api.asciidoc
index 2861f1c11e..db6b8ba952 100644
--- a/docs/public-api.asciidoc
+++ b/docs/public-api.asciidoc
@@ -551,6 +551,15 @@ use `UNKNOWN` when the outcome can't be properly known.
Outcome is used to compute error rates between services, using `UNKNOWN` will not alter those rates.
The value set through API will have higher priority over the value that might be set by auto-instrumentation.
+[float]
+[[api-transaction-set-class-loader]]
+==== `Transaction setApplicationClassLoader(ClassLoader classLoader)`
+Sets the class loader of the application that started the transaction.
+
+* `classLoader` the class loader of the application that started the transaction
+
+NOTE: The class loader is used to determine the service name and version of the transaction. If this method is called after
+child spans are already created, they may be associated with the wrong service name and version.
[float]
[[api-transaction-set-start-timestamp]]