From d61f7157a5ec3edcf01f51a83f59dc9bac16d7ae Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 10 Apr 2026 10:53:29 +0200 Subject: [PATCH 1/2] Add smoke test for websphere-jmx --- .../datadog/smoketest/ProcessManager.groovy | 2 +- dd-smoke-tests/websphere-jmx/build.gradle | 15 +++ .../smoketest/WebSphereJmxSmokeTest.groovy | 119 ++++++++++++++++++ .../src/test/resources/jvm-config.props | 6 + settings.gradle.kts | 1 + 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 dd-smoke-tests/websphere-jmx/build.gradle create mode 100644 dd-smoke-tests/websphere-jmx/src/test/groovy/datadog/smoketest/WebSphereJmxSmokeTest.groovy create mode 100644 dd-smoke-tests/websphere-jmx/src/test/resources/jvm-config.props diff --git a/dd-smoke-tests/src/main/groovy/datadog/smoketest/ProcessManager.groovy b/dd-smoke-tests/src/main/groovy/datadog/smoketest/ProcessManager.groovy index 83a8ff2ccb3..31e24c70488 100644 --- a/dd-smoke-tests/src/main/groovy/datadog/smoketest/ProcessManager.groovy +++ b/dd-smoke-tests/src/main/groovy/datadog/smoketest/ProcessManager.groovy @@ -50,7 +50,7 @@ abstract class ProcessManager extends Specification { // Here for backwards compatibility with single process case @Shared - def logFilePath = logFilePaths[0] + def logFilePath = logFilePaths.length > 0 ? logFilePaths[0] : null def setup() { testedProcesses.each { diff --git a/dd-smoke-tests/websphere-jmx/build.gradle b/dd-smoke-tests/websphere-jmx/build.gradle new file mode 100644 index 00000000000..3cafe416fb6 --- /dev/null +++ b/dd-smoke-tests/websphere-jmx/build.gradle @@ -0,0 +1,15 @@ +apply from: "$rootDir/gradle/java.gradle" + +dependencies { + testImplementation project(':dd-smoke-tests') + testImplementation libs.testcontainers +} + +testJvmConstraints { + // there is no need to run it multiple times since it runs on a container + maxJavaVersion = JavaVersion.VERSION_1_8 +} + +tasks.withType(Test).configureEach { + usesService(testcontainersLimit) +} diff --git a/dd-smoke-tests/websphere-jmx/src/test/groovy/datadog/smoketest/WebSphereJmxSmokeTest.groovy b/dd-smoke-tests/websphere-jmx/src/test/groovy/datadog/smoketest/WebSphereJmxSmokeTest.groovy new file mode 100644 index 00000000000..85e687062ec --- /dev/null +++ b/dd-smoke-tests/websphere-jmx/src/test/groovy/datadog/smoketest/WebSphereJmxSmokeTest.groovy @@ -0,0 +1,119 @@ +package datadog.smoketest + + +import java.time.Duration +import java.util.concurrent.ArrayBlockingQueue +import java.util.concurrent.BlockingQueue +import java.util.concurrent.TimeUnit +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.output.Slf4jLogConsumer +import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.utility.MountableFile +import spock.lang.Shared + +/** + * Smoke test for the websphere-jmx instrumentation. + * + * Builds and starts a WebSphere traditional (tWAS) container with the dd-java-agent baked in + * via jvm-config.props, then verifies that jmxfetch reports WebSphere thread pool metrics + * via statsd UDP. + * + * Note that the websphere related metrics will only arrive if our instrumentation is applied. + */ +class WebSphereJmxSmokeTest extends AbstractSmokeTest { + + private static final Logger LOG = LoggerFactory.getLogger(WebSphereJmxSmokeTest) + + @Override + protected int numberOfProcesses() { + return 0 + } + + @Shared + DatagramSocket statsdSocket + + @Shared + int statsdPort + + @Shared + BlockingQueue statsdMessages = new ArrayBlockingQueue<>(256) + + @Shared + Thread listenerThread + + @Shared + GenericContainer websphere + + def setupSpec() { + statsdSocket = new DatagramSocket() + statsdPort = statsdSocket.getLocalPort() + LOG.info("StatsDServer listening on UDP port {}", statsdPort) + + listenerThread = Thread.start { + byte[] buf = new byte[2048] + while (!Thread.currentThread().interrupted()) { + try { + DatagramPacket packet = new DatagramPacket(buf, buf.length) + statsdSocket.receive(packet) + String msg = new String(packet.getData(), 0, packet.getLength()) + LOG.debug("Received statsd: {}", msg) + statsdMessages.offer(msg, 1, TimeUnit.SECONDS) + } catch (Exception ignored) { + break + } + } + } + + websphere = new GenericContainer("icr.io/appcafe/websphere-traditional:latest") + // inject wished jvm props for the server we are running + .withCopyFileToContainer(MountableFile.forClasspathResource("jvm-config.props"), "/work/config/") + // copy the agent jar + .withCopyFileToContainer(MountableFile.forHostPath(shadowJarPath), "/opt/dd-java-agent.jar") + // let it run on a macos for dev + .withCreateContainerCmdModifier { it.withPlatform('linux/amd64') } + // this is required to send back udp datagrams to us + .withExtraHost('host.docker.internal', 'host-gateway') + // set jmxfetch props + .withEnv('DD_JMXFETCH_STATSD_HOST', 'host.docker.internal') + .withEnv('DD_JMXFETCH_STATSD_PORT', String.valueOf(statsdPort)) + .withLogConsumer(new Slf4jLogConsumer(LOG).withPrefix('websphere')) + // the server will restart 2 times. First to update the jvm props + .waitingFor(Wait.forLogMessage('.*open for e-business.*', 2)) + // it can be long + .withStartupTimeout(Duration.ofMinutes(8)) + // override the command (by default it's /work/start_server.sh) + .withCommand("bash", "-c", "/work/configure.sh && /work/start_server.sh") + + websphere.start() + LOG.info("WebSphere container started") + } + + def cleanupSpec() { + websphere?.stop() + listenerThread?.interrupt() + listenerThread?.join() + statsdSocket?.close() + } + + def "jmxfetch reports WebSphere thread pool metrics via statsd"() { + when: "waiting for websphere.thread_pool metrics" + String metric = waitForMetric('websphere.thread_pool', Duration.ofMinutes(3)) + + then: "at least one thread pool metric arrives" + metric != null + metric.contains('websphere.thread_pool') + } + + def waitForMetric(String prefix, Duration timeout) { + long deadline = System.currentTimeMillis() + timeout.toMillis() + while (System.currentTimeMillis() < deadline) { + String msg = statsdMessages.poll(5, TimeUnit.SECONDS) + if (msg?.contains(prefix)) { + return msg + } + } + return null + } +} diff --git a/dd-smoke-tests/websphere-jmx/src/test/resources/jvm-config.props b/dd-smoke-tests/websphere-jmx/src/test/resources/jvm-config.props new file mode 100644 index 00000000000..530ec041825 --- /dev/null +++ b/dd-smoke-tests/websphere-jmx/src/test/resources/jvm-config.props @@ -0,0 +1,6 @@ +ResourceType=JavaVirtualMachine +ImplementingResourceType=Server +ResourceId=Cell=!{cellName}:Node=!{nodeName}:Server=!{serverName}:JavaProcessDef=:JavaVirtualMachine= +AttributeInfo=jvmEntries + +genericJvmArguments=-javaagent:/opt/dd-java-agent.jar -Ddd.trace.debug=true -Ddd.jmxfetch.websphere.enabled=true diff --git a/settings.gradle.kts b/settings.gradle.kts index 0bac052092a..5525cf3bc94 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -248,6 +248,7 @@ include( ":dd-smoke-tests:vertx-3.9", ":dd-smoke-tests:vertx-3.9-resteasy", ":dd-smoke-tests:vertx-4.2", + ":dd-smoke-tests:websphere-jmx", ":dd-smoke-tests:wildfly", ":dd-smoke-tests:appsec", ":dd-smoke-tests:appsec:spring-tomcat7", From 7002719936fd66cce2c894781d9788545ba8f845 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 10 Apr 2026 11:50:17 +0200 Subject: [PATCH 2/2] avoid udp listener hangs on cleanup --- .../test/groovy/datadog/smoketest/WebSphereJmxSmokeTest.groovy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dd-smoke-tests/websphere-jmx/src/test/groovy/datadog/smoketest/WebSphereJmxSmokeTest.groovy b/dd-smoke-tests/websphere-jmx/src/test/groovy/datadog/smoketest/WebSphereJmxSmokeTest.groovy index 85e687062ec..28b72c111c7 100644 --- a/dd-smoke-tests/websphere-jmx/src/test/groovy/datadog/smoketest/WebSphereJmxSmokeTest.groovy +++ b/dd-smoke-tests/websphere-jmx/src/test/groovy/datadog/smoketest/WebSphereJmxSmokeTest.groovy @@ -92,9 +92,8 @@ class WebSphereJmxSmokeTest extends AbstractSmokeTest { def cleanupSpec() { websphere?.stop() - listenerThread?.interrupt() - listenerThread?.join() statsdSocket?.close() + listenerThread?.join() } def "jmxfetch reports WebSphere thread pool metrics via statsd"() {