diff --git a/dd-java-agent/instrumentation/cics/build.gradle b/dd-java-agent/instrumentation/cics/build.gradle
new file mode 100644
index 00000000000..8fe999ac9f0
--- /dev/null
+++ b/dd-java-agent/instrumentation/cics/build.gradle
@@ -0,0 +1,148 @@
+apply from: "$rootDir/gradle/java.gradle"
+
+// Configuration for downloading CICS SDK from IBM
+ext {
+ cicsVersion = '9.1'
+ cicsSdkName = 'CICS_TG_SDK_91_Unix'
+}
+
+repositories {
+ ivy {
+ url = 'https://public.dhe.ibm.com/software/htp/cics/support/supportpacs/individual/'
+ patternLayout {
+ artifact '[module].[ext]'
+ }
+ metadataSources {
+ it.artifact()
+ }
+ }
+}
+
+configurations {
+ register('cicsSdk') {
+ canBeResolved = true
+ canBeConsumed = false
+ }
+ register('cicsJars') {
+ canBeResolved = true
+ canBeConsumed = false
+ }
+}
+
+// Task to extract the CICS SDK and get the required JARs
+abstract class ExtractCicsJars extends DefaultTask {
+ @InputFiles
+ final ConfigurableFileCollection sdkArchive = project.objects.fileCollection()
+
+ @OutputDirectory
+ final DirectoryProperty outputDir = project.objects.directoryProperty()
+
+ ExtractCicsJars() {
+ outputDir.convention(project.layout.buildDirectory.dir('cics-jars'))
+ }
+
+ @TaskAction
+ def extract() {
+ def sdkFile = sdkArchive.singleFile
+ def buildDir = outputDir.get().asFile
+ buildDir.mkdirs()
+
+ // Extract outer tar.gz to get the inner tar.gz
+ def tempDir = new File(buildDir, 'temp')
+ tempDir.mkdirs()
+
+ project.copy {
+ from project.tarTree(sdkFile)
+ into tempDir
+ }
+
+ // Find and extract the multiplatforms SDK
+ def multiplatformsSdk = new File(tempDir, 'CICS_TG_SDK_91_Multiplatforms.tar.gz')
+ if (!multiplatformsSdk.exists()) {
+ throw new GradleException("Could not find CICS_TG_SDK_91_Multiplatforms.tar.gz in extracted archive")
+ }
+
+ def sdkDir = new File(tempDir, 'sdk')
+ sdkDir.mkdirs()
+
+ project.copy {
+ from project.tarTree(multiplatformsSdk)
+ into sdkDir
+ }
+
+ // Extract cicseci.rar to get cicseci.jar, ctgclient.jar, and ctgserver.jar
+ def cicsEciRar = new File(sdkDir, 'cicstgsdk/api/jee/runtime/managed/cicseci.rar')
+ if (!cicsEciRar.exists()) {
+ throw new GradleException("Could not find cicseci.rar at expected location")
+ }
+
+ project.copy {
+ from project.zipTree(cicsEciRar)
+ into buildDir
+ include 'cicseci.jar'
+ include 'ctgclient.jar'
+ include 'ctgserver.jar'
+ }
+
+ // Copy cicsjee.jar
+ def cicsJeeJar = new File(sdkDir, 'cicstgsdk/api/jee/runtime/nonmanaged/cicsjee.jar')
+ if (!cicsJeeJar.exists()) {
+ throw new GradleException("Could not find cicsjee.jar at expected location")
+ }
+
+ project.copy {
+ from cicsJeeJar
+ into buildDir
+ }
+
+ // Clean up temp directory
+ tempDir.deleteDir()
+
+ logger.lifecycle("Extracted CICS JARs to: ${buildDir.absolutePath}")
+ }
+}
+
+tasks.register('extractCicsJars', ExtractCicsJars) {
+ sdkArchive.from(configurations.named('cicsSdk'))
+
+ // Only extract if the output directory doesn't exist or SDK configuration changed
+ outputs.upToDateWhen {
+ def outputDir = it.outputDir.get().asFile
+ outputDir.exists() &&
+ new File(outputDir, 'cicseci.jar').exists() &&
+ new File(outputDir, 'ctgclient.jar').exists() &&
+ new File(outputDir, 'ctgserver.jar').exists() &&
+ new File(outputDir, 'cicsjee.jar').exists()
+ }
+}
+
+dependencies {
+ // Download the CICS SDK from IBM
+ cicsSdk "${cicsSdkName}:${cicsSdkName}:@tar.gz"
+
+ // Compile-time dependencies (eliminates reflection)
+ compileOnly group: 'javax.resource', name: 'javax.resource-api', version: '1.7.1'
+ compileOnly files(tasks.named('extractCicsJars').map { task ->
+ project.fileTree(task.outputDir) {
+ include 'cicseci.jar'
+ }
+ })
+
+ // Test dependencies
+ testImplementation group: 'javax.resource', name: 'javax.resource-api', version: '1.7.1'
+ testImplementation libs.bundles.mockito
+ testImplementation files(tasks.named('extractCicsJars').map { task ->
+ project.fileTree(task.outputDir) {
+ include '*.jar'
+ }
+ })
+}
+
+// Ensure extraction happens before compilation
+tasks.named('compileJava') {
+ dependsOn 'extractCicsJars'
+}
+
+tasks.named('compileTestGroovy') {
+ dependsOn 'extractCicsJars'
+}
diff --git a/dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/CicsDecorator.java b/dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/CicsDecorator.java
new file mode 100644
index 00000000000..f21feed5b68
--- /dev/null
+++ b/dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/CicsDecorator.java
@@ -0,0 +1,128 @@
+package datadog.trace.instrumentation.cics;
+
+import com.ibm.connector2.cics.ECIInteractionSpec;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
+import datadog.trace.bootstrap.instrumentation.api.Tags;
+import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
+import datadog.trace.bootstrap.instrumentation.decorator.ClientDecorator;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+
+public class CicsDecorator extends ClientDecorator {
+ public static final CharSequence CICS_CLIENT = UTF8BytesString.create("cics-client");
+ public static final CharSequence ECI_EXECUTE_OPERATION = UTF8BytesString.create("cics.execute");
+ public static final CharSequence GATEWAY_FLOW_OPERATION = UTF8BytesString.create("gateway.flow");
+
+ public static final CicsDecorator DECORATE = new CicsDecorator();
+
+ @Override
+ protected String[] instrumentationNames() {
+ return new String[] {"cics"};
+ }
+
+ @Override
+ protected String service() {
+ return null; // Use default service name
+ }
+
+ @Override
+ protected CharSequence component() {
+ return CICS_CLIENT;
+ }
+
+ @Override
+ protected CharSequence spanType() {
+ return InternalSpanTypes.RPC;
+ }
+
+ @Override
+ public AgentSpan afterStart(AgentSpan span) {
+ assert span != null;
+ span.setTag("rpc.system", "cics");
+ return super.afterStart(span);
+ }
+
+ /**
+ * Adds connection details to a span from JavaGatewayInterface fields.
+ *
+ * @param span the span to decorate
+ * @param strAddress the hostname/address string
+ * @param port the port number
+ * @param ipGateway the resolved InetAddress (can be null)
+ */
+ public AgentSpan onConnection(
+ final AgentSpan span, final String strAddress, final int port, final InetAddress ipGateway) {
+ if (strAddress != null) {
+ span.setTag(Tags.PEER_HOSTNAME, strAddress);
+ }
+
+ if (ipGateway != null) {
+ onPeerConnection(span, ipGateway, false);
+ }
+
+ if (port > 0) {
+ setPeerPort(span, port);
+ }
+
+ return span;
+ }
+
+ /**
+ * Adds local connection details to a span from a socket address.
+ *
+ * @param span the span to decorate
+ * @param localAddr the socket (can be null)
+ */
+ public AgentSpan onLocalConnection(final AgentSpan span, final InetSocketAddress localAddr) {
+ if (localAddr != null && localAddr.getAddress() != null) {
+ span.setTag("network.local.address", localAddr.getAddress().getHostAddress());
+ span.setTag("network.local.port", localAddr.getPort());
+ }
+ return span;
+ }
+
+ /**
+ * Converts ECI interaction verb code to string representation.
+ *
+ * @param verb the interaction verb code
+ * @return string representation of the verb
+ * @see InteractionSpec
+ * constants
+ */
+ private String getInteractionVerbString(final int verb) {
+ switch (verb) {
+ case 0:
+ return "SYNC_SEND";
+ case 1:
+ return "SYNC_SEND_RECEIVE";
+ case 2:
+ return "SYNC_RECEIVE";
+ default:
+ return "UNKNOWN_" + verb;
+ }
+ }
+
+ public AgentSpan onECIInteraction(final AgentSpan span, final ECIInteractionSpec spec) {
+ final String interactionVerb = getInteractionVerbString(spec.getInteractionVerb());
+ final String functionName = spec.getFunctionName();
+ final String tranName = spec.getTranName();
+ final String tpnName = spec.getTPNName();
+
+ span.setResourceName(interactionVerb + " " + functionName);
+ span.setTag("cics.interaction", interactionVerb);
+
+ if (functionName != null) {
+ span.setTag("rpc.method", functionName);
+ }
+ if (tranName != null) {
+ span.setTag("cics.tran", tranName);
+ }
+ if (tpnName != null) {
+ span.setTag("cics.tpn", tpnName);
+ }
+
+ return span;
+ }
+}
diff --git a/dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/ECIInteractionInstrumentation.java b/dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/ECIInteractionInstrumentation.java
new file mode 100644
index 00000000000..3c71ca8009e
--- /dev/null
+++ b/dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/ECIInteractionInstrumentation.java
@@ -0,0 +1,72 @@
+package datadog.trace.instrumentation.cics;
+
+import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan;
+import static datadog.trace.instrumentation.cics.CicsDecorator.DECORATE;
+import static datadog.trace.instrumentation.cics.CicsDecorator.ECI_EXECUTE_OPERATION;
+
+import com.google.auto.service.AutoService;
+import com.ibm.connector2.cics.ECIInteraction;
+import com.ibm.connector2.cics.ECIInteractionSpec;
+import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.agent.tooling.InstrumenterModule;
+import datadog.trace.bootstrap.CallDepthThreadLocalMap;
+import datadog.trace.bootstrap.instrumentation.api.AgentScope;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import net.bytebuddy.asm.Advice;
+
+@AutoService(InstrumenterModule.class)
+public final class ECIInteractionInstrumentation extends InstrumenterModule.Tracing
+ implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice {
+
+ public ECIInteractionInstrumentation() {
+ super("cics");
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {packageName + ".CicsDecorator"};
+ }
+
+ @Override
+ public String instrumentedType() {
+ return "com.ibm.connector2.cics.ECIInteraction";
+ }
+
+ @Override
+ public void methodAdvice(MethodTransformer transformer) {
+ transformer.applyAdvice(named("execute"), getClass().getName() + "$ExecuteAdvice");
+ }
+
+ public static class ExecuteAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static AgentScope enter(@Advice.Argument(0) final Object spec) {
+ // Coordinating with JavaGatewayInterfaceInstrumentation
+ CallDepthThreadLocalMap.incrementCallDepth(ECIInteraction.class);
+
+ if (!(spec instanceof ECIInteractionSpec)) {
+ return null;
+ }
+
+ AgentSpan span = startSpan(ECI_EXECUTE_OPERATION);
+ DECORATE.afterStart(span);
+ DECORATE.onECIInteraction(span, (ECIInteractionSpec) spec);
+
+ return activateSpan(span);
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void exit(
+ @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) {
+ CallDepthThreadLocalMap.decrementCallDepth(ECIInteraction.class);
+
+ if (null != scope) {
+ DECORATE.onError(scope.span(), throwable);
+ DECORATE.beforeFinish(scope.span());
+ scope.span().finish();
+ scope.close();
+ }
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/JavaGatewayInterfaceInstrumentation.java b/dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/JavaGatewayInterfaceInstrumentation.java
new file mode 100644
index 00000000000..fb39f2868b6
--- /dev/null
+++ b/dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/JavaGatewayInterfaceInstrumentation.java
@@ -0,0 +1,104 @@
+package datadog.trace.instrumentation.cics;
+
+import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.extendsClass;
+import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan;
+import static datadog.trace.instrumentation.cics.CicsDecorator.DECORATE;
+import static datadog.trace.instrumentation.cics.CicsDecorator.GATEWAY_FLOW_OPERATION;
+import static net.bytebuddy.matcher.ElementMatchers.declaresField;
+
+import com.google.auto.service.AutoService;
+import com.ibm.connector2.cics.ECIInteraction;
+import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.agent.tooling.InstrumenterModule;
+import datadog.trace.bootstrap.CallDepthThreadLocalMap;
+import datadog.trace.bootstrap.instrumentation.api.AgentScope;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+@AutoService(InstrumenterModule.class)
+public final class JavaGatewayInterfaceInstrumentation extends InstrumenterModule.Tracing
+ implements Instrumenter.ForTypeHierarchy, Instrumenter.HasMethodAdvice {
+
+ public JavaGatewayInterfaceInstrumentation() {
+ super("cics");
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {packageName + ".CicsDecorator"};
+ }
+
+ @Override
+ public String hierarchyMarkerType() {
+ return "com.ibm.ctg.client.JavaGatewayInterface";
+ }
+
+ @Override
+ public ElementMatcher hierarchyMatcher() {
+ // Only instrument subclasses that have a socket field (TcpJavaGateway, SslJavaGateway)
+ return extendsClass(named(hierarchyMarkerType())).and(declaresField(named("socJGate")));
+ }
+
+ @Override
+ public void methodAdvice(MethodTransformer transformer) {
+ transformer.applyAdvice(named("flow"), getClass().getName() + "$FlowAdvice");
+ }
+
+ public static class FlowAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static AgentScope enter(
+ @Advice.FieldValue("strAddress") final String strAddress,
+ @Advice.FieldValue("iPort") final int port,
+ @Advice.FieldValue("ipGateway") final InetAddress ipGateway) {
+ // Coordinating with ECIInteractionInstrumentation
+ final int callDepth = CallDepthThreadLocalMap.getCallDepth(ECIInteraction.class);
+ if (callDepth > 0) {
+ // Inside execute() - add connection tags to the existing span instead of creating new one
+ final AgentSpan parentSpan = activeSpan();
+ if (parentSpan != null) {
+ DECORATE.onConnection(parentSpan, strAddress, port, ipGateway);
+ }
+ return null;
+ }
+
+ // Not inside execute() - create a new span
+ final AgentSpan span = startSpan(GATEWAY_FLOW_OPERATION);
+ DECORATE.afterStart(span);
+ DECORATE.onConnection(span, strAddress, port, ipGateway);
+ return activateSpan(span);
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void exit(
+ @Advice.Enter final AgentScope scope,
+ @Advice.Thrown final Throwable throwable,
+ @Advice.FieldValue("socJGate") final Socket socket) {
+ if (null == scope) {
+ return;
+ }
+
+ final AgentSpan span = scope.span();
+
+ if (socket != null) {
+ final SocketAddress socketAddress = socket.getLocalSocketAddress();
+ if (socketAddress instanceof InetSocketAddress) {
+ DECORATE.onLocalConnection(span, (InetSocketAddress) socketAddress);
+ }
+ }
+
+ DECORATE.onError(span, throwable);
+ DECORATE.beforeFinish(span);
+ span.finish();
+ scope.close();
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/cics/src/test/groovy/ECIInteractionInstrumentationTest.groovy b/dd-java-agent/instrumentation/cics/src/test/groovy/ECIInteractionInstrumentationTest.groovy
new file mode 100644
index 00000000000..abc52dab886
--- /dev/null
+++ b/dd-java-agent/instrumentation/cics/src/test/groovy/ECIInteractionInstrumentationTest.groovy
@@ -0,0 +1,147 @@
+import com.ibm.connector2.cics.CICSUserInputException
+import com.ibm.connector2.cics.ECIInteractionSpec
+import com.ibm.connector2.cics.ECIManagedConnectionFactory
+import datadog.trace.agent.test.InstrumentationSpecification
+import datadog.trace.api.DDSpanTypes
+import datadog.trace.bootstrap.instrumentation.api.Tags
+import javax.resource.cci.InteractionSpec
+import javax.resource.spi.ConnectionManager
+
+import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
+import static org.mockito.Mockito.*
+
+/**
+ * Tests for ECIInteractionInstrumentation.
+ *
+ * These tests purposefully don't mock all of the objects passed to execute
+ * because the early failure that throws CICSUserInputException still lets us validate the instrumentation without needing to do
+ * a lot more mocking (and also tying to the test more tightly to the implementation).
+ */
+class ECIInteractionInstrumentationTest extends InstrumentationSpecification {
+ def "ECI execute creates span with minimal fields"() {
+ setup:
+ def spec = new ECIInteractionSpec()
+ spec.setFunctionName("TESTPROG")
+
+ def mcf = new ECIManagedConnectionFactory()
+ def managedConnection = mcf.createManagedConnection(null, null)
+
+ def mockConnectionManager = mock(ConnectionManager)
+ when(mockConnectionManager.allocateConnection(any(), any())).thenReturn(managedConnection.getConnection(null, null))
+
+ def factory = mcf.createConnectionFactory(mockConnectionManager)
+ def connection = factory.getConnection()
+ def interaction = connection.createInteraction()
+
+ when:
+ runUnderTrace("parent") {
+ try {
+ // Method will fail with CICSUserInputException (input record is null)
+ interaction.execute(spec as InteractionSpec, null, null)
+ } catch (CICSUserInputException ignore) {
+ // Expected - we're just testing that the span is created
+ } finally {
+ try {
+ interaction?.close()
+ } catch (Throwable ignored) {}
+ try {
+ connection?.close()
+ } catch (Throwable ignored) {}
+ }
+ }
+
+ then:
+ assertTraces(1) {
+ trace(2) {
+ span(0) {
+ operationName "parent"
+ parent()
+ }
+ span(1) {
+ operationName "cics.execute"
+ spanType DDSpanTypes.RPC
+ resourceName "SYNC_SEND_RECEIVE TESTPROG"
+ childOf(span(0))
+ errored true
+ tags {
+ "$Tags.COMPONENT" "cics-client"
+ "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT
+ "rpc.system" "cics"
+ "rpc.method" "TESTPROG"
+ "cics.interaction" "SYNC_SEND_RECEIVE"
+ "error.type" "com.ibm.connector2.cics.CICSUserInputException"
+ "error.stack" String
+ "error.message" String
+ defaultTags()
+ }
+ }
+ }
+ }
+ }
+
+ def "ECI execute creates span with all fields"() {
+ setup:
+ def spec = new ECIInteractionSpec()
+ spec.setFunctionName("FULLPROG")
+ spec.setTranName("FULL")
+ spec.setTPNName("CPMI")
+ spec.setInteractionVerb(1) // SYNC_SEND_RECEIVE
+
+ def mcf = new ECIManagedConnectionFactory()
+ def managedConnection = mcf.createManagedConnection(null, null)
+
+ def mockConnectionManager = mock(ConnectionManager)
+ when(mockConnectionManager.allocateConnection(any(), any())).thenReturn(managedConnection.getConnection(null, null))
+
+ def factory = mcf.createConnectionFactory(mockConnectionManager)
+ def connection = factory.getConnection()
+ def interaction = connection.createInteraction()
+
+ when:
+ runUnderTrace("parent") {
+ try {
+ // Method will fail with CICSUserInputException (input record is null)
+ interaction.execute(spec as InteractionSpec, null, null)
+ } catch (CICSUserInputException expected) {
+ // Expected - we're just testing that the span is created
+ } finally {
+ try {
+ interaction?.close()
+ } catch (Throwable ignored) {}
+ try {
+ connection?.close()
+ } catch (Throwable ignored) {}
+ }
+ }
+
+ then:
+ assertTraces(1) {
+ trace(2) {
+ span(0) {
+ operationName "parent"
+ parent()
+ }
+ span(1) {
+ operationName "cics.execute"
+ spanType DDSpanTypes.RPC
+ resourceName "SYNC_SEND_RECEIVE FULLPROG"
+ childOf(span(0))
+ errored true
+ tags {
+ "$Tags.COMPONENT" "cics-client"
+ "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT
+ "rpc.system" "cics"
+ "rpc.method" "FULLPROG"
+ "cics.tran" "FULL"
+ "cics.tpn" "CPMI"
+ "cics.interaction" "SYNC_SEND_RECEIVE"
+ "error.type" "com.ibm.connector2.cics.CICSUserInputException"
+ "error.stack" String
+ "error.message" String
+ defaultTags()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/cics/src/test/groovy/JavaGatewayInterfaceInstrumentationTest.groovy b/dd-java-agent/instrumentation/cics/src/test/groovy/JavaGatewayInterfaceInstrumentationTest.groovy
new file mode 100644
index 00000000000..20d005bd4c6
--- /dev/null
+++ b/dd-java-agent/instrumentation/cics/src/test/groovy/JavaGatewayInterfaceInstrumentationTest.groovy
@@ -0,0 +1,121 @@
+import com.ibm.connector2.cics.ECIInteraction
+import com.ibm.ctg.client.JavaGateway
+import datadog.trace.agent.test.InstrumentationSpecification
+import datadog.trace.api.DDSpanTypes
+import datadog.trace.bootstrap.CallDepthThreadLocalMap
+import datadog.trace.bootstrap.instrumentation.api.Tags
+
+import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
+
+class JavaGatewayInterfaceInstrumentationTest extends InstrumentationSpecification {
+
+ ServerSocket serverSocket
+ Thread serverThread
+ int port
+
+ def setup() {
+ // Start a server that accepts and immediately closes connections
+ serverSocket = new ServerSocket(0)
+ port = serverSocket.getLocalPort()
+ serverThread = new Thread({
+ try {
+ while (!serverSocket.isClosed()) {
+ Socket clientSocket = serverSocket.accept()
+ clientSocket.close()
+ }
+ } catch (IOException ignored) {
+ // expected when server socket closes
+ }
+ })
+ serverThread.start()
+
+ // Wait for server to be ready to accept connections
+ // Try to connect to ensure the server thread has entered accept()
+ int maxAttempts = 50
+ int attemptDelayMs = 10
+ for (int i = 0; i < maxAttempts; i++) {
+ try {
+ Socket testSocket = new Socket()
+ testSocket.connect(new InetSocketAddress("127.0.0.1", port), 100)
+ testSocket.close()
+ break // Successfully connected, server is ready
+ } catch (IOException e) {
+ if (i == maxAttempts - 1) {
+ throw new RuntimeException("Server failed to start accepting connections after ${maxAttempts * attemptDelayMs}ms", e)
+ }
+ Thread.sleep(attemptDelayMs)
+ }
+ }
+ }
+
+ def cleanup() {
+ serverSocket?.close()
+ serverThread?.join(1000)
+ }
+
+ def "flow without parent creates new span"() {
+ when:
+ try {
+ new JavaGateway("127.0.0.1", port) // use IPv4 address so we can make sure peer.ipv4 is in the tags
+ } catch (IOException ignored) {
+ // expected - connection will be closed by server
+ }
+
+ then:
+ assertTraces(1) {
+ trace(1) {
+ span(0) {
+ operationName "gateway.flow"
+ spanType DDSpanTypes.RPC
+ errored true
+ tags {
+ "$Tags.COMPONENT" "cics-client"
+ "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT
+ "rpc.system" "cics"
+ "$Tags.PEER_HOSTNAME" "127.0.0.1"
+ "$Tags.PEER_PORT" port
+ "$Tags.PEER_HOST_IPV4" "127.0.0.1"
+ errorTags IOException, String
+ defaultTags()
+ }
+ }
+ }
+ }
+ }
+
+ def "flow with parent span merges into parent"() {
+ when:
+ try {
+ runUnderTrace("parent") {
+ // Simulate being inside ECIInteraction.execute() (cics.execute operation)
+ CallDepthThreadLocalMap.incrementCallDepth(ECIInteraction.class)
+ try {
+ new JavaGateway("127.0.0.1", port) // use IPv4 address so we can make sure peer.ipv4 is in the tags
+ } finally {
+ CallDepthThreadLocalMap.decrementCallDepth(ECIInteraction.class)
+ }
+ }
+ } catch (IOException ignored) {
+ // expected - connection will be closed by server
+ }
+
+ then:
+ assertTraces(1) {
+ trace(1) {
+ span(0) {
+ operationName "parent"
+ errored true
+ tags {
+ // Component and rpc.system are NOT set because we didn't create a new span
+ // We only added connection tags to the existing parent span
+ "$Tags.PEER_HOSTNAME" "127.0.0.1"
+ "$Tags.PEER_PORT" port
+ "$Tags.PEER_HOST_IPV4" "127.0.0.1"
+ errorTags IOException, String
+ defaultTags()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b7b1c2d88ea..b10b7a44146 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -301,6 +301,7 @@ include(
":dd-java-agent:instrumentation:azure-functions-1.2.2",
":dd-java-agent:instrumentation:caffeine-1.0",
":dd-java-agent:instrumentation:cdi-1.2",
+ ":dd-java-agent:instrumentation:cics",
":dd-java-agent:instrumentation:commons-codec-1.1",
":dd-java-agent:instrumentation:commons-fileupload-1.5",
":dd-java-agent:instrumentation:commons-httpclient-2.0",