Skip to content
Draft
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
21 changes: 21 additions & 0 deletions dd-java-agent/instrumentation/guidewire-10.0/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Guidewire is proprietary and not published to any repository, so this module has no
// compile dependency on it: the target classes are matched purely by name at runtime.
muzzle {
pass {
coreJdk()
}
}

apply from: "${rootDir}/gradle/java.gradle"

tasks.named("compileJava") {
configureCompiler(it, 8)
}

dependencies {
// Not required (the module self-activates in $Activate); kept so the test also exercises the
// default config where RunnableInstrumentation wraps run() too — the double activation is safe.
testImplementation project(':dd-java-agent:instrumentation:java:java-concurrent:java-concurrent-1.8')
// @Trace on fixture methods, to materialize the child span whose parent we assert.
testImplementation project(':dd-java-agent:instrumentation:datadog:tracing:trace-annotation')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package datadog.trace.instrumentation.guidewire;

import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.extendsClass;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.nameStartsWith;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.capture;
import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.endTaskScope;
import static datadog.trace.bootstrap.instrumentation.java.concurrent.AdviceUtils.startTaskScope;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.isConstructor;
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;

import com.google.auto.service.AutoService;
import datadog.context.ContextScope;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.agent.tooling.InstrumenterModule;
import datadog.trace.bootstrap.InstrumentationContext;
import datadog.trace.bootstrap.instrumentation.java.concurrent.State;
import java.util.Map;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

/**
* Propagates trace context across the raw thread Guidewire's WSI layer spawns per outbound SOAP
* call ({@code AsyncResponseImpl$WebserviceInvocationThread}, seen at runtime as {@code
* "WSI-Invocation"}).
*
* <p>{@code java.lang.Thread} can't be instrumented (agent global-ignore + bootstrap), so we match
* the application-loaded worker subclass instead: capture the context in its {@code <init>} (still
* on the parent thread) and re-activate it in {@code run()}. Both halves live here so it works even
* if the runnable/executor integration is off; if that also wraps {@code run()}, the double
* activation is safe because the continuation is consumed once.
*/
@AutoService(InstrumenterModule.class)
public final class WsiAsyncResponseInstrumentation extends InstrumenterModule.ContextTracking
implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {

private static final String ASYNC_RESPONSE = "gw.internal.xml.ws.AsyncResponseImpl";

public WsiAsyncResponseInstrumentation() {
super("guidewire");
}

@Override
public String hierarchyMarkerType() {
return ASYNC_RESPONSE;
}

@Override
public ElementMatcher<TypeDescription> hierarchyMatcher() {
// '$' matches only nested classes of AsyncResponseImpl, not top-level siblings.
return nameStartsWith(ASYNC_RESPONSE + "$").and(extendsClass(named("java.lang.Thread")));
}

@Override
public Map<String, String> contextStore() {
return singletonMap(Runnable.class.getName(), State.class.getName());
}

@Override
public void methodAdvice(MethodTransformer transformer) {
transformer.applyAdvice(isConstructor(), getClass().getName() + "$Capture");
transformer.applyAdvice(
named("run").and(takesArguments(0)).and(isPublic()), getClass().getName() + "$Activate");
}

public static final class Capture {
@Advice.OnMethodExit(suppress = Throwable.class)
public static void onConstruct(@Advice.This final Runnable thiz) {
capture(InstrumentationContext.get(Runnable.class, State.class), thiz);
}
}

public static final class Activate {
@Advice.OnMethodEnter(suppress = Throwable.class)
public static ContextScope enter(@Advice.This final Runnable thiz) {
return startTaskScope(InstrumentationContext.get(Runnable.class, State.class), thiz);
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void exit(@Advice.Enter final ContextScope scope) {
endTaskScope(scope);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package datadog.trace.instrumentation.guidewire;

import static datadog.trace.agent.test.assertions.SpanMatcher.span;
import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME;
import static datadog.trace.agent.test.assertions.TraceMatcher.trace;
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan;
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan;

import datadog.context.ContextScope;
import datadog.trace.agent.test.AbstractInstrumentationTest;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import gw.internal.xml.ws.AsyncResponseImpl;
import gw.internal.xml.ws.UnrelatedWorker;
import org.junit.jupiter.api.Test;

class WsiAsyncResponseInstrumentationTest extends AbstractInstrumentationTest {

@FunctionalInterface
interface Body {
void run() throws Exception;
}

private static void runUnderTrace(String operationName, Body body) throws Exception {
AgentSpan span = startSpan("guidewire-test", operationName);
try (ContextScope scope = activateSpan(span)) {
body.run();
} finally {
span.finish();
}
}

@Test
void namedWorkerPropagatesContext() throws Exception {
// Constructed and run while the caller's span is active; invoke() blocks until the worker ends.
runUnderTrace("parent", () -> new AsyncResponseImpl().invoke());

assertTraces(
trace(
SORT_BY_START_TIME,
span().root().operationName("parent"),
span().childOfPrevious().operationName("soap.call")));
}

@Test
void anonymousWorkerPropagatesContext() throws Exception {
runUnderTrace("parent", () -> AsyncResponseImpl.anonymous().invoke());

assertTraces(
trace(
SORT_BY_START_TIME,
span().root().operationName("parent"),
span().childOfPrevious().operationName("soap.call")));
}

@Test
void synchronousRunPropagatesContext() throws Exception {
// callTimeout <= 0 path: AsyncResponseImpl.run() calls _thread.run() on the caller thread.
runUnderTrace("parent", () -> new AsyncResponseImpl().invokeSync());

assertTraces(
trace(
SORT_BY_START_TIME,
span().root().operationName("parent"),
span().childOfPrevious().operationName("soap.call")));
}

@Test
void unrelatedThreadIsNotInstrumented() throws Exception {
// Same construction pattern, but a class the narrow matcher must ignore.
runUnderTrace(
"parent",
() -> {
UnrelatedWorker worker = new UnrelatedWorker();
worker.start();
worker.join();
});

// No propagation: the worker's span starts its own trace instead of joining "parent".
assertTraces(
trace(span().root().operationName("parent")),
trace(span().root().operationName("unrelated.work")));
}

@Test
void noContextLeakToSubsequentInvocation() throws Exception {
runUnderTrace("parent", () -> new AsyncResponseImpl().invoke());
// Second invocation runs with no active span: capture is a no-op, so soap.call is its own root.
new AsyncResponseImpl().invoke();

assertTraces(
trace(
SORT_BY_START_TIME,
span().root().operationName("parent"),
span().childOfPrevious().operationName("soap.call")),
trace(span().root().operationName("soap.call")));
}

@Test
void workerConstructedButNeverRunDoesNotCorruptLaterTraces() throws Exception {
// Constructed under an active span but never run: capture happens but is never activated.
// Guards that the stranded continuation does not mis-attribute a later, unrelated trace.
runUnderTrace("outer", () -> new AsyncResponseImpl());
runUnderTrace("independent", () -> {});

// 'independent' is a clean, standalone root regardless of the stranded continuation.
assertTraces(trace(span().root().operationName("independent")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package gw.internal.xml.ws;

import datadog.trace.api.Trace;

/**
* Test double for Guidewire's WSI worker; kept in package {@code gw.internal.xml.ws} so the matcher
* applies.
*/
public class AsyncResponseImpl {

private final Thread thread;

public AsyncResponseImpl() {
this.thread = new WebserviceInvocationThread();
}

// The boolean only distinguishes this overload from the no-arg constructor; its value is unused.
private AsyncResponseImpl(boolean anonymous) {
this.thread =
new Thread() {
@Override
public void run() {
soapCall();
}
};
}

public static AsyncResponseImpl anonymous() {
return new AsyncResponseImpl(true);
}

public void invoke() throws InterruptedException {
thread.start();
thread.join();
}

public void invokeSync() {
thread.run();
}

@Trace(operationName = "soap.call")
static void soapCall() {}

// Chained constructor: two <init> frames make capture fire twice, testing the State CAS dedup.
public static final class WebserviceInvocationThread extends Thread {
public WebserviceInvocationThread() {
this("WSI-Invocation");
}

private WebserviceInvocationThread(String name) {
super(name);
}

@Override
public void run() {
soapCall();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package gw.internal.xml.ws;

import datadog.trace.api.Trace;

/**
* Negative control: a Thread subclass the matcher must ignore (not an {@code AsyncResponseImpl$…}).
*/
public class UnrelatedWorker extends Thread {

@Override
public void run() {
unrelatedWork();
}

@Trace(operationName = "unrelated.work")
static void unrelatedWork() {}
}
1 change: 1 addition & 0 deletions metadata/agent-jar-checks.properties
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ expected.integrations = IastInstrumentation,\
grpc,\
gson,\
guava,\
guidewire,\
hazelcast,\
hazelcast_legacy,\
hibernate,\
Expand Down
8 changes: 8 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -6281,6 +6281,14 @@
"aliases": ["DD_TRACE_INTEGRATION_GUAVA_ENABLED", "DD_INTEGRATION_GUAVA_ENABLED"]
}
],
"DD_TRACE_GUIDEWIRE_ENABLED": [
{
"version": "A",
"type": "boolean",
"default": "true",
"aliases": ["DD_TRACE_INTEGRATION_GUIDEWIRE_ENABLED", "DD_INTEGRATION_GUIDEWIRE_ENABLED"]
}
],
"DD_TRACE_HAZELCAST_ENABLED": [
{
"version": "A",
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ include(
":dd-java-agent:instrumentation:grpc-1.5",
":dd-java-agent:instrumentation:gson-1.6",
":dd-java-agent:instrumentation:guava-10.0",
":dd-java-agent:instrumentation:guidewire-10.0",
":dd-java-agent:instrumentation:hazelcast:hazelcast-3.6",
":dd-java-agent:instrumentation:hazelcast:hazelcast-3.9",
":dd-java-agent:instrumentation:hazelcast:hazelcast-4.0",
Expand Down