diff --git a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectionTest.java b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectionTest.java index 462af5087..cbcbb895b 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectionTest.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectionTest.java @@ -12,8 +12,11 @@ import java.util.function.Function; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; @@ -43,6 +46,9 @@ public class JmxConnectionTest { private static Network network; + // temporary folder for files that are copied to container + @TempDir private Path tempDir; + @BeforeAll static void beforeAll() { network = Network.newNetwork(); @@ -53,6 +59,12 @@ static void afterAll() { network.close(); } + @BeforeEach + void beforeEach() { + // extra safety to ensure temp folder is empty before each test method + assertThat(tempDir).isEmptyDirectory(); + } + @Test void connectionError() { try (JmxScraperContainer scraper = scraperContainer().withRmiServiceUrl("unknown_host", 1234)) { @@ -62,32 +74,43 @@ void connectionError() { } } - @Test - void connectNoAuth() { + @ParameterizedTest + @EnumSource + void connectNoAuth(JmxScraperContainer.ConfigSource configSource) { connectionTest( - app -> app.withJmxPort(JMX_PORT), scraper -> scraper.withRmiServiceUrl(APP_HOST, JMX_PORT)); + app -> app.withJmxPort(JMX_PORT), + scraper -> scraper.withRmiServiceUrl(APP_HOST, JMX_PORT).withConfigSource(configSource)); } - @Test - void userPassword() { + @ParameterizedTest + @EnumSource + void userPassword(JmxScraperContainer.ConfigSource configSource) { String login = "user"; String pwd = "t0p!Secret"; connectionTest( app -> app.withJmxPort(JMX_PORT).withUserAuth(login, pwd), - scraper -> scraper.withRmiServiceUrl(APP_HOST, JMX_PORT).withUser(login).withPassword(pwd)); + scraper -> + scraper + .withRmiServiceUrl(APP_HOST, JMX_PORT) + .withUser(login) + .withPassword(pwd) + .withConfigSource(configSource)); } - @Test - void serverSsl(@TempDir Path tempDir) { - testServerSsl(tempDir, /* sslRmiRegistry= */ false); + @ParameterizedTest + @EnumSource + void serverSsl(JmxScraperContainer.ConfigSource configSource) { + testServerSsl(/* sslRmiRegistry= */ false, configSource); } - @Test - void serverSslWithSslRmiRegistry(@TempDir Path tempDir) { - testServerSsl(tempDir, /* sslRmiRegistry= */ true); + @ParameterizedTest + @EnumSource + void serverSslWithSslRmiRegistry(JmxScraperContainer.ConfigSource configSource) { + testServerSsl(/* sslRmiRegistry= */ true, configSource); } - private static void testServerSsl(Path tempDir, boolean sslRmiRegistry) { + private void testServerSsl( + boolean sslRmiRegistry, JmxScraperContainer.ConfigSource configSource) { // two keystores: // server keystore with public/private key pair // client trust store with certificate from server @@ -109,11 +132,13 @@ private static void testServerSsl(Path tempDir, boolean sslRmiRegistry) { scraper -> (sslRmiRegistry ? scraper.withSslRmiRegistry() : scraper) .withRmiServiceUrl(APP_HOST, JMX_PORT) - .withTrustStore(clientTrustStore)); + .withTrustStore(clientTrustStore) + .withConfigSource(configSource)); } - @Test - void serverSslClientSsl(@TempDir Path tempDir) { + @ParameterizedTest + @EnumSource(value = JmxScraperContainer.ConfigSource.class) + void serverSslClientSsl(JmxScraperContainer.ConfigSource configSource) { // Note: this could have been made simpler by relying on the fact that keystore could be used // as a trust store, but having clear split provides also some extra clarity // @@ -152,7 +177,8 @@ void serverSslClientSsl(@TempDir Path tempDir) { scraper .withRmiServiceUrl(APP_HOST, JMX_PORT) .withKeyStore(clientKeyStore) - .withTrustStore(clientTrustStore)); + .withTrustStore(clientTrustStore) + .withConfigSource(configSource)); } private static void connectionTest( 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 index 12e1dc487..72e702d98 100644 --- a/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java +++ b/jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java @@ -8,15 +8,18 @@ import static org.assertj.core.api.Assertions.assertThat; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.MountableFile; @@ -35,6 +38,19 @@ public class JmxScraperContainer extends GenericContainer { private TestKeyStore keyStore; private TestKeyStore trustStore; private boolean sslRmiRegistry; + private ConfigSource configSource; + + /** Defines different strategies to provide scraper configuration */ + public enum ConfigSource { + /** system properties with "-D" prefix in JVM command */ + SYSTEM_PROPERTIES, + /** properties file */ + PROPERTIES_FILE, + /** standard input */ + STDIN, + /** environment variables with "OTEL_" prefix, non-otel options as system properties */ + ENVIRONMENT_VARIABLES; + } public JmxScraperContainer(String otlpEndpoint, String baseImage) { super(baseImage); @@ -48,6 +64,7 @@ public JmxScraperContainer(String otlpEndpoint, String baseImage) { this.targetSystems = new HashSet<>(); this.customYamlFiles = new HashSet<>(); this.extraJars = new ArrayList<>(); + this.configSource = ConfigSource.SYSTEM_PROPERTIES; } /** @@ -182,59 +199,129 @@ public JmxScraperContainer withSslRmiRegistry() { return this; } + /** + * Sets how configuration is provided to scraper + * + * @param source configuration source + * @return this + */ + @CanIgnoreReturnValue + public JmxScraperContainer withConfigSource(ConfigSource source) { + this.configSource = source; + return this; + } + @Override public void start() { - // for now only configure through JVM args - List arguments = new ArrayList<>(); - arguments.add("java"); - arguments.add("-Dotel.metrics.exporter=otlp"); - arguments.add("-Dotel.exporter.otlp.endpoint=" + endpoint); + + Map config = new HashMap<>(); + config.put("otel.metrics.exporter", "otlp"); + config.put("otel.exporter.otlp.endpoint", endpoint); if (!targetSystems.isEmpty()) { - arguments.add("-Dotel.jmx.target.system=" + String.join(",", targetSystems)); + config.put("otel.jmx.target.system", String.join(",", targetSystems)); } if (serviceUrl == null) { throw new IllegalStateException("Missing service URL"); } - arguments.add("-Dotel.jmx.service.url=" + serviceUrl); + config.put("otel.jmx.service.url", serviceUrl); + // always use a very short export interval for testing - arguments.add("-Dotel.metric.export.interval=1s"); + config.put("otel.metric.export.interval", "1s"); if (user != null) { - arguments.add("-Dotel.jmx.username=" + user); + config.put("otel.jmx.username", user); } if (password != null) { - arguments.add("-Dotel.jmx.password=" + password); + config.put("otel.jmx.password", password); } - arguments.addAll(addSecureStore(keyStore, /* isKeyStore= */ true)); - arguments.addAll(addSecureStore(trustStore, /* isKeyStore= */ false)); + addSecureStore(keyStore, /* isKeyStore= */ true, config); + addSecureStore(trustStore, /* isKeyStore= */ false, config); if (sslRmiRegistry) { - arguments.add("-Dotel.jmx.remote.registry.ssl=true"); + config.put("otel.jmx.remote.registry.ssl", "true"); } if (!customYamlFiles.isEmpty()) { for (String yaml : customYamlFiles) { this.withCopyFileToContainer(MountableFile.forClasspathResource(yaml), yaml); } - arguments.add("-Dotel.jmx.config=" + String.join(",", customYamlFiles)); + config.put("otel.jmx.config", String.join(",", customYamlFiles)); + } + + List cmd = new ArrayList<>(); + cmd.add("java"); + + switch (configSource) { + case SYSTEM_PROPERTIES: + cmd.addAll( + toKeyValueString(config).stream().map(s -> "-D" + s).collect(Collectors.toList())); + break; + case PROPERTIES_FILE: + try { + Path configFile = Files.createTempFile("config", ".properties"); + Files.write(configFile, toKeyValueString(config)); + this.withCopyFileToContainer(MountableFile.forHostPath(configFile), "/config.properties"); + } catch (IOException e) { + throw new IllegalStateException(e); + } + break; + case STDIN: + // nothing needed here + break; + case ENVIRONMENT_VARIABLES: + Map env = new HashMap<>(); + Map other = new HashMap<>(); + config.forEach( + (k, v) -> { + if (k.startsWith("otel.")) { + env.put(k.toUpperCase(Locale.ROOT).replace(".", "_"), v); + } else { + other.put(k, v); + } + }); + + if (!other.isEmpty()) { + env.put( + "JAVA_TOOL_OPTIONS", + toKeyValueString(other).stream().map(s -> "-D" + s).collect(Collectors.joining(" "))); + } + this.withEnv(env); + env.forEach((k, v) -> logger().info("Using environment variable {} = {} ", k, v)); + + break; } if (extraJars.isEmpty()) { // using "java -jar" to start - arguments.add("-jar"); - arguments.add("/scraper.jar"); + cmd.add("-jar"); + cmd.add("/scraper.jar"); } else { // using "java -cp" to start - arguments.add("-cp"); - arguments.add("/scraper.jar:" + String.join(":", extraJars)); - arguments.add("io.opentelemetry.contrib.jmxscraper.JmxScraper"); + cmd.add("-cp"); + cmd.add("/scraper.jar:" + String.join(":", extraJars)); + cmd.add("io.opentelemetry.contrib.jmxscraper.JmxScraper"); + } + + switch (configSource) { + case SYSTEM_PROPERTIES: + case ENVIRONMENT_VARIABLES: + // no extra program argument needed + break; + case PROPERTIES_FILE: + cmd.add("-config"); + cmd.add("/config.properties"); + break; + case STDIN: + cmd.add("-config"); + cmd.add("-"); + break; } if (testJmx) { - arguments.add("-test"); + cmd.add("-test"); this.waitingFor(Wait.forLogMessage(".*JMX connection test.*", 1)); } else { this.waitingFor( @@ -242,23 +329,55 @@ public void start() { .withStartupTimeout(Duration.ofSeconds(10))); } - this.withCommand(arguments.toArray(new String[0])); + if (configSource != ConfigSource.STDIN) { + this.withCommand(cmd.toArray(new String[0])); + } else { + // generate shell script to feed standard input with config + List lines = new ArrayList<>(); + lines.add("#!/bin/bash"); + lines.add(String.join(" ", cmd) + "< addSecureStore(TestKeyStore keyStore, boolean isKeyStore) { + private void addSecureStore( + TestKeyStore keyStore, boolean isKeyStore, Map config) { if (keyStore == null) { - return Collections.emptyList(); + return; } Path path = keyStore.getPath(); String containerPath = "/" + path.getFileName().toString(); this.withCopyFileToContainer(MountableFile.forHostPath(path), containerPath); - String prefix = String.format("-Djavax.net.ssl.%sStore", isKeyStore ? "key" : "trust"); - return Arrays.asList( - prefix + "=" + containerPath, prefix + "Password=" + keyStore.getPassword()); + String prefix = String.format("javax.net.ssl.%sStore", isKeyStore ? "key" : "trust"); + + config.put(prefix, containerPath); + config.put(prefix + "Password", keyStore.getPassword()); + } + + private static List toKeyValueString(Map options) { + return options.entrySet().stream() + .map(e -> String.format("%s=%s", e.getKey(), e.getValue())) + .collect(Collectors.toList()); } }