diff --git a/jmx-scraper/build.gradle.kts b/jmx-scraper/build.gradle.kts index d3c7e4b58..1ffe28de6 100644 --- a/jmx-scraper/build.gradle.kts +++ b/jmx-scraper/build.gradle.kts @@ -29,6 +29,9 @@ testing { dependencies { implementation("org.testcontainers:junit-jupiter") implementation("org.slf4j:slf4j-simple") + implementation("com.linecorp.armeria:armeria-junit5") + implementation("com.linecorp.armeria:armeria-grpc") + implementation("io.opentelemetry.proto:opentelemetry-proto:0.20.0-alpha") } } } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java new file mode 100644 index 000000000..f85a5ba17 --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java @@ -0,0 +1,107 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.MountableFile; + +/** Test container that allows to execute {@link JmxScraper} in an isolated container */ +public class JmxScraperContainer extends GenericContainer { + + private final String endpoint; + private final Set targetSystems; + private String serviceUrl; + private int intervalMillis; + private final Set customYamlFiles; + + public JmxScraperContainer(String otlpEndpoint) { + super("openjdk:8u272-jre-slim"); + + String scraperJarPath = System.getProperty("shadow.jar.path"); + assertThat(scraperJarPath).isNotNull(); + + this.withCopyFileToContainer(MountableFile.forHostPath(scraperJarPath), "/scraper.jar") + .waitingFor( + Wait.forLogMessage(".*JMX scraping started.*", 1) + .withStartupTimeout(Duration.ofSeconds(10))); + + this.endpoint = otlpEndpoint; + this.targetSystems = new HashSet<>(); + this.customYamlFiles = new HashSet<>(); + this.intervalMillis = 1000; + } + + @CanIgnoreReturnValue + public JmxScraperContainer withTargetSystem(String targetSystem) { + targetSystems.add(targetSystem); + return this; + } + + @CanIgnoreReturnValue + public JmxScraperContainer withIntervalMillis(int intervalMillis) { + this.intervalMillis = intervalMillis; + return this; + } + + @CanIgnoreReturnValue + public JmxScraperContainer withService(String host, int port) { + // TODO: adding a way to provide 'host:port' syntax would make this easier for end users + this.serviceUrl = + String.format( + Locale.getDefault(), "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", host, port); + return this; + } + + @CanIgnoreReturnValue + public JmxScraperContainer withCustomYaml(String yamlPath) { + this.customYamlFiles.add(yamlPath); + return this; + } + + @Override + public void start() { + // for now only configure through JVM args + List arguments = new ArrayList<>(); + arguments.add("java"); + arguments.add("-Dotel.exporter.otlp.endpoint=" + endpoint); + + if (!targetSystems.isEmpty()) { + arguments.add("-Dotel.jmx.target.system=" + String.join(",", targetSystems)); + } + + if (serviceUrl == null) { + throw new IllegalStateException("Missing service URL"); + } + arguments.add("-Dotel.jmx.service.url=" + serviceUrl); + arguments.add("-Dotel.jmx.interval.milliseconds=" + intervalMillis); + + if (!customYamlFiles.isEmpty()) { + for (String yaml : customYamlFiles) { + this.withCopyFileToContainer(MountableFile.forClasspathResource(yaml), yaml); + } + arguments.add("-Dotel.jmx.config=" + String.join(",", customYamlFiles)); + } + + arguments.add("-jar"); + arguments.add("/scraper.jar"); + + this.withCommand(arguments.toArray(new String[0])); + + logger().info("Starting scraper with command: " + String.join(" ", arguments)); + + super.start(); + } +} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java new file mode 100644 index 000000000..a38dd7ace --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java @@ -0,0 +1,126 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.shaded.com.google.errorprone.annotations.CanIgnoreReturnValue; +import org.testcontainers.utility.MountableFile; + +/** Test container that allows to execute {@link TestApp} in an isolated container */ +public class TestAppContainer extends GenericContainer { + + private final Map properties; + private int port; + private String login; + private String pwd; + + public TestAppContainer() { + super("openjdk:8u272-jre-slim"); + + this.properties = new HashMap<>(); + + String appJar = System.getProperty("app.jar.path"); + assertThat(Paths.get(appJar)).isNotEmptyFile().isReadable(); + + this.withCopyFileToContainer(MountableFile.forHostPath(appJar), "/app.jar") + .waitingFor( + Wait.forLogMessage(TestApp.APP_STARTED_MSG + "\\n", 1) + .withStartupTimeout(Duration.ofSeconds(5))) + .withCommand("java", "-jar", "/app.jar"); + } + + @CanIgnoreReturnValue + public TestAppContainer withJmxPort(int port) { + this.port = port; + properties.put("com.sun.management.jmxremote.port", Integer.toString(port)); + return this.withExposedPorts(port); + } + + @CanIgnoreReturnValue + public TestAppContainer withUserAuth(String login, String pwd) { + this.login = login; + this.pwd = pwd; + return this; + } + + @Override + public void start() { + + // TODO: add support for ssl + properties.put("com.sun.management.jmxremote.ssl", "false"); + + if (pwd == null) { + properties.put("com.sun.management.jmxremote.authenticate", "false"); + } else { + properties.put("com.sun.management.jmxremote.authenticate", "true"); + + Path pwdFile = createPwdFile(login, pwd); + this.withCopyFileToContainer(MountableFile.forHostPath(pwdFile), "/jmx.password"); + properties.put("com.sun.management.jmxremote.password.file", "/jmx.password"); + + Path accessFile = createAccessFile(login); + this.withCopyFileToContainer(MountableFile.forHostPath(accessFile), "/jmx.access"); + properties.put("com.sun.management.jmxremote.access.file", "/jmx.access"); + } + + String confArgs = + properties.entrySet().stream() + .map( + e -> { + String s = "-D" + e.getKey(); + if (!e.getValue().isEmpty()) { + s += "=" + e.getValue(); + } + return s; + }) + .collect(Collectors.joining(" ")); + + this.withEnv("JAVA_TOOL_OPTIONS", confArgs); + + logger().info("Test application JAVA_TOOL_OPTIONS = " + confArgs); + + super.start(); + + logger().info("Test application JMX port mapped to {}:{}", getHost(), getMappedPort(port)); + } + + private static Path createPwdFile(String login, String pwd) { + try { + Path path = Files.createTempFile("test", ".pwd"); + writeLine(path, String.format("%s %s", login, pwd)); + return path; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Path createAccessFile(String login) { + try { + Path path = Files.createTempFile("test", ".pwd"); + writeLine(path, String.format("%s %s", login, "readwrite")); + return path; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void writeLine(Path path, String line) throws IOException { + line = line + "\n"; + Files.write(path, line.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java index 291fc9ac5..ec7b9ba61 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClientTest.java @@ -8,61 +8,35 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.contrib.jmxscraper.TestApp; -import java.io.Closeable; +import io.opentelemetry.contrib.jmxscraper.TestAppContainer; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Duration; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import javax.management.ObjectName; import javax.management.remote.JMXConnector; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.shaded.com.google.errorprone.annotations.CanIgnoreReturnValue; -import org.testcontainers.utility.MountableFile; public class JmxRemoteClientTest { - private static final Logger logger = LoggerFactory.getLogger(JmxRemoteClientTest.class); - private static Network network; - private static final List toClose = new ArrayList<>(); - @BeforeAll static void beforeAll() { network = Network.newNetwork(); - toClose.add(network); } @AfterAll static void afterAll() { - for (AutoCloseable item : toClose) { - try { - item.close(); - } catch (Exception e) { - logger.warn("Error closing " + item, e); - } - } + network.close(); } @Test void noAuth() { - try (AppContainer app = new AppContainer().withJmxPort(9990).start()) { - testConnector(() -> JmxRemoteClient.createNew(app.getHost(), app.getPort()).connect()); + try (TestAppContainer app = new TestAppContainer().withNetwork(network).withJmxPort(9990)) { + app.start(); + testConnector( + () -> JmxRemoteClient.createNew(app.getHost(), app.getMappedPort(9990)).connect()); } } @@ -70,10 +44,12 @@ void noAuth() { void loginPwdAuth() { String login = "user"; String pwd = "t0p!Secret"; - try (AppContainer app = new AppContainer().withJmxPort(9999).withUserAuth(login, pwd).start()) { + try (TestAppContainer app = + new TestAppContainer().withNetwork(network).withJmxPort(9999).withUserAuth(login, pwd)) { + app.start(); testConnector( () -> - JmxRemoteClient.createNew(app.getHost(), app.getPort()) + JmxRemoteClient.createNew(app.getHost(), app.getMappedPort(9999)) .userCredentials(login, pwd) .connect()); } @@ -115,126 +91,4 @@ private static void testConnector(ConnectorSupplier connectorSupplier) { private interface ConnectorSupplier { JMXConnector get() throws IOException; } - - private static class AppContainer implements Closeable { - - private final GenericContainer appContainer; - private final Map properties; - private int port; - private String login; - private String pwd; - - private AppContainer() { - this.properties = new HashMap<>(); - - properties.put("com.sun.management.jmxremote.ssl", "false"); // TODO : - - // SSL registry : com.sun.management.jmxremote.registry.ssl - // client side ssl auth: com.sun.management.jmxremote.ssl.need.client.auth - - String appJar = System.getProperty("app.jar.path"); - assertThat(Paths.get(appJar)).isNotEmptyFile().isReadable(); - - this.appContainer = - new GenericContainer<>("openjdk:8u272-jre-slim") - .withCopyFileToContainer(MountableFile.forHostPath(appJar), "/app.jar") - .withLogConsumer(new Slf4jLogConsumer(logger)) - .withNetwork(network) - .waitingFor( - Wait.forLogMessage(TestApp.APP_STARTED_MSG + "\\n", 1) - .withStartupTimeout(Duration.ofSeconds(5))) - .withCommand("java", "-jar", "/app.jar"); - } - - @CanIgnoreReturnValue - public AppContainer withJmxPort(int port) { - this.port = port; - properties.put("com.sun.management.jmxremote.port", Integer.toString(port)); - appContainer.withExposedPorts(port); - return this; - } - - @CanIgnoreReturnValue - public AppContainer withUserAuth(String login, String pwd) { - this.login = login; - this.pwd = pwd; - return this; - } - - @CanIgnoreReturnValue - AppContainer start() { - if (pwd == null) { - properties.put("com.sun.management.jmxremote.authenticate", "false"); - } else { - properties.put("com.sun.management.jmxremote.authenticate", "true"); - - Path pwdFile = createPwdFile(login, pwd); - appContainer.withCopyFileToContainer(MountableFile.forHostPath(pwdFile), "/jmx.password"); - properties.put("com.sun.management.jmxremote.password.file", "/jmx.password"); - - Path accessFile = createAccessFile(login); - appContainer.withCopyFileToContainer(MountableFile.forHostPath(accessFile), "/jmx.access"); - properties.put("com.sun.management.jmxremote.access.file", "/jmx.access"); - } - - String confArgs = - properties.entrySet().stream() - .map( - e -> { - String s = "-D" + e.getKey(); - if (!e.getValue().isEmpty()) { - s += "=" + e.getValue(); - } - return s; - }) - .collect(Collectors.joining(" ")); - - appContainer.withEnv("JAVA_TOOL_OPTIONS", confArgs).start(); - - logger.info("Test application JMX port mapped to {}:{}", getHost(), getPort()); - - toClose.add(this); - return this; - } - - int getPort() { - return appContainer.getMappedPort(port); - } - - String getHost() { - return appContainer.getHost(); - } - - @Override - public void close() { - if (appContainer.isRunning()) { - appContainer.stop(); - } - } - - private static Path createPwdFile(String login, String pwd) { - try { - Path path = Files.createTempFile("test", ".pwd"); - writeLine(path, String.format("%s %s", login, pwd)); - return path; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static Path createAccessFile(String login) { - try { - Path path = Files.createTempFile("test", ".pwd"); - writeLine(path, String.format("%s %s", login, "readwrite")); - return path; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static void writeLine(Path path, String line) throws IOException { - line = line + "\n"; - Files.write(path, line.getBytes(StandardCharsets.UTF_8)); - } - } } diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java new file mode 100644 index 000000000..d1972371f --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/JvmIntegrationTest.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.target_systems; + +import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; +import io.opentelemetry.contrib.jmxscraper.TestAppContainer; +import org.testcontainers.containers.GenericContainer; + +public class JvmIntegrationTest extends TargetSystemIntegrationTest { + + @Override + protected GenericContainer createTargetContainer(int jmxPort) { + // reusing test application for JVM metrics and custom yaml + return new TestAppContainer().withJmxPort(jmxPort); + } + + @Override + protected JmxScraperContainer customizeScraperContainer(JmxScraperContainer scraper) { + return scraper.withTargetSystem("jvm"); + } +} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java new file mode 100644 index 000000000..71754ea6f --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TargetSystemIntegrationTest.java @@ -0,0 +1,163 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.target_systems; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.server.grpc.GrpcService; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import io.grpc.stub.StreamObserver; +import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; +import io.opentelemetry.contrib.jmxscraper.client.JmxRemoteClient; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; +import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.LinkedBlockingDeque; +import javax.management.remote.JMXConnector; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.Testcontainers; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +public abstract class TargetSystemIntegrationTest { + + private static final Logger logger = LoggerFactory.getLogger(TargetSystemIntegrationTest.class); + private static final String TARGET_SYSTEM_NETWORK_ALIAS = "targetsystem"; + private static String otlpEndpoint; + + /** + * Create target system container + * + * @param jmxPort JMX port target JVM should listen to + * @return target system container + */ + protected abstract GenericContainer createTargetContainer(int jmxPort); + + private static Network network; + private static OtlpGrpcServer otlpServer; + private GenericContainer target; + private JmxScraperContainer scraper; + + private static final String OTLP_HOST = "host.testcontainers.internal"; + private static final int JMX_PORT = 9999; + + @BeforeAll + static void beforeAll() { + network = Network.newNetwork(); + otlpServer = new OtlpGrpcServer(); + otlpServer.start(); + Testcontainers.exposeHostPorts(otlpServer.httpPort()); + otlpEndpoint = "http://" + OTLP_HOST + ":" + otlpServer.httpPort(); + } + + @AfterAll + static void afterAll() { + network.close(); + try { + otlpServer.stop().get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + @AfterEach + void afterEach() { + if (target != null && target.isRunning()) { + target.stop(); + } + if (scraper != null && scraper.isRunning()) { + scraper.stop(); + } + if (otlpServer != null) { + otlpServer.reset(); + } + } + + @Test + void endToEndTest() { + + target = + createTargetContainer(JMX_PORT) + .withLogConsumer(new Slf4jLogConsumer(logger)) + .withNetwork(network) + .withExposedPorts(JMX_PORT) + .withNetworkAliases(TARGET_SYSTEM_NETWORK_ALIAS); + target.start(); + + String targetHost = target.getHost(); + Integer targetPort = target.getMappedPort(JMX_PORT); + logger.info( + "Target system started, JMX port: {} mapped to {}:{}", JMX_PORT, targetHost, targetPort); + + // TODO : wait for metrics to be sent and add assertions on what is being captured + // for now we just test that we can connect to remote JMX using our client. + try (JMXConnector connector = JmxRemoteClient.createNew(targetHost, targetPort).connect()) { + assertThat(connector.getMBeanServerConnection()).isNotNull(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + scraper = + new JmxScraperContainer(otlpEndpoint) + .withNetwork(network) + .withService(TARGET_SYSTEM_NETWORK_ALIAS, JMX_PORT); + + scraper = customizeScraperContainer(scraper); + scraper.start(); + + // TODO: replace with real assertions + assertThat(otlpServer.getMetrics()).isEmpty(); + } + + protected JmxScraperContainer customizeScraperContainer(JmxScraperContainer scraper) { + return scraper; + } + + private static class OtlpGrpcServer extends ServerExtension { + + private final BlockingQueue metricRequests = + new LinkedBlockingDeque<>(); + + List getMetrics() { + return new ArrayList<>(metricRequests); + } + + void reset() { + metricRequests.clear(); + } + + @Override + protected void configure(ServerBuilder sb) { + sb.service( + GrpcService.builder() + .addService( + new MetricsServiceGrpc.MetricsServiceImplBase() { + @Override + public void export( + ExportMetricsServiceRequest request, + StreamObserver responseObserver) { + metricRequests.add(request); + responseObserver.onNext(ExportMetricsServiceResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }) + .build()); + sb.http(0); + } + } +} diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java new file mode 100644 index 000000000..f6b9870c5 --- /dev/null +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/target_systems/TomcatIntegrationTest.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.jmxscraper.target_systems; + +import io.opentelemetry.contrib.jmxscraper.JmxScraperContainer; +import java.time.Duration; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.ImageFromDockerfile; + +public class TomcatIntegrationTest extends TargetSystemIntegrationTest { + + @Override + protected GenericContainer createTargetContainer(int jmxPort) { + return new GenericContainer<>( + new ImageFromDockerfile() + .withDockerfileFromBuilder( + builder -> + builder + .from("tomcat:9.0") + .run("rm", "-fr", "/usr/local/tomcat/webapps/ROOT") + .add( + "https://tomcat.apache.org/tomcat-9.0-doc/appdev/sample/sample.war", + "/usr/local/tomcat/webapps/ROOT.war") + .build())) + .withEnv("LOCAL_JMX", "no") + .withEnv( + "CATALINA_OPTS", + "-Dcom.sun.management.jmxremote.local.only=false" + + " -Dcom.sun.management.jmxremote.authenticate=false" + + " -Dcom.sun.management.jmxremote.ssl=false" + + " -Dcom.sun.management.jmxremote.port=" + + jmxPort + + " -Dcom.sun.management.jmxremote.rmi.port=" + + jmxPort) + .withStartupTimeout(Duration.ofMinutes(2)) + .waitingFor(Wait.forListeningPort()); + } + + @Override + protected JmxScraperContainer customizeScraperContainer(JmxScraperContainer scraper) { + return scraper.withTargetSystem("tomcat"); + } +} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java index 835eebd1e..9ab051d44 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java @@ -5,29 +5,30 @@ package io.opentelemetry.contrib.jmxscraper; +import io.opentelemetry.contrib.jmxscraper.client.JmxRemoteClient; import io.opentelemetry.contrib.jmxscraper.config.ConfigurationException; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfigFactory; -import io.opentelemetry.contrib.jmxscraper.jmx.JmxClient; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; -import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.Properties; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; import java.util.logging.Logger; +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; public class JmxScraper { private static final Logger logger = Logger.getLogger(JmxScraper.class.getName()); - private static final int EXECUTOR_TERMINATION_TIMEOUT_MS = 5000; - private final ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); - private final JmxScraperConfig config; + + private final JmxRemoteClient client; + + // TODO depend on instrumentation 2.9.0 snapshot + // private final JmxMetricInsight service; /** * Main method to create and run a {@link JmxScraper} instance. @@ -36,21 +37,13 @@ public class JmxScraper { */ @SuppressWarnings({"SystemOut", "SystemExitOutsideMain"}) public static void main(String[] args) { + JmxScraperConfig config; + JmxScraper jmxScraper = null; try { JmxScraperConfigFactory factory = new JmxScraperConfigFactory(); - JmxScraperConfig config = JmxScraper.createConfigFromArgs(Arrays.asList(args), factory); - - JmxScraper jmxScraper = new JmxScraper(config); - jmxScraper.start(); - - Runtime.getRuntime() - .addShutdownHook( - new Thread() { - @Override - public void run() { - jmxScraper.shutdown(); - } - }); + config = JmxScraper.createConfigFromArgs(Arrays.asList(args), factory); + jmxScraper = new JmxScraper(config); + } catch (ArgumentsParsingException e) { System.err.println( "Usage: java -jar " @@ -60,6 +53,13 @@ public void run() { System.err.println(e.getMessage()); System.exit(1); } + + try { + Objects.requireNonNull(jmxScraper).start(); + } catch (IOException e) { + System.err.println("Unable to connect " + e.getMessage()); + System.exit(2); + } } /** @@ -104,52 +104,35 @@ private static void loadPropertiesFromPath(Properties props, String path) } JmxScraper(JmxScraperConfig config) throws ConfigurationException { - this.config = config; - - try { - @SuppressWarnings("unused") // TODO: Temporary - JmxClient jmxClient = new JmxClient(config); - } catch (MalformedURLException e) { - throw new ConfigurationException("Malformed serviceUrl: ", e); + String serviceUrl = config.getServiceUrl(); + int interval = config.getIntervalMilliseconds(); + if (interval < 0) { + throw new ConfigurationException("interval must be positive"); } + this.client = JmxRemoteClient.createNew(serviceUrl); + // TODO: depend on instrumentation 2.9.0 snapshot + // this.service = JmxMetricInsight.createService(GlobalOpenTelemetry.get(), interval); } - @SuppressWarnings("FutureReturnValueIgnored") // TODO: Temporary - private void start() { - exec.scheduleWithFixedDelay( - () -> { - logger.fine("JMX scraping triggered"); - // try { - // runner.run(); - // } catch (Throwable e) { - // logger.log(Level.SEVERE, "Error gathering JMX metrics", e); - // } - }, - 0, - config.getIntervalMilliseconds(), - TimeUnit.MILLISECONDS); + private void start() throws IOException { + + JMXConnector connector = client.connect(); + + @SuppressWarnings("unused") + MBeanServerConnection connection = connector.getMBeanServerConnection(); + + // TODO: depend on instrumentation 2.9.0 snapshot + // MetricConfiguration metricConfig = new MetricConfiguration(); + // TODO create JMX insight config from scraper config + // service.startRemote(metricConfig, () -> Collections.singletonList(connection)); + logger.info("JMX scraping started"); - } - private void shutdown() { - logger.info("Shutting down JmxScraper and exporting final metrics."); - // Prevent new tasks to be submitted - exec.shutdown(); + // TODO: wait a bit to keep the JVM running, this won't be needed once calling jmx insight try { - // Wait a while for existing tasks to terminate - if (!exec.awaitTermination(EXECUTOR_TERMINATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { - // Cancel currently executing tasks - exec.shutdownNow(); - // Wait a while for tasks to respond to being cancelled - if (!exec.awaitTermination(EXECUTOR_TERMINATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { - logger.warning("Thread pool did not terminate in time: " + exec); - } - } + Thread.sleep(5000); } catch (InterruptedException e) { - // (Re-)Cancel if current thread also interrupted - exec.shutdownNow(); - // Preserve interrupt status - Thread.currentThread().interrupt(); + throw new IllegalStateException(e); } } } diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java index 449b05ba4..483f63b9d 100644 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java +++ b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/client/JmxRemoteClient.java @@ -11,10 +11,10 @@ import java.security.Provider; import java.security.Security; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; @@ -30,21 +30,23 @@ public class JmxRemoteClient { private static final Logger logger = Logger.getLogger(JmxRemoteClient.class.getName()); - private final String host; - private final int port; + private final JMXServiceURL url; @Nullable private String userName; @Nullable private String password; @Nullable private String profile; @Nullable private String realm; private boolean sslRegistry; - private JmxRemoteClient(@Nonnull String host, int port) { - this.host = host; - this.port = port; + private JmxRemoteClient(JMXServiceURL url) { + this.url = url; } public static JmxRemoteClient createNew(String host, int port) { - return new JmxRemoteClient(host, port); + return new JmxRemoteClient(buildUrl(host, port)); + } + + public static JmxRemoteClient createNew(String url) { + return new JmxRemoteClient(buildUrl(url)); } @CanIgnoreReturnValue @@ -84,6 +86,7 @@ public JMXConnector connect() throws IOException { try { // Not all supported versions of Java contain this Provider + // Also it might not be accessible due to java.security.sasl module not accessible Class klass = Class.forName("com.sun.security.sasl.Provider"); Provider provider = (Provider) klass.getDeclaredConstructor().newInstance(); Security.addProvider(provider); @@ -106,10 +109,9 @@ public JMXConnector connect() throws IOException { } }); } catch (ReflectiveOperationException e) { - logger.log(Level.WARNING, "SASL unsupported in current environment: " + e.getMessage(), e); + logger.log(Level.WARNING, "SASL unsupported in current environment: " + e.getMessage()); } - JMXServiceURL url = buildUrl(host, port); try { if (sslRegistry) { return doConnectSslRegistry(url, env); @@ -132,14 +134,14 @@ public JMXConnector doConnectSslRegistry(JMXServiceURL url, Map } private static JMXServiceURL buildUrl(String host, int port) { - StringBuilder sb = new StringBuilder("service:jmx:rmi:///jndi/rmi://"); - if (host != null) { - sb.append(host); - } - sb.append(":").append(port).append("/jmxrmi"); + return buildUrl( + String.format( + Locale.getDefault(), "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", host, port)); + } + private static JMXServiceURL buildUrl(String url) { try { - return new JMXServiceURL(sb.toString()); + return new JMXServiceURL(url); } catch (MalformedURLException e) { throw new IllegalArgumentException("invalid url", e); } diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/ClientCallbackHandler.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/ClientCallbackHandler.java deleted file mode 100644 index 2dfa01115..000000000 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/ClientCallbackHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.jmxscraper.jmx; - -import javax.annotation.Nullable; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.sasl.RealmCallback; - -public class ClientCallbackHandler implements CallbackHandler { - private final String username; - @Nullable private final char[] password; - private final String realm; - - /** - * Constructor for the {@link ClientCallbackHandler}, a CallbackHandler implementation for - * authenticating with an MBean server. - * - * @param username - authenticating username - * @param password - authenticating password (plaintext) - * @param realm - authenticating realm - */ - public ClientCallbackHandler(String username, String password, String realm) { - this.username = username; - this.password = password != null ? password.toCharArray() : null; - this.realm = realm; - } - - @Override - public void handle(Callback[] callbacks) throws UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (callback instanceof NameCallback) { - ((NameCallback) callback).setName(this.username); - } else if (callback instanceof PasswordCallback) { - ((PasswordCallback) callback).setPassword(this.password); - } else if (callback instanceof RealmCallback) { - ((RealmCallback) callback).setText(this.realm); - } else { - throw new UnsupportedCallbackException(callback); - } - } - } -} diff --git a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/JmxClient.java b/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/JmxClient.java deleted file mode 100644 index 0c71d9cc9..000000000 --- a/jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/jmx/JmxClient.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.contrib.jmxscraper.jmx; - -import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig; -import io.opentelemetry.contrib.jmxscraper.util.StringUtils; -import java.io.IOException; -import java.net.MalformedURLException; -import java.security.Provider; -import java.security.Security; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.Nullable; -import javax.management.MBeanServerConnection; -import javax.management.ObjectName; -import javax.management.remote.JMXConnector; -import javax.management.remote.JMXServiceURL; - -@SuppressWarnings("unused") // TODO: Temporary -public class JmxClient { - private static final Logger logger = Logger.getLogger(JmxClient.class.getName()); - - private final JMXServiceURL url; - private final String username; - private final String password; - private final String realm; - private final String remoteProfile; - private final boolean registrySsl; - @Nullable private JMXConnector jmxConn; - - public JmxClient(JmxScraperConfig config) throws MalformedURLException { - this.url = new JMXServiceURL(config.getServiceUrl()); - this.username = config.getUsername(); - this.password = config.getPassword(); - this.realm = config.getRealm(); - this.remoteProfile = config.getRemoteProfile(); - this.registrySsl = config.isRegistrySsl(); - } - - @Nullable - public MBeanServerConnection getConnection() { - if (jmxConn != null) { - try { - return jmxConn.getMBeanServerConnection(); - } catch (IOException e) { - // Attempt to connect with authentication below. - } - } - try { - @SuppressWarnings("ModifiedButNotUsed") // TODO: Temporary - Map env = new HashMap<>(); - if (!StringUtils.isBlank(username)) { - env.put(JMXConnector.CREDENTIALS, new String[] {this.username, this.password}); - } - try { - // Not all supported versions of Java contain this Provider - Class klass = Class.forName("com.sun.security.sasl.Provider"); - Provider provider = (Provider) klass.getDeclaredConstructor().newInstance(); - Security.addProvider(provider); - - env.put("jmx.remote.profile", this.remoteProfile); - env.put( - "jmx.remote.sasl.callback.handler", - new ClientCallbackHandler(this.username, this.password, this.realm)); - } catch (ReflectiveOperationException e) { - logger.warning("SASL unsupported in current environment: " + e.getMessage()); - } - - // jmxConn = JmxConnectorHelper.connect(url, env, registrySsl); - // return jmxConn.getMBeanServerConnection(); - return jmxConn == null ? null : jmxConn.getMBeanServerConnection(); // Temporary - - } catch (IOException e) { - logger.log(Level.WARNING, "Could not connect to remote JMX server: ", e); - return null; - } - } - - /** - * Query the MBean server for a given ObjectName. - * - * @param objectName ObjectName to query - * @return the sorted list of applicable ObjectName instances found by server - */ - public List query(ObjectName objectName) { - MBeanServerConnection mBeanServerConnection = getConnection(); - if (mBeanServerConnection == null) { - return Collections.emptyList(); - } - - try { - List objectNames = - new ArrayList<>(mBeanServerConnection.queryNames(objectName, null)); - Collections.sort(objectNames); - return Collections.unmodifiableList(objectNames); - } catch (IOException e) { - logger.log(Level.WARNING, "Could not query remote JMX server: ", e); - return Collections.emptyList(); - } - } -}