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 9c813398b..d1431647b 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 @@ -15,6 +15,8 @@ import org.junit.jupiter.api.BeforeAll; 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; @@ -44,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(); @@ -63,32 +68,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 @@ -110,11 +126,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 // @@ -153,7 +171,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 29dedd7b6..8d06c0d7a 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,19 @@ 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 javax.annotation.Nullable; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.MountableFile; @@ -26,7 +30,7 @@ public class JmxScraperContainer extends GenericContainer { private final String endpoint; private final Set targetSystems; - private String targetSystemSource; + @Nullable private String targetSystemSource; private String serviceUrl; private final Set customYamlFiles; private String user; @@ -36,6 +40,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); @@ -47,9 +64,9 @@ public JmxScraperContainer(String otlpEndpoint, String baseImage) { this.endpoint = otlpEndpoint; this.targetSystems = new HashSet<>(); - this.targetSystemSource = "auto"; this.customYamlFiles = new HashSet<>(); this.extraJars = new ArrayList<>(); + this.configSource = ConfigSource.SYSTEM_PROPERTIES; } /** @@ -196,84 +213,205 @@ 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); - if (!targetSystems.isEmpty()) { - arguments.add("-Dotel.jmx.target.system=" + String.join(",", targetSystems)); - arguments.add("-Dotel.jmx.target.source=" + targetSystemSource); - } + Map config = initConfig(); - if (serviceUrl == null) { - throw new IllegalStateException("Missing service URL"); - } - arguments.add("-Dotel.jmx.service.url=" + serviceUrl); - // always use a very short export interval for testing - arguments.add("-Dotel.metric.export.interval=1s"); + List cmd = createCommand(config); - if (user != null) { - arguments.add("-Dotel.jmx.username=" + user); - } - if (password != null) { - arguments.add("-Dotel.jmx.password=" + password); + if (configSource != ConfigSource.STDIN) { + this.withCommand(cmd.toArray(new String[0])); + } else { + Path script = generateShellScript(cmd, config); + + this.withCopyFileToContainer(MountableFile.forHostPath(script, 500), "/scraper.sh"); + this.withCommand("/scraper.sh"); } - arguments.addAll(addSecureStore(keyStore, /* isKeyStore= */ true)); - arguments.addAll(addSecureStore(trustStore, /* isKeyStore= */ false)); + logger().info("Starting scraper with command: " + String.join(" ", this.getCommandParts())); + super.start(); + } - if (sslRmiRegistry) { - arguments.add("-Dotel.jmx.remote.registry.ssl=true"); + private Path generateShellScript(List cmd, Map config) { + // generate shell script to feed standard input with config + List lines = new ArrayList<>(); + lines.add("#!/bin/bash"); + lines.add(String.join(" ", cmd) + "< createCommand(Map config) { + 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( Wait.forLogMessage(".*JMX scraping started.*", 1) .withStartupTimeout(Duration.ofSeconds(10))); } + return cmd; + } - this.withCommand(arguments.toArray(new String[0])); + private Map initConfig() { + Map config = new HashMap<>(); + config.put("otel.metrics.exporter", "otlp"); + config.put("otel.exporter.otlp.endpoint", endpoint); - logger().info("Starting scraper with command: " + String.join(" ", arguments)); + if (!targetSystems.isEmpty()) { + config.put("otel.jmx.target.system", String.join(",", targetSystems)); - super.start(); + // rely on default when explicitly set + if (targetSystemSource != null) { + config.put("otel.jmx.target.source", targetSystemSource); + } + } + + if (serviceUrl == null) { + throw new IllegalStateException("Missing service URL"); + } + config.put("otel.jmx.service.url", serviceUrl); + + // always use a very short export interval for testing + config.put("otel.metric.export.interval", "1s"); + + if (user != null) { + config.put("otel.jmx.username", user); + } + if (password != null) { + config.put("otel.jmx.password", password); + } + + addSecureStore(keyStore, /* isKeyStore= */ true, config); + addSecureStore(trustStore, /* isKeyStore= */ false, config); + + if (sslRmiRegistry) { + config.put("otel.jmx.remote.registry.ssl", "true"); + } + + if (!customYamlFiles.isEmpty()) { + for (String yaml : customYamlFiles) { + this.withCopyFileToContainer(MountableFile.forClasspathResource(yaml), yaml); + } + config.put("otel.jmx.config", String.join(",", customYamlFiles)); + } + return config; } - private List 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()); } }