Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jmx-scraper test config sources #1750

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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)) {
Expand All @@ -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
Expand All @@ -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
//
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,6 +38,19 @@ public class JmxScraperContainer extends GenericContainer<JmxScraperContainer> {
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);
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -182,83 +199,185 @@ 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<String> arguments = new ArrayList<>();
arguments.add("java");
arguments.add("-Dotel.metrics.exporter=otlp");
arguments.add("-Dotel.exporter.otlp.endpoint=" + endpoint);

Map<String, String> 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<String> 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<String, String> env = new HashMap<>();
Map<String, String> 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)));
}

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<String> lines = new ArrayList<>();
lines.add("#!/bin/bash");
lines.add(String.join(" ", cmd) + "<<EOF");
lines.addAll(toKeyValueString(config));
lines.add("EOF");

Path script;
try {
script = Files.createTempFile("scraper", ".sh");
Files.write(script, lines);
} catch (IOException e) {
throw new IllegalStateException(e);
}

logger().info("Starting scraper with command: " + String.join(" ", arguments));
logger().info("Scraper executed with /scraper.sh shell script");
for (int i = 0; i < lines.size(); i++) {
logger().info("/scrapper.sh:{} {}", i, lines.get(i));
}

this.withCopyFileToContainer(MountableFile.forHostPath(script, 500), "/scraper.sh");
this.withCommand("/scraper.sh");
}

logger().info("Starting scraper with command: " + String.join(" ", this.getCommandParts()));
super.start();
}

private List<String> addSecureStore(TestKeyStore keyStore, boolean isKeyStore) {
private void addSecureStore(
TestKeyStore keyStore, boolean isKeyStore, Map<String, String> 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<String> toKeyValueString(Map<String, String> options) {
return options.entrySet().stream()
.map(e -> String.format("%s=%s", e.getKey(), e.getValue()))
.collect(Collectors.toList());
}
}
Loading