Skip to content

Commit d9370ea

Browse files
authored
Fix OTel bridge behavior with multiple classloaders + improved activation (#2735)
* degug add classloader info * test more versions + stricter tests * minor refactor * add external plugin test with otel * minor cleanup * properly size wrapper * move wrapping to thread-local active stack * cleanup * improve javadoc + tune sizing * finalize testing & bugfix * update changelog * fix misleading comment * remove duplication * clarify javadoc * post-review changes
1 parent df96649 commit d9370ea

File tree

23 files changed

+943
-69
lines changed

23 files changed

+943
-69
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ version of classes that are available also in the parent (bootstrap) class loade
5252
* Update `async-profiler` to 1.8.8 to avoid missing symbol spam - {pull}2775[#2775]
5353
* Fix container ID discovery for containers managed through AWS Fargate - {pull}2772[#2772]
5454
* Make `traceparent` header computation thread-safe - {pull}2747[#2747]
55+
* Fix OTel bridge with multiple OTel APIs or external plugins - {pull}2735[#2735]
5556
5657
[[release-notes-1.x]]
5758
=== Java Agent version 1.x

apm-agent-core/src/main/java/co/elastic/apm/agent/configuration/CoreConfiguration.java

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
import org.stagemonitor.configuration.source.ConfigurationSource;
3939

4040
import javax.annotation.Nullable;
41+
import java.io.IOException;
42+
import java.nio.file.DirectoryStream;
43+
import java.nio.file.Files;
44+
import java.nio.file.Path;
45+
import java.nio.file.Paths;
4146
import java.util.Arrays;
4247
import java.util.Collection;
4348
import java.util.Collections;
@@ -232,7 +237,7 @@ public class CoreConfiguration extends ConfigurationOptionProvider {
232237
.description("Limits the amount of spans that are recorded per transaction.\n\n" +
233238
"This is helpful in cases where a transaction creates a very high amount of spans (e.g. thousands of SQL queries).\n\n" +
234239
"Setting an upper limit will prevent overloading the agent and the APM server with too much work for such edge cases.\n\n" +
235-
"A message will be logged when the max number of spans has been exceeded but only at a rate of once every " + TimeUnit.MICROSECONDS.toMinutes(Span.MAX_LOG_INTERVAL_MICRO_SECS) + " minutes to ensure performance is not impacted.")
240+
"A message will be logged when the max number of spans has been exceeded but only at a rate of once every " + TimeUnit.MICROSECONDS.toMinutes(Span.MAX_LOG_INTERVAL_MICRO_SECS) + " minutes to ensure performance is not impacted.")
236241
.dynamic(true)
237242
.buildWithDefault(500);
238243

@@ -241,20 +246,20 @@ public class CoreConfiguration extends ConfigurationOptionProvider {
241246
.key("sanitize_field_names")
242247
.configurationCategory(CORE_CATEGORY)
243248
.description("Sometimes it is necessary to sanitize the data sent to Elastic APM,\n" +
244-
"e.g. remove sensitive data.\n" +
245-
"\n" +
246-
"Configure a list of wildcard patterns of field names which should be sanitized.\n" +
247-
"These apply for example to HTTP headers and `application/x-www-form-urlencoded` data.\n" +
248-
"\n" +
249-
WildcardMatcher.DOCUMENTATION + "\n" +
250-
"\n" +
251-
"NOTE: Data in the query string is considered non-sensitive,\n" +
252-
"as sensitive information should not be sent in the query string.\n" +
253-
"See https://www.owasp.org/index.php/Information_exposure_through_query_strings_in_url for more information\n" +
254-
"\n" +
255-
"NOTE: Review the data captured by Elastic APM carefully to make sure it does not capture sensitive information.\n" +
256-
"If you do find sensitive data in the Elasticsearch index,\n" +
257-
"you should add an additional entry to this list (make sure to also include the default entries)."
249+
"e.g. remove sensitive data.\n" +
250+
"\n" +
251+
"Configure a list of wildcard patterns of field names which should be sanitized.\n" +
252+
"These apply for example to HTTP headers and `application/x-www-form-urlencoded` data.\n" +
253+
"\n" +
254+
WildcardMatcher.DOCUMENTATION + "\n" +
255+
"\n" +
256+
"NOTE: Data in the query string is considered non-sensitive,\n" +
257+
"as sensitive information should not be sent in the query string.\n" +
258+
"See https://www.owasp.org/index.php/Information_exposure_through_query_strings_in_url for more information\n" +
259+
"\n" +
260+
"NOTE: Review the data captured by Elastic APM carefully to make sure it does not capture sensitive information.\n" +
261+
"If you do find sensitive data in the Elasticsearch index,\n" +
262+
"you should add an additional entry to this list (make sure to also include the default entries)."
258263
/* A disadvantage of this approach is when a user adds a custom value,
259264
* they don't automatically pick up new default values.
260265
* But the possibility to remove default values which are leading to false positive for the user
@@ -850,7 +855,7 @@ public List<WildcardMatcher> getUnnestExceptions() {
850855
return unnestExceptions.get();
851856
}
852857

853-
public List<WildcardMatcher> getIgnoreExceptions(){
858+
public List<WildcardMatcher> getIgnoreExceptions() {
854859
return ignoreExceptions.get();
855860
}
856861

@@ -967,6 +972,29 @@ public String getPluginsDir() {
967972
}
968973
}
969974

975+
public int getExternalPluginsCount() {
976+
String pathString = getPluginsDir();
977+
if (pathString == null) {
978+
return 0;
979+
}
980+
Path pluginsDir = Paths.get(pathString);
981+
if (!Files.isDirectory(pluginsDir)) {
982+
return 0;
983+
}
984+
985+
int count = 0;
986+
try (DirectoryStream<Path> paths = Files.newDirectoryStream(pluginsDir)) {
987+
for (Path p : paths) {
988+
if (p.getFileName().endsWith(".jar")) {
989+
count++;
990+
}
991+
}
992+
} catch (IOException e) {
993+
// silently ignored
994+
}
995+
return count;
996+
}
997+
970998
public long getMetadataDiscoveryTimeoutMs() {
971999
return metadataTimeoutMs.get().getMillis();
9721000
}

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/ElasticApmTracer.java

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import co.elastic.apm.agent.impl.transaction.AbstractSpan;
3434
import co.elastic.apm.agent.impl.transaction.BinaryHeaderGetter;
3535
import co.elastic.apm.agent.impl.transaction.ElasticContext;
36+
import co.elastic.apm.agent.impl.transaction.ElasticContextWrapper;
3637
import co.elastic.apm.agent.impl.transaction.Span;
3738
import co.elastic.apm.agent.impl.transaction.TextHeaderGetter;
3839
import co.elastic.apm.agent.impl.transaction.TraceContext;
@@ -62,6 +63,8 @@
6263
import java.util.Deque;
6364
import java.util.List;
6465
import java.util.Map;
66+
import java.util.Objects;
67+
import java.util.concurrent.Callable;
6568
import java.util.concurrent.CopyOnWriteArrayList;
6669
import java.util.concurrent.ScheduledThreadPoolExecutor;
6770
import java.util.concurrent.ThreadPoolExecutor;
@@ -98,11 +101,13 @@ protected Deque<ElasticContext<?>> initialValue() {
98101
}
99102
};
100103

104+
101105
private final CoreConfiguration coreConfiguration;
102106
private final SpanConfiguration spanConfiguration;
103107
private final List<ActivationListener> activationListeners;
104108
private final MetricRegistry metricRegistry;
105109
private final ScheduledThreadPoolExecutor sharedPool;
110+
private final int approximateContextSize;
106111
private Sampler sampler;
107112
boolean assertionsEnabled = false;
108113

@@ -160,6 +165,11 @@ public void onChange(ConfigurationOption<?> configurationOption, Double oldValue
160165
this.activationListeners = DependencyInjectingServiceLoader.load(ActivationListener.class, this);
161166
sharedPool = ExecutorUtils.createSingleThreadSchedulingDaemonPool("shared");
162167

168+
// The estimated number of wrappers is linear to the number of the number of external/OTel plugins
169+
// - for an internal agent context, there will be at most one wrapper per external/OTel plugin.
170+
// - for a context created by an external/OTel, we have one less wrapper required
171+
approximateContextSize = coreConfiguration.getExternalPluginsCount() + 1; // +1 extra is for the OTel API plugin
172+
163173
// sets the assertionsEnabled flag to true if indeed enabled
164174
//noinspection AssertWithSideEffects
165175
assert assertionsEnabled = true;
@@ -515,15 +525,10 @@ public ObjectPoolFactory getObjectPoolFactory() {
515525
@Override
516526
@Nullable
517527
public AbstractSpan<?> getActive() {
518-
ElasticContext<?> active = activeStack.get().peek();
528+
ElasticContext<?> active = currentContext();
519529
return active != null ? active.getSpan() : null;
520530
}
521531

522-
@Nullable
523-
public ElasticContext<?> getActiveContext() {
524-
return activeStack.get().peek();
525-
}
526-
527532
@Nullable
528533
@Override
529534
public Span getActiveSpan() {
@@ -725,38 +730,70 @@ public <T> T getLifecycleListener(Class<T> listenerClass) {
725730
return null;
726731
}
727732

733+
/**
734+
* @return the currently active context, {@literal null} if there is none.
735+
*/
728736
@Nullable
729737
public ElasticContext<?> currentContext() {
730-
return activeStack.get().peek();
738+
ElasticContext<?> current = activeStack.get().peek();
739+
740+
// When the active context is wrapped, the wrapper should be transparent to the caller, thus we always return
741+
// the underlying wrapped context.
742+
if (current instanceof ElasticContextWrapper) {
743+
return ((ElasticContextWrapper<?>) current).getWrappedContext();
744+
}
745+
return current;
746+
}
747+
748+
/**
749+
* Lazily wraps the currently active context if required, wrapper instance is cached with wrapperClass as key.
750+
* Wrapping is transparently handled by {@link #currentContext()}.
751+
*
752+
* @param wrapperClass wrapper type
753+
* @param wrapFunction wrapper creation function
754+
* @param <T> wrapper type
755+
* @return newly (or previously) created wrapper
756+
*/
757+
public <T extends ElasticContext<T>> T wrapActiveContextIfRequired(Class<T> wrapperClass, Callable<T> wrapFunction) {
758+
759+
// the current context might be either a "regular" one or a "wrapped" one if it has already been wrapped
760+
ElasticContext<?> current = activeStack.get().peek();
761+
762+
Objects.requireNonNull(current, "active context required for wrapping");
763+
ElasticContextWrapper<?> wrapper;
764+
if (current instanceof ElasticContextWrapper) {
765+
wrapper = (ElasticContextWrapper<?>) current;
766+
} else {
767+
wrapper = new ElasticContextWrapper<>(approximateContextSize, current);
768+
}
769+
T wrapped = wrapper.wrapIfRequired(wrapperClass, wrapFunction);
770+
771+
// replace the currently active on the stack, however currentContext() will make sure to return the original
772+
// context in order to keep wrapping transparent.
773+
774+
activeStack.get().remove();
775+
activeStack.get().push(wrapper);
776+
777+
return wrapped;
731778
}
732779

733780
public void activate(ElasticContext<?> context) {
734781
if (logger.isDebugEnabled()) {
735782
logger.debug("Activating {} on thread {}", context, Thread.currentThread().getId());
736783
}
737784

738-
ElasticContext<?> currentContext = currentContext();
739-
ElasticContext<?> newContext = context;
740-
741785
AbstractSpan<?> span = context.getSpan();
742786
if (span != null) {
743787
span.incrementReferences();
744788
triggerActivationListeners(span, true);
745-
} else if(currentContext != null) {
746-
// when there is no span attached to the context we are attaching to but there is one in the current
747-
// context, we just propagate to the context that will be activated.
748-
span = currentContext.getSpan();
749-
if (span != null) {
750-
newContext = context.withActiveSpan(span);
751-
}
752789
}
753790

754-
activeStack.get().push(newContext);
791+
activeStack.get().push(context);
755792
}
756793

757794
public Scope activateInScope(final ElasticContext<?> context) {
758795
// already in scope
759-
if (getActiveContext() == context) {
796+
if (currentContext() == context) {
760797
return Scope.NoopScope.INSTANCE;
761798
}
762799
context.activate();
@@ -777,12 +814,14 @@ public void deactivate(ElasticContext<?> context) {
777814
if (logger.isDebugEnabled()) {
778815
logger.debug("Deactivating {} on thread {}", context, Thread.currentThread().getId());
779816
}
780-
ElasticContext<?> activeContext = activeStack.get().poll();
817+
ElasticContext<?> activeContext = currentContext();
818+
activeStack.get().remove();
819+
781820
AbstractSpan<?> span = context.getSpan();
782821

783-
if (activeContext != context && context == span) {
784-
// when context has been upgraded, we need to deactivate the original span
785-
activeContext = context;
822+
if (activeContext != context && activeContext instanceof ElasticContextWrapper) {
823+
// when context has been wrapped, we need to get the underlying context
824+
activeContext = ((ElasticContextWrapper<?>) activeContext).getWrappedContext();
786825
}
787826

788827
try {

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,9 +421,10 @@ public <H, C> boolean addSpanLink(
421421

422422
/**
423423
* Adds a span link based on the tracecontext header retrieved from the provided parent.
424+
*
424425
* @param childContextCreator the proper tracecontext inference implementation, which retrieves the header
425-
* @param parent the object from which the tracecontext header is to be retrieved
426-
* @param <T> the parent type - AbstractSpan, TraceContext or Tracer
426+
* @param parent the object from which the tracecontext header is to be retrieved
427+
* @param <T> the parent type - AbstractSpan, TraceContext or Tracer
427428
* @return {@code true} if added, {@code false} otherwise
428429
*/
429430
public <T> boolean addSpanLink(TraceContext.ChildContextCreator<T> childContextCreator, T parent) {

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/ElasticContext.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,5 @@ public interface ElasticContext<T extends ElasticContext<T>> {
6565
*/
6666
@Nullable
6767
Transaction getTransaction();
68+
6869
}

0 commit comments

Comments
 (0)