-
Notifications
You must be signed in to change notification settings - Fork 323
🪞 10269 - Feature: CICS tracing #10301
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
base: master
Are you sure you want to change the base?
Changes from 4 commits
d304d32
d1d5b05
65c1cb6
d3372cd
47d95f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks for having used otel attribute names |
||
| 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 <a | ||
| * href="https://docs.oracle.com/javaee/6/api/constant-values.html#javax.resource.cci.InteractionSpec.SYNC_SEND">InteractionSpec | ||
| * constants</a> | ||
| */ | ||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as a future improvement this might be cached if the cardinality is not too high
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So far, in my experience, the cardinality has 1 per service (with the note that we tend to do microservices). |
||
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package datadog.trace.instrumentation.cics; | ||
|
|
||
| import static java.util.Arrays.asList; | ||
|
|
||
| import com.google.auto.service.AutoService; | ||
| import datadog.trace.agent.tooling.Instrumenter; | ||
| import datadog.trace.agent.tooling.InstrumenterModule; | ||
| import java.util.List; | ||
|
|
||
| @AutoService(InstrumenterModule.class) | ||
| public class CicsModule extends InstrumenterModule.Tracing { | ||
| public CicsModule() { | ||
| super("cics"); | ||
| } | ||
|
|
||
| @Override | ||
| public String[] helperClassNames() { | ||
| return new String[] {packageName + ".CicsDecorator"}; | ||
| } | ||
|
|
||
| @Override | ||
| public List<Instrumenter> typeInstrumentations() { | ||
| return asList(new ECIInteractionInstrumentation(), new JavaGatewayInterfaceInstrumentation()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| 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.ibm.connector2.cics.ECIInteraction; | ||
| import com.ibm.connector2.cics.ECIInteractionSpec; | ||
| import datadog.trace.agent.tooling.Instrumenter; | ||
| import datadog.trace.bootstrap.CallDepthThreadLocalMap; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentScope; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import net.bytebuddy.asm.Advice; | ||
|
|
||
| public final class ECIInteractionInstrumentation | ||
| implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { | ||
| @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(); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is no need to override it since it's null on the base method