diff --git a/pom.xml b/pom.xml index 18cd989b3..3b1ab826d 100644 --- a/pom.xml +++ b/pom.xml @@ -200,7 +200,13 @@ org.testcontainers testcontainers - 1.18.0 + 1.19.3 + test + + + org.hamcrest + hamcrest-library + 1.3 test diff --git a/src/main/java/org/datadog/jmxfetch/Instance.java b/src/main/java/org/datadog/jmxfetch/Instance.java index 1cc3b91f9..9da8de750 100644 --- a/src/main/java/org/datadog/jmxfetch/Instance.java +++ b/src/main/java/org/datadog/jmxfetch/Instance.java @@ -16,7 +16,6 @@ import java.io.InputStream; import java.lang.management.ManagementFactory; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; diff --git a/src/test/java/org/datadog/jmxfetch/TestCommon.java b/src/test/java/org/datadog/jmxfetch/TestCommon.java index edb8bde68..49fb77a12 100644 --- a/src/test/java/org/datadog/jmxfetch/TestCommon.java +++ b/src/test/java/org/datadog/jmxfetch/TestCommon.java @@ -1,9 +1,5 @@ package org.datadog.jmxfetch; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -19,11 +15,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import javax.management.InstanceAlreadyExistsException; import javax.management.InstanceNotFoundException; import javax.management.MBeanRegistrationException; @@ -31,12 +24,15 @@ import javax.management.MalformedObjectNameException; import javax.management.NotCompliantMBeanException; import javax.management.ObjectName; + +import org.junit.After; +import org.junit.BeforeClass; + import org.datadog.jmxfetch.reporter.ConsoleReporter; import org.datadog.jmxfetch.reporter.Reporter; import org.datadog.jmxfetch.util.CustomLogger; +import org.datadog.jmxfetch.util.MetricsAssert; import org.datadog.jmxfetch.util.LogLevel; -import org.junit.After; -import org.junit.BeforeClass; final class ConfigUtil { public static Path writeConfigYamlToTemp(String content, String yamlName) throws IOException { @@ -261,49 +257,7 @@ public void assertMetric( List additionalTags, int countTags, String metricType) { - List tags = new ArrayList(commonTags); - tags.addAll(additionalTags); - - for (Map m : metrics) { - String mName = (String) (m.get("name")); - Double mValue = (Double) (m.get("value")); - Set mTags = new HashSet(Arrays.asList((String[]) (m.get("tags")))); - - if (mName.equals(name)) { - - if (!value.equals(-1)) { - assertEquals((Double) value.doubleValue(), mValue); - } else if (!lowerBound.equals(-1) || !upperBound.equals(-1)) { - assertTrue(mValue > (Double) lowerBound.doubleValue()); - assertTrue(mValue < (Double) upperBound.doubleValue()); - } - - if (countTags != -1) { - assertEquals(countTags, mTags.size()); - } - for (String t : tags) { - assertThat(mTags, hasItem(t)); - } - - if (metricType != null) { - assertEquals(metricType, m.get("type")); - } - // Brand the metric - m.put("tested", true); - - return; - } - } - fail( - "Metric assertion failed (name: " - + name - + ", value: " - + value - + ", tags: " - + tags - + ", #tags: " - + countTags - + ")."); + MetricsAssert.assertMetric(name, value, lowerBound, upperBound, commonTags, additionalTags, countTags, metricType, this.metrics); } public void assertMetric( diff --git a/src/test/java/org/datadog/jmxfetch/TestGCMetrics.java b/src/test/java/org/datadog/jmxfetch/TestGCMetrics.java new file mode 100644 index 000000000..fdb5f4d45 --- /dev/null +++ b/src/test/java/org/datadog/jmxfetch/TestGCMetrics.java @@ -0,0 +1,214 @@ +package org.datadog.jmxfetch; + +import static org.datadog.jmxfetch.util.MetricsAssert.*; +import static org.datadog.jmxfetch.util.server.JDKImage.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import lombok.extern.slf4j.Slf4j; + +import org.junit.Test; + +import org.datadog.jmxfetch.reporter.ConsoleReporter; +import org.datadog.jmxfetch.util.MetricsAssert; +import org.datadog.jmxfetch.util.server.MisbehavingJMXServer; +import org.datadog.jmxfetch.util.server.SimpleAppContainer; + +@Slf4j +public class TestGCMetrics extends TestCommon { + + @Test + public void testJMXDirectBasic() throws Exception { + try (final SimpleAppContainer container = new SimpleAppContainer()) { + container.start(); + final String ipAddress = container.getIp(); + final String remoteJmxServiceUrl = String.format( + "service:jmx:rmi:///jndi/rmi://%s:%s/jmxrmi", ipAddress, container.getRMIPort()); + final JMXServiceURL jmxUrl = new JMXServiceURL(remoteJmxServiceUrl); + final JMXConnector conn = JMXConnectorFactory.connect(jmxUrl); + final MBeanServerConnection mBeanServerConnection = conn.getMBeanServerConnection(); + assertDomainPresent("java.lang", mBeanServerConnection); + } + } + + @Test + public void testDefaultOldGC() throws IOException { + try (final MisbehavingJMXServer server = new MisbehavingJMXServer.Builder().build()) { + final List> actualMetrics = startAndGetMetrics(server, false); + List gcGenerations = Arrays.asList( + "G1 Old Generation", + "G1 Young Generation"); + assertGCMetric(actualMetrics, "jvm.gc.cms.count", gcGenerations); + assertGCMetric(actualMetrics, "jvm.gc.parnew.time", gcGenerations); + } + } + + @Test + public void testDefaultNewGCMetricsUseParallelGC() throws IOException { + try (final MisbehavingJMXServer server = new MisbehavingJMXServer.Builder().withJDKImage( + JDK_11).appendJavaOpts("-XX:+UseParallelGC").build()) { + final List> actualMetrics = startAndGetMetrics(server, true); + assertThat(actualMetrics, hasSize(13)); + assertGCMetric(actualMetrics, + "jvm.gc.minor_collection_count", "PS Scavenge", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.minor_collection_time", "PS Scavenge", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.major_collection_count", "PS MarkSweep", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.major_collection_time", "PS MarkSweep", "counter"); + } + } + + @Test + public void testDefaultNewGCMetricsUseConcMarkSweepGC() throws IOException { + try (final MisbehavingJMXServer server = new MisbehavingJMXServer.Builder().withJDKImage( + JDK_11).appendJavaOpts("-XX:+UseConcMarkSweepGC").build()) { + final List> actualMetrics = startAndGetMetrics(server, true); + assertThat(actualMetrics, hasSize(13)); + assertGCMetric(actualMetrics, + "jvm.gc.minor_collection_count", "ParNew", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.minor_collection_time", "ParNew", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.major_collection_count", "ConcurrentMarkSweep", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.major_collection_time", "ConcurrentMarkSweep", "counter"); + } + } + + @Test + public void testDefaultNewGCMetricsUseG1GC() throws IOException { + try (final MisbehavingJMXServer server = new MisbehavingJMXServer.Builder().withJDKImage( + JDK_17).appendJavaOpts("-XX:+UseG1GC").build()) { + final List> actualMetrics = startAndGetMetrics(server, true); + assertThat(actualMetrics, hasSize(13)); + assertGCMetric(actualMetrics, + "jvm.gc.minor_collection_count", "G1 Young Generation", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.minor_collection_time", "G1 Young Generation", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.major_collection_count", "G1 Old Generation", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.major_collection_time", "G1 Old Generation", "counter"); + } + } + + @Test + public void testDefaultNewGCMetricsUseZGC() throws IOException { + try (final MisbehavingJMXServer server = new MisbehavingJMXServer.Builder().withJDKImage( + JDK_17).appendJavaOpts("-XX:+UseZGC").build()) { + final List> actualMetrics = startAndGetMetrics(server, true); + assertThat(actualMetrics, hasSize(13)); + assertGCMetric(actualMetrics, + "jvm.gc.zgc_pauses_collection_count", "ZGC Pauses", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.zgc_pauses_collection_time", "ZGC Pauses", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.zgc_cycles_collection_count", "ZGC Cycles", "counter"); + assertGCMetric(actualMetrics, + "jvm.gc.zgc_cycles_collection_time", "ZGC Cycles", "counter"); + } + } + + private List> startAndGetMetrics(final MisbehavingJMXServer server, + final boolean newGCMetrics) throws IOException { + server.start(); + this.initApplicationWithYamlLines( + "init_config:", + " is_jmx: true", + " new_gc_metrics: " + newGCMetrics, + "", + "instances:", + " - name: jmxint_container", + " host: " + server.getIp(), + " collect_default_jvm_metrics: true", + " max_returned_metrics: 300000", + " port: " + server.getRMIPort()); + // Run one iteration first + // TODO: Investigate why we have to run this twice - AMLII-1353 + this.app.doIteration(); + // And then pull get the metrics or else reporter does not have correct number of metrics + ((ConsoleReporter) appConfig.getReporter()).getMetrics(); + + // Actual iteration we care about + this.app.doIteration(); + return ((ConsoleReporter) appConfig.getReporter()).getMetrics(); + } + + private static void assertGCMetric(final List> actualMetrics, + final String expectedMetric, + final String gcGeneration, + final String metricType) { + MetricsAssert.assertMetric( + expectedMetric, + -1, + -1, + 10.0, + Collections.singletonList(String.format("name:%s", gcGeneration)), + Arrays.asList( + "instance:jmxint_container", + "jmx_domain:java.lang", + "type:GarbageCollector"), + 5, + metricType, + actualMetrics); + } + + /* + This function is needed as the TestGCMetrics.testDefaultOldGC asserts on two metrics that have + different tags. MetricsAssert.assertMetric expects metrics to have unique names so can't be used + to verify correctly G1 Old Generation and G1 Young Generation for the metric jvm.gc.cms.count and + jvm.gc.parnew.time. + */ + private static void assertGCMetric(final List> actualMetrics, + final String expectedMetric, + final List gcGenerations) { + final List> filteredMetrics = new ArrayList<>(); + for (Map actualMetric : actualMetrics) { + final String name = (String) actualMetric.get("name"); + if(expectedMetric.equals(name)) { + filteredMetrics.add(actualMetric); + } + } + assertThat(filteredMetrics, hasSize(gcGenerations.size())); + for (final String name : gcGenerations) { + log.debug("Asserting for metric '{}'", name); + boolean found = false; + for (Map filteredMetric : filteredMetrics) { + final Set mTags = new HashSet<>( + Arrays.asList((String[]) (filteredMetric.get("tags")))); + + if(mTags.contains(String.format("name:%s", name))) { + assertThat(mTags, not(empty())); + assertThat(mTags, hasSize(5)); + log.debug("mTags '{}' has size: {}\n{}", name, mTags.size(), mTags); + assertThat(mTags, hasItems( + "instance:jmxint_container", + "jmx_domain:java.lang", + "type:GarbageCollector", + String.format("name:%s", name))); + found = true; + } + } + assertThat(String.format("Did not find metric '%s'", name), found, is(true)); + } + } +} diff --git a/src/test/java/org/datadog/jmxfetch/TestReconnectContainer.java b/src/test/java/org/datadog/jmxfetch/TestReconnectContainer.java index 518bb4b8c..1c964529e 100644 --- a/src/test/java/org/datadog/jmxfetch/TestReconnectContainer.java +++ b/src/test/java/org/datadog/jmxfetch/TestReconnectContainer.java @@ -1,6 +1,10 @@ package org.datadog.jmxfetch; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import static org.datadog.jmxfetch.util.MetricsAssert.assertDomainPresent; +import static org.datadog.jmxfetch.util.MetricsAssert.isDomainPresent; import java.io.IOException; import java.util.Collections; @@ -12,7 +16,6 @@ import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -39,21 +42,6 @@ public class TestReconnectContainer extends TestCommon { private JMXServerSupervisorClient supervisorClient; private static Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(log); - private static boolean isDomainPresent(String domain, MBeanServerConnection mbs) { - boolean found = false; - try { - String[] domains = mbs.getDomains(); - for (int i = 0; i < domains.length; i++) { - if (domains[i].equals(domain)) { - found = true; - } - } - } catch (IOException e) { - found = false; - } - return found; - } - private static ImageFromDockerfile img = new ImageFromDockerfile() .withFileFromPath(".", Paths.get("./tools/misbehaving-jmx-server/")); @@ -101,7 +89,7 @@ public void testJMXDirectBasic() throws Exception { JMXConnector conn = JMXConnectorFactory.connect(jmxUrl); MBeanServerConnection mBeanServerConnection = conn.getMBeanServerConnection(); - assertEquals(true, isDomainPresent("Bohnanza", mBeanServerConnection)); + assertDomainPresent("Bohnanza", mBeanServerConnection); } @Test @@ -117,15 +105,15 @@ public void testJMXDirectReconnect() throws Exception { JMXConnector conn = JMXConnectorFactory.connect(jmxUrl); MBeanServerConnection mBeanServerConnection = conn.getMBeanServerConnection(); - assertEquals(true, isDomainPresent("Bohnanza", mBeanServerConnection)); + assertDomainPresent("Bohnanza", mBeanServerConnection); this.controlClient.jmxCutNetwork(); - assertEquals(false, isDomainPresent("Bohnanza", mBeanServerConnection)); + assertFalse(isDomainPresent("Bohnanza", mBeanServerConnection)); this.controlClient.jmxRestoreNetwork(); - assertEquals(true, isDomainPresent("Bohnanza", mBeanServerConnection)); + assertDomainPresent("Bohnanza", mBeanServerConnection); } @Test diff --git a/src/test/java/org/datadog/jmxfetch/util/MetricsAssert.java b/src/test/java/org/datadog/jmxfetch/util/MetricsAssert.java new file mode 100644 index 000000000..41ca0a8ad --- /dev/null +++ b/src/test/java/org/datadog/jmxfetch/util/MetricsAssert.java @@ -0,0 +1,99 @@ +package org.datadog.jmxfetch.util; + +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.management.MBeanServerConnection; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MetricsAssert { + + public static void assertMetric( + String name, + Number value, + Number lowerBound, + Number upperBound, + List commonTags, + List additionalTags, + int countTags, + String metricType, + List> actualMetrics) { + List tags = new ArrayList<>(commonTags); + tags.addAll(additionalTags); + + for (Map m : actualMetrics) { + String mName = (String) (m.get("name")); + Double mValue = (Double) (m.get("value")); + Set mTags = new HashSet<>(Arrays.asList((String[]) (m.get("tags")))); + + if (mName.equals(name)) { + + if (!value.equals(-1)) { + assertEquals((Double) value.doubleValue(), mValue); + } else if (!lowerBound.equals(-1) || !upperBound.equals(-1)) { + assertTrue(mValue > (Double) lowerBound.doubleValue()); + assertTrue(mValue < (Double) upperBound.doubleValue()); + } + + if (countTags != -1) { + assertEquals(countTags, mTags.size()); + } + for (String t : tags) { + assertThat(mTags, hasItem(t)); + } + + if (metricType != null) { + assertEquals(metricType, m.get("type")); + } + // Brand the metric + m.put("tested", true); + + return; + } + } + fail( + "Metric assertion failed (name: " + + name + + ", value: " + + value + + ", tags: " + + tags + + ", #tags: " + + countTags + + ")."); + + } + + public static void assertDomainPresent(final String domain, final MBeanServerConnection mbs){ + assertThat(String.format("Could not find domain '%s'", domain), + isDomainPresent(domain, mbs), equalTo(true)); + } + + public static boolean isDomainPresent(final String domain, final MBeanServerConnection mbs) { + boolean found = false; + try { + final String[] domains = mbs.getDomains(); + for (String s : domains) { + if(s.equals(domain)) { + found = true; + break; + } + } + } catch (IOException e) { + log.warn("Got an exception checking if domain is present", e); + } + return found; + } +} diff --git a/src/test/java/org/datadog/jmxfetch/util/server/JDKImage.java b/src/test/java/org/datadog/jmxfetch/util/server/JDKImage.java new file mode 100644 index 000000000..365f7bd5b --- /dev/null +++ b/src/test/java/org/datadog/jmxfetch/util/server/JDKImage.java @@ -0,0 +1,19 @@ +package org.datadog.jmxfetch.util.server; + +public enum JDKImage { + BASE("base"), + JDK_11("eclipse-temurin:11"), + JDK_17("eclipse-temurin:17"), + JDK_21("eclipse-temurin:21"); + + private final String image; + + private JDKImage(final String image) { + this.image = image; + } + + @Override + public String toString() { + return this.image; + } +} diff --git a/src/test/java/org/datadog/jmxfetch/util/server/MisbehavingJMXServer.java b/src/test/java/org/datadog/jmxfetch/util/server/MisbehavingJMXServer.java new file mode 100644 index 000000000..7ebf69dda --- /dev/null +++ b/src/test/java/org/datadog/jmxfetch/util/server/MisbehavingJMXServer.java @@ -0,0 +1,165 @@ +package org.datadog.jmxfetch.util.server; + +import java.io.IOException; +import java.nio.file.Paths; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.lifecycle.Startable; + +import lombok.extern.slf4j.Slf4j; + +import org.datadog.jmxfetch.JMXServerControlClient; +import org.datadog.jmxfetch.JMXServerSupervisorClient; + +@Slf4j +public class MisbehavingJMXServer implements Startable { + + public static final int DEFAULT_RMI_PORT = 9090; + public static final int DEFAULT_CONTROL_PORT = 9091; + public static final int DEFAULT_SUPERVISOR_PORT = 9092; + private static final String DEFAULT_JDK_IMAGE = "base"; + private static final String DEFAULT_MISBEHAVING_OPTS = "-Xmx128M -Xms128M"; + private static final String RMI_PORT = "RMI_PORT"; + private static final String CONTROL_PORT = "CONTROL_PORT"; + private static final String SUPERVISOR_PORT = "SUPERVISOR_PORT"; + public static final String MISBEHAVING_OPTS = "MISBEHAVING_OPTS"; + private final String jdkImage; + + private final String javaOpts; + private final int rmiPort; + private final int controlPort; + private final int supervisorPort; + private final GenericContainer server; + private JMXServerControlClient controlClient; + private JMXServerSupervisorClient supervisorClient; + + public MisbehavingJMXServer( + final String jdkImage, + final String javaOpts, + final int rmiPort, + final int controlPort, + final int supervisorPort) { + this.javaOpts = javaOpts; + this.rmiPort = rmiPort; + this.controlPort = controlPort; + this.supervisorPort = supervisorPort; + this.jdkImage = jdkImage; + final ImageFromDockerfile img = new ImageFromDockerfile() + .withFileFromPath(".", Paths.get("./tools/misbehaving-jmx-server/")) + .withBuildArg("FINAL_JRE_IMAGE", this.jdkImage); + this.server = new GenericContainer<>(img) + .withEnv(RMI_PORT, String.valueOf(rmiPort)) + .withEnv(CONTROL_PORT, String.valueOf(controlPort)) + .withEnv(SUPERVISOR_PORT, String.valueOf(supervisorPort)) + .withEnv(MISBEHAVING_OPTS, this.javaOpts) + .waitingFor(Wait.forLogMessage( + ".*Supervisor HTTP Server Started. Waiting for initialization payload POST to /init.*", + 1)); + } + + @Override + public void start() { + log.info("Starting MisbehavingJMXServer with Docker image '{}' with MISBEHAVING_OPTS '{}'", + this.jdkImage, this.javaOpts); + this.server.start(); + final String ipAddress = this.getIp(); + this.controlClient = new JMXServerControlClient(ipAddress, this.controlPort); + this.supervisorClient = new JMXServerSupervisorClient(ipAddress, this.supervisorPort); + try { + log.debug("Initializing JMXServer"); + this.supervisorClient.initializeJMXServer(ipAddress); + } catch (IOException e) { + log.error("Could not initialize JMX Server", e); + } + } + + @Override + public void stop() { + this.server.stop(); + } + + @Override + public void close() { + this.stop(); + } + + public String getIp() { + return this.server.getContainerInfo().getNetworkSettings().getIpAddress(); + } + + public void cutNetwork() throws IOException { + this.controlClient.jmxCutNetwork(); + } + + public void restoreNetwork() throws IOException { + this.controlClient.jmxRestoreNetwork(); + } + + public int getRMIPort() { + return this.rmiPort; + } + + public static class Builder { + + private String jdkImage; + private String javaOpts; + private int rmiPort; + private int controlPort; + private int supervisorPort; + + public Builder() { + this.jdkImage = MisbehavingJMXServer.DEFAULT_JDK_IMAGE; + this.javaOpts = MisbehavingJMXServer.DEFAULT_MISBEHAVING_OPTS; + this.rmiPort = MisbehavingJMXServer.DEFAULT_RMI_PORT; + this.controlPort = MisbehavingJMXServer.DEFAULT_CONTROL_PORT; + this.supervisorPort = MisbehavingJMXServer.DEFAULT_SUPERVISOR_PORT; + } + + public Builder withJDKImage(final String jdkImage) { + this.jdkImage = jdkImage; + return this; + } + + public Builder withJDKImage(final JDKImage jdkImage) { + this.jdkImage = jdkImage.toString(); + return this; + } + + public Builder withJavaOpts(String javaOpts) { + this.javaOpts = javaOpts; + return this; + } + + public Builder appendJavaOpts(String javaOpts) { + this.javaOpts = String.format("%s %s", javaOpts, this.javaOpts); + return this; + } + + public Builder withRmiPort(int rmiPort) { + this.rmiPort = rmiPort; + return this; + } + + public Builder withControlPort(int controlPort) { + this.controlPort = controlPort; + return this; + } + + public Builder withSupervisorPort(int supervisorPort) { + this.supervisorPort = supervisorPort; + return this; + } + + public MisbehavingJMXServer build() { + return new MisbehavingJMXServer( + this.jdkImage, + this.javaOpts, + this.rmiPort, + this.controlPort, + this.supervisorPort + ); + } + } +} diff --git a/src/test/java/org/datadog/jmxfetch/util/server/SimpleApp.java b/src/test/java/org/datadog/jmxfetch/util/server/SimpleApp.java new file mode 100644 index 000000000..81c0daee2 --- /dev/null +++ b/src/test/java/org/datadog/jmxfetch/util/server/SimpleApp.java @@ -0,0 +1,95 @@ +package org.datadog.jmxfetch.util.server; + +import java.lang.management.ManagementFactory; +import java.util.Hashtable; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import javax.management.InstanceAlreadyExistsException; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; + +// TODO: Create tests to check all supported versions of Java work with this server - AMLII-1354 +class SimpleApp { + public interface SampleMBean { + + Integer getShouldBe100(); + + Double getShouldBe1000(); + + Long getShouldBe1337(); + + Float getShouldBe1_1(); + + int getShouldBeCounter(); + } + + public static class Sample implements SampleMBean { + + private final AtomicInteger counter = new AtomicInteger(0); + + @Override + public Integer getShouldBe100() { + return 100; + } + + @Override + public Double getShouldBe1000() { + return 200.0; + } + + @Override + public Long getShouldBe1337() { + return 1337L; + } + + @Override + public Float getShouldBe1_1() { + return 1.1F; + } + + @Override + public int getShouldBeCounter() { + return this.counter.get(); + } + } + + public static void main(String[] args) { + System.out.println("Starting sample app..."); + try { + final Hashtable pairs = new Hashtable<>(); + pairs.put("name", "default"); + pairs.put("type", "simple"); + final Thread daemonThread = getThread(pairs); + daemonThread.start(); + daemonThread.join(); + } catch (MalformedObjectNameException | InstanceAlreadyExistsException | + MBeanRegistrationException | NotCompliantMBeanException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private static Thread getThread(final Hashtable pairs) + throws MalformedObjectNameException, InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException { + final ObjectName objectName = new ObjectName("dd.test.sample", pairs); + final MBeanServer server = ManagementFactory.getPlatformMBeanServer(); + final Sample sample = new Sample(); + server.registerMBean(sample, objectName); + final Thread daemonThread = new Thread(new Runnable() { + @Override + public void run() { + while (sample.counter.incrementAndGet() > 0) { + try { + Thread.sleep(TimeUnit.SECONDS.toSeconds(5)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + }); + daemonThread.setDaemon(true); + return daemonThread; + } +} diff --git a/src/test/java/org/datadog/jmxfetch/util/server/SimpleAppContainer.java b/src/test/java/org/datadog/jmxfetch/util/server/SimpleAppContainer.java new file mode 100644 index 000000000..b6702252e --- /dev/null +++ b/src/test/java/org/datadog/jmxfetch/util/server/SimpleAppContainer.java @@ -0,0 +1,68 @@ +package org.datadog.jmxfetch.util.server; + +import java.nio.file.Paths; +import java.time.Duration; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.lifecycle.Startable; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SimpleAppContainer implements Startable { + + private static final String JAVA_OPTS = "JAVA_OPTS"; + private static final String RMI_PORT = "RMI_PORT"; + private final String jreDockerImage; + private final String javaOpts; + private final int rmiPort; + private final GenericContainer server; + + public SimpleAppContainer() { + this("eclipse-temurin:17", "", MisbehavingJMXServer.DEFAULT_RMI_PORT); + + } + + public SimpleAppContainer(final String jreDockerImage, final String javaOpts, final int rmiPort) { + this.jreDockerImage = jreDockerImage; + this.javaOpts = javaOpts; + this.rmiPort = rmiPort; + final ImageFromDockerfile img = new ImageFromDockerfile() + .withFileFromPath("app.java", Paths.get("./src/test/java/org/datadog/jmxfetch/util/server/SimpleApp.java")) + .withFileFromClasspath("Dockerfile", "org/datadog/jmxfetch/util/server/Dockerfile-SimpleApp") + .withFileFromClasspath("run.sh", "org/datadog/jmxfetch/util/server/run-SimpleApp.sh") + .withBuildArg("JRE_DOCKER_IMAGE", jreDockerImage); + this.server = new GenericContainer<>(img) + .withEnv(JAVA_OPTS, this.javaOpts) + .withEnv(RMI_PORT, Integer.toString(this.rmiPort)) + .withExposedPorts(this.rmiPort) + .waitingFor(Wait.forListeningPorts(this.rmiPort).withStartupTimeout(Duration.ofSeconds(10))); + } + + @Override + public void start() { + log.info("Starting SimpleApp with Docker image '{}' with JAVA_OPTS '{}' in port '{}'", + this.jreDockerImage, this.javaOpts, this.rmiPort); + this.server.start(); + log.info(this.server.getLogs()); + } + + @Override + public void stop() { + this.server.stop(); + } + + public void close() { + this.stop(); + } + + public String getIp() { + return this.server.getContainerInfo().getNetworkSettings().getIpAddress(); + } + + public int getRMIPort() { + return this.rmiPort; + } +} diff --git a/src/test/resources/org/datadog/jmxfetch/util/server/Dockerfile-SimpleApp b/src/test/resources/org/datadog/jmxfetch/util/server/Dockerfile-SimpleApp new file mode 100644 index 000000000..81f60f0ef --- /dev/null +++ b/src/test/resources/org/datadog/jmxfetch/util/server/Dockerfile-SimpleApp @@ -0,0 +1,15 @@ +# syntax=docker/dockerfile:1 + +# Allows to cheange the JDK image used +ARG JRE_DOCKER_IMAGE=eclipse-temurin:11 + +# Use the official JDK image as the base image +FROM ${JRE_DOCKER_IMAGE} + +WORKDIR /app + +COPY run.sh app.java /app/ + +EXPOSE 9010 + +ENTRYPOINT [ "/app/run.sh" ] diff --git a/src/test/resources/org/datadog/jmxfetch/util/server/run-SimpleApp.sh b/src/test/resources/org/datadog/jmxfetch/util/server/run-SimpleApp.sh new file mode 100755 index 000000000..5e1f63b65 --- /dev/null +++ b/src/test/resources/org/datadog/jmxfetch/util/server/run-SimpleApp.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +set -f + +[ -n "$JAVA_OPTS" ] || JAVA_OPTS="-Xmx128M -Xms128M" +[ -n "$RMI_PORT" ] || RMI_PORT="9010" + +echo "Using `java --version`" +echo "With JAVA_OPTS '${JAVA_OPTS}'" +CONTAINER_IP=`awk 'END{print $1}' /etc/hosts` + +# shellcheck disable=SC2086 +javac -d app app.java + +echo "Starting app with hostname set to ${CONTAINER_IP}" + +java -cp ./app \ + ${JAVA_OPTS} \ + -Dcom.sun.management.jmxremote=true \ + -Dcom.sun.management.jmxremote.port=${RMI_PORT} \ + -Dcom.sun.management.jmxremote.rmi.port=${RMI_PORT} \ + -Dcom.sun.management.jmxremote.authenticate=false \ + -Dcom.sun.management.jmxremote.ssl=false \ + -Djava.rmi.server.hostname=${CONTAINER_IP} \ + org.datadog.jmxfetch.util.server.SimpleApp + +# java -jar jmxterm-1.0.2-uber.jar -l service:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi diff --git a/tools/misbehaving-jmx-server/Dockerfile b/tools/misbehaving-jmx-server/Dockerfile index 938e8c112..81fe4e01f 100644 --- a/tools/misbehaving-jmx-server/Dockerfile +++ b/tools/misbehaving-jmx-server/Dockerfile @@ -1,3 +1,6 @@ +# syntax=docker/dockerfile:1 +# Use by default the JDK image used to build the jar +ARG FINAL_JRE_IMAGE=base # Use the official JDK image as the base image FROM eclipse-temurin:17 AS base @@ -9,22 +12,42 @@ WORKDIR /app # Copy the pom.xml and Maven files and install the dependencies COPY .mvn .mvn/ COPY pom.xml mvnw mvnw.cmd ./ + +# TODO: investigate why mount caching does not seem to work Test containers +# Enabling this will speed up tests as the Maven cache can be shared between all builds +# RUN --mount=type=cache,id=mavenCache,target=/root/.m2,sharing=locked \ RUN set -eu && \ ./mvnw dependency:resolve; # Copy the source code and build the JAR file COPY src/ src/ + +# TODO: investigate why mount caching does not seem to work Test containers +# RUN --mount=type=cache,id=mavenCache,target=/root/.m2,sharing=locked \ RUN set -eu && \ ./mvnw clean package assembly:single; -# Use the base image as the the final image -FROM base AS final +# Use the image specified by FINAL_JRE_IMAGE build arg (default "base") +FROM ${FINAL_JRE_IMAGE} AS final # Set the working directory to /app WORKDIR /app +COPY scripts/start.sh /usr/bin/ + # Copy the JAR file from the Maven image to the final image COPY --from=build /app/target/misbehavingjmxserver-1.0-SNAPSHOT-jar-with-dependencies.jar . +# RMI Port +EXPOSE 9090 + +# Control Port +EXPOSE 9091 + +# Supervisor Port +EXPOSE 9092 + # Run the supervisor class from the jar -CMD ["java", "-cp", "misbehavingjmxserver-1.0-SNAPSHOT-jar-with-dependencies.jar", "org.datadog.supervisor.App"] \ No newline at end of file +ENTRYPOINT [ "/usr/bin/start.sh" ] + +CMD [ "org.datadog.supervisor.App" ] diff --git a/tools/misbehaving-jmx-server/README.md b/tools/misbehaving-jmx-server/README.md index 93df69e07..38c100b2d 100644 --- a/tools/misbehaving-jmx-server/README.md +++ b/tools/misbehaving-jmx-server/README.md @@ -25,6 +25,7 @@ a secondary `init` payload that contains the correct RMI Hostname. It is designe - `RMI_HOST` - hostname for JMX to listen on (default localhost) - `CONTROL_PORT` - HTTP control port (default 8080) - `SUPERVISOR_PORT` - HTTP control port for the supervisor process (if using) (default 8088) +- `MISBEHAVING_OPTS` - Manages memory, GC configurations, and system properties of the Java process running the JMXServer (default `-Xmx128M -Xms128M`) ## HTTP Control Actions (jmx-server) - POST `/cutNetwork` - Denies any requests to create a new socket (ie, no more connections will be 'accept'ed) and then closes existing TCP sockets diff --git a/tools/misbehaving-jmx-server/pom.xml b/tools/misbehaving-jmx-server/pom.xml index 9a8ef20b6..d19bcfd6e 100644 --- a/tools/misbehaving-jmx-server/pom.xml +++ b/tools/misbehaving-jmx-server/pom.xml @@ -53,10 +53,9 @@ - junit - junit - 4.11 - test + org.apache.commons + commons-lang3 + 3.12.0 @@ -64,6 +63,13 @@ snakeyaml ${snakeyaml.version} + + + junit + junit + 4.11 + test + diff --git a/tools/misbehaving-jmx-server/scripts/start.sh b/tools/misbehaving-jmx-server/scripts/start.sh new file mode 100755 index 000000000..87ec5a88c --- /dev/null +++ b/tools/misbehaving-jmx-server/scripts/start.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh + +set -f + +echo "Running $@" + +[ -n "$MISBEHAVING_OPTS" ] || MISBEHAVING_OPTS="-Xmx128M -Xms128M" + +echo "Using `java --version`" +echo "With MISBEHAVING_OPTS '${MISBEHAVING_OPTS}'" + +# shellcheck disable=SC2086 +java -Xmx64M -Xms64M \ + -cp misbehavingjmxserver-1.0-SNAPSHOT-jar-with-dependencies.jar \ + "$@" diff --git a/tools/misbehaving-jmx-server/src/main/java/org/datadog/misbehavingjmxserver/App.java b/tools/misbehaving-jmx-server/src/main/java/org/datadog/misbehavingjmxserver/App.java index 2597dbce5..ca659dc72 100644 --- a/tools/misbehaving-jmx-server/src/main/java/org/datadog/misbehavingjmxserver/App.java +++ b/tools/misbehaving-jmx-server/src/main/java/org/datadog/misbehavingjmxserver/App.java @@ -145,8 +145,7 @@ public class App { private static boolean started = false; final static String testDomain = "Bohnanza"; - public static void main( String[] args ) throws IOException, MalformedObjectNameException, InstanceAlreadyExistsException, MBeanRegistrationException, NotCompliantMBeanException - { + public static void main( String[] args ) throws IOException{ AppConfig config = new AppConfig(); JCommander jCommander = JCommander.newBuilder() @@ -258,7 +257,7 @@ public static void main( String[] args ) throws IOException, MalformedObjectName try { Thread.currentThread().join(); } catch (InterruptedException e) { - e.printStackTrace(); + log.error("Got an InterruptedException", e); } } } diff --git a/tools/misbehaving-jmx-server/src/main/java/org/datadog/supervisor/App.java b/tools/misbehaving-jmx-server/src/main/java/org/datadog/supervisor/App.java index b9d13933e..312dfc760 100644 --- a/tools/misbehaving-jmx-server/src/main/java/org/datadog/supervisor/App.java +++ b/tools/misbehaving-jmx-server/src/main/java/org/datadog/supervisor/App.java @@ -1,16 +1,15 @@ package org.datadog.supervisor; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.net.ConnectException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import org.datadog.Defaults; import lombok.extern.slf4j.Slf4j; @@ -38,6 +37,8 @@ class SupervisorInitSpec { @Slf4j public class App { + + private static final String MISBEHAVING_OPTS_ENV = "MISBEHAVING_OPTS"; private static Process process = null; // marked when OS process is started private static AtomicBoolean running = new AtomicBoolean(false); @@ -123,12 +124,21 @@ static void stopJMXServer() throws IOException, InterruptedException { } static void startJMXServer() throws IOException { - ProcessBuilder pb = new ProcessBuilder("java", + /* + MISBEHAVING_OPTS_ENV is the environment variable used to pass configuration flags and + system properties to the JVM that runs the JMXServer. This allows you to do such things as + change the garbage collector use by passing "-XX:+UseParallelGC" to it. + */ + final String misbehavingOpts = System.getenv(MISBEHAVING_OPTS_ENV); + final String[] extraOpts = misbehavingOpts !=null ? StringUtils.split(misbehavingOpts) : ArrayUtils.EMPTY_STRING_ARRAY; + final String[] command = ArrayUtils.addAll( ArrayUtils.insert(0, extraOpts, "java"), "-cp", selfJarPath, jmxServerEntrypoint, "--rmi-host", App.config.rmiHostname); + log.info("Running JMXServer with command '{}'", ArrayUtils.toString(command)); + final ProcessBuilder pb = new ProcessBuilder(command); pb.inheritIO(); process = pb.start(); running.set(true);