From 9ade058857685ca384f70d16ca3d305abd2dd92d Mon Sep 17 00:00:00 2001 From: DJ Gregor Date: Mon, 22 Dec 2025 11:37:16 -0800 Subject: [PATCH] Add CICS instrumentation - ECIInteraction.execute -- instruments the entry point for CICS calls via IBM's javax.resource.cci.Interaction implementation, creating "cics.execute" span and recording a few tags. - JavaGatewayInstrumentation.flow -- records the peer.* tags on the "cics.execute" span created above, or if it doesn't exist, creates a new "gateway.flow" span. The tests don't fully exercise the CICS client-side code, however they exercise enough to ensure the instrumentation creates spans and adds tags as expected. This requires a few JAR files from IBM's CICS SDK for compliation and testing that are not available in Maven Central. A tar.gz artifact is downloaded from IBM's public CICS support archive and the necessary JARs are extracted, following the same pattern used for the JBoss Wildfly smoke tests. --- .../instrumentation/cics/build.gradle | 148 ++++++++++++++++++ .../instrumentation/cics/CicsDecorator.java | 128 +++++++++++++++ .../cics/ECIInteractionInstrumentation.java | 72 +++++++++ .../JavaGatewayInterfaceInstrumentation.java | 104 ++++++++++++ .../ECIInteractionInstrumentationTest.groovy | 147 +++++++++++++++++ ...GatewayInterfaceInstrumentationTest.groovy | 121 ++++++++++++++ settings.gradle.kts | 1 + 7 files changed, 721 insertions(+) create mode 100644 dd-java-agent/instrumentation/cics/build.gradle create mode 100644 dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/CicsDecorator.java create mode 100644 dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/ECIInteractionInstrumentation.java create mode 100644 dd-java-agent/instrumentation/cics/src/main/java/datadog/trace/instrumentation/cics/JavaGatewayInterfaceInstrumentation.java create mode 100644 dd-java-agent/instrumentation/cics/src/test/groovy/ECIInteractionInstrumentationTest.groovy create mode 100644 dd-java-agent/instrumentation/cics/src/test/groovy/JavaGatewayInterfaceInstrumentationTest.groovy 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",