Skip to content

Refactor out camel ContextWithScope into a bootstrap package so it can be referenced by multiple ClassLoaders. #14294

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .fossa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ targets:
- type: gradle
path: ./
target: ':instrumentation:c3p0-0.9:library'
- type: gradle
path: ./
target: ':instrumentation:camel-2.20:bootstrap'
- type: gradle
path: ./
target: ':instrumentation:camel-2.20:javaagent'
Expand Down
3 changes: 3 additions & 0 deletions instrumentation/camel-2.20/bootstrap/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
plugins {
id("otel.javaagent-bootstrap")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.bootstrap.apachecamel;

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import javax.annotation.Nullable;

public class ContextWithScope {
@Nullable private final ContextWithScope parent;
@Nullable private final Context context;
@Nullable private final Scope scope;

public ContextWithScope(ContextWithScope parent, Context context, Scope scope) {
this.parent = parent;
this.context = context;
this.scope = scope;
}

public static ContextWithScope activate(ContextWithScope parent, Context context) {
Scope scope = context != null ? context.makeCurrent() : null;
return new ContextWithScope(parent, context, scope);
}

public Context getContext() {
return context;
}

public ContextWithScope getParent() {
return parent;
}

public void deactivate() {
if (scope == null) {
return;
}
scope.close();
}

@Override
public String toString() {
return "ContextWithScope [context=" + context + ", scope=" + scope + "]";
}
}
1 change: 1 addition & 0 deletions instrumentation/camel-2.20/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {

compileOnly("com.google.auto.value:auto-value-annotations")
annotationProcessor("com.google.auto.value:auto-value")
bootstrap(project(":instrumentation:camel-2.20:bootstrap"))

testInstrumentation(project(":instrumentation:apache-httpclient:apache-httpclient-2.0:javaagent"))
testInstrumentation(project(":instrumentation:servlet:servlet-3.0:javaagent"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@

package io.opentelemetry.javaagent.instrumentation.apachecamel;

import static io.opentelemetry.javaagent.instrumentation.apachecamel.CamelSingletons.instrumenter;
import static java.util.logging.Level.FINE;

import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
import io.opentelemetry.javaagent.bootstrap.apachecamel.ContextWithScope;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import org.apache.camel.Exchange;

/** Utility class for managing active contexts as a stack associated with an exchange. */
Expand All @@ -45,13 +43,12 @@ private ActiveContextManager() {}
* This method activates the supplied context for the supplied exchange. If an existing context is
* found for the exchange it will be pushed onto a stack.
*
* @param context The exchange
* @param request The context
* @param context The context
* @param exchange The exchange
*/
public static void activate(Context context, CamelRequest request) {
Exchange exchange = request.getExchange();
public static void activate(Context context, Exchange exchange) {
ContextWithScope parent = exchange.getProperty(ACTIVE_CONTEXT_PROPERTY, ContextWithScope.class);
ContextWithScope contextWithScope = ContextWithScope.activate(parent, context, request);
ContextWithScope contextWithScope = ContextWithScope.activate(parent, context);
exchange.setProperty(ACTIVE_CONTEXT_PROPERTY, contextWithScope);
logger.log(FINE, "Activated a span: {0}", contextWithScope);
}
Expand All @@ -68,50 +65,12 @@ public static Context deactivate(Exchange exchange) {
exchange.getProperty(ACTIVE_CONTEXT_PROPERTY, ContextWithScope.class);

if (contextWithScope != null) {
contextWithScope.deactivate(exchange.getException());
contextWithScope.deactivate();
exchange.setProperty(ACTIVE_CONTEXT_PROPERTY, contextWithScope.getParent());
logger.log(FINE, "Deactivated span: {0}", contextWithScope);
return contextWithScope.context;
return contextWithScope.getContext();
}

return null;
}

private static class ContextWithScope {
@Nullable private final ContextWithScope parent;
@Nullable private final Context context;
private final CamelRequest request;
@Nullable private final Scope scope;

public ContextWithScope(
ContextWithScope parent, Context context, CamelRequest request, Scope scope) {
this.parent = parent;
this.context = context;
this.request = request;
this.scope = scope;
}

public static ContextWithScope activate(
ContextWithScope parent, Context context, CamelRequest request) {
Scope scope = context != null ? context.makeCurrent() : null;
return new ContextWithScope(parent, context, request, scope);
}

public ContextWithScope getParent() {
return parent;
}

public void deactivate(Exception exception) {
if (scope == null) {
return;
}
scope.close();
instrumenter().end(context, request, null, exception);
}

@Override
public String toString() {
return "ContextWithScope [context=" + context + ", scope=" + scope + "]";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.opentelemetry.context.Context;
import java.util.EventObject;
import java.util.logging.Logger;
import org.apache.camel.Exchange;
import org.apache.camel.management.event.ExchangeSendingEvent;
import org.apache.camel.management.event.ExchangeSentEvent;
import org.apache.camel.support.EventNotifierSupport;
Expand Down Expand Up @@ -63,7 +64,7 @@ private static void onExchangeSending(ExchangeSendingEvent ese) {
sd.getInitiatorSpanKind());
Context context = startOnExchangeSending(request);

ActiveContextManager.activate(context, request);
ActiveContextManager.activate(context, request.getExchange());
CamelPropagationUtil.injectParent(context, ese.getExchange().getIn().getHeaders());

logger.log(FINE, "[Exchange sending] Initiator span started: {0}", context);
Expand All @@ -84,7 +85,12 @@ private static void onExchangeSent(ExchangeSentEvent event) {
return;
}

Context context = ActiveContextManager.deactivate(event.getExchange());
Exchange exchange = event.getExchange();
Context context = ActiveContextManager.deactivate(exchange);
CamelRequest request =
CamelRequest.create(
sd, exchange, event.getEndpoint(), CamelDirection.OUTBOUND, sd.getInitiatorSpanKind());
instrumenter().end(context, request, null, exchange.getException());
Comment on lines +90 to +93
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imo this isn't equivalent to the previous code. Previously instrumenter().end() was skipped when scope field in ContextWithScope was null, now it is always run. You could probably get this done with less changes by keeping the request field in ContextWithScope, but changing the type to Object. Then you would only need to modify ActiveContextManager.deactivate method.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if I keep Request field and make it an Object, I would still need to take interfaces like Exchange to the bootstrap package (and thus have a dependency on camel in the bootstrap package) right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you don't. It is similar to storing anything in ArrayList, which is in boot loader. Data is kept in Object[] and contain any kind of object, including the ones whose class is not in boot loader. When you read back the object you cast it to the desired type.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you mean something like this?

/*
 * Copyright The OpenTelemetry Authors
 * SPDX-License-Identifier: Apache-2.0
 */

package io.opentelemetry.javaagent.instrumentation.apachecamel;

import com.google.auto.value.AutoValue;
import io.opentelemetry.api.trace.SpanKind;

@AutoValue
abstract class CamelRequest {

  public static CamelRequest create(
      Object spanDecorator,
      Object exchange,
      Object endpoint,
      CamelDirection camelDirection,
      SpanKind spanKind) {
    return new AutoValue_CamelRequest(spanDecorator, exchange, endpoint, camelDirection, spanKind);
  }

  public abstract Object getSpanDecorator();

  public abstract Object getExchange();

  public abstract Object getEndpoint();

  public abstract CamelDirection getCamelDirection();

  public abstract SpanKind getSpanKind();
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laurit Apologies about the delay.. been rethinking about how to solve this..
Another issue with the double classloader path is that we end up with duplicate spans.. each one started by the instrumentation in the different class loaders.

But the actual Camel Exchange object used by both of these class loaders is (and could only ever be) from one of those ClassLoaders.. So if we suppress the creation of a span if the ClassLoader of the instrumentation code != the ClassLoader of the Exchange object, we solve two birds with one stone.

That way we don't need to go through the trouble of having to load certain things in the bootstrap ClassLoader and we don't end up with duplicate spans. But that is slightly more cumbersome that I had hoped it would be? The InstrumenterBuilder is used, but there seems to be no good way to override the shouldStart method?

Is that a possible avenue to explore?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a possible avenue to explore?

definitely. I don't know what exactly is causing the issue for you, but lets assume that the problem is that the instrumentation in CamelContextInstrumentation instruments all implementations of CamelContext and you have one implementation that extends another and that the the start method is called for both. That could cause CamelTracingService to be added twice. You could resolve this by excluding one of the classes in the typeMatcher method or use CallDepth like in

to suppress the instrumentation in the nested call. Other possibility could be using classLoaderMatcher in InstrumentationModule if you can recognize the class loader where you don't wish to run the instrumentation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh.. that looks very promising indeed. I will have a play with those in the next few days to see what I can come up with.. cheers!

logger.log(FINE, "[Exchange sent] Initiator span finished: {0}", context);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private static Context spanOnExchangeBegin(
return null;
}
Context context = instrumenter().start(parentContext, request);
ActiveContextManager.activate(context, request);
ActiveContextManager.activate(context, exchange);
return context;
}

Expand All @@ -80,7 +80,12 @@ public void onExchangeBegin(Route route, Exchange exchange) {
/** Route exchange done. Get active CAMEL span, finish, remove from CAMEL holder. */
@Override
public void onExchangeDone(Route route, Exchange exchange) {
SpanDecorator sd = getSpanDecorator(route.getEndpoint());
Context context = ActiveContextManager.deactivate(exchange);
CamelRequest request =
CamelRequest.create(
sd, exchange, route.getEndpoint(), CamelDirection.INBOUND, SpanKind.INTERNAL);
instrumenter().end(context, request, null, exchange.getException());
logger.log(FINE, "[Route finished] Receiver span finished {0}", context);
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ include(":instrumentation:azure-core:azure-core-1.36:library-instrumentation-sha
include(":instrumentation:c3p0-0.9:javaagent")
include(":instrumentation:c3p0-0.9:library")
include(":instrumentation:c3p0-0.9:testing")
include(":instrumentation:camel-2.20:bootstrap")
include(":instrumentation:camel-2.20:javaagent")
include(":instrumentation:camel-2.20:javaagent-unit-tests")
include(":instrumentation:cassandra:cassandra-3.0:javaagent")
Expand Down
Loading