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 connection test #1684

Merged
merged 10 commits into from
Feb 10, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* 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.util.function.Function;
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;

/**
* Tests all supported ways to connect to remote JMX interface. This indirectly tests
* JmxConnectionBuilder and relies on containers to minimize the JMX/RMI network complications which
* are not NAT-friendly.
*/
public class JmxConnectionTest {

// OTLP endpoint is not used in test mode, but still has to be provided
private static final String DUMMY_OTLP_ENDPOINT = "http://dummy-otlp-endpoint:8080/";
private static final String SCRAPER_BASE_IMAGE = "openjdk:8u342-jre-slim";

private static final int JMX_PORT = 9999;
private static final String APP_HOST = "app";

private static final Logger jmxScraperLogger = LoggerFactory.getLogger("JmxScraperContainer");
private static final Logger appLogger = LoggerFactory.getLogger("TestAppContainer");

private static Network network;

@BeforeAll
static void beforeAll() {
network = Network.newNetwork();
}

@AfterAll
static void afterAll() {
network.close();
}

@Test
void connectionError() {
try (JmxScraperContainer scraper = scraperContainer().withRmiServiceUrl("unknown_host", 1234)) {
scraper.start();
waitTerminated(scraper);
checkConnectionLogs(scraper, /* expectedOk= */ false);
}
}

@Test
void connectNoAuth() {
connectionTest(
app -> app.withJmxPort(JMX_PORT), scraper -> scraper.withRmiServiceUrl(APP_HOST, JMX_PORT));
}

@Test
void userPassword() {
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));
}

private static void connectionTest(
Function<TestAppContainer, TestAppContainer> customizeApp,
Function<JmxScraperContainer, JmxScraperContainer> customizeScraper) {
try (TestAppContainer app = customizeApp.apply(appContainer())) {
app.start();
try (JmxScraperContainer scraper = customizeScraper.apply(scraperContainer())) {
scraper.start();
waitTerminated(scraper);
checkConnectionLogs(scraper, /* expectedOk= */ true);
}
}
}

private static void checkConnectionLogs(JmxScraperContainer scraper, boolean expectedOk) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[for reviewer] we have to parse and check scraper logs in order to know if the connection is OK or not, this is because we can't access the process exit value when the program completes.


String[] logLines = scraper.getLogs().split("\n");
String lastLine = logLines[logLines.length - 1];

if (expectedOk) {
assertThat(lastLine)
.describedAs("should log connection success")
.endsWith("JMX connection test OK");
} else {
assertThat(lastLine)
.describedAs("should log connection failure")
.endsWith("JMX connection test ERROR");
}
}

private static void waitTerminated(GenericContainer<?> container) {
int retries = 10;
while (retries > 0 && container.isRunning()) {
retries--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
assertThat(retries)
.describedAs("container should stop when testing connection")
.isNotEqualTo(0);
}

private static JmxScraperContainer scraperContainer() {
return new JmxScraperContainer(DUMMY_OTLP_ENDPOINT, SCRAPER_BASE_IMAGE)
.withLogConsumer(new Slf4jLogConsumer(jmxScraperLogger))
.withNetwork(network)
// mandatory to have a target system even if we don't collect metrics
.withTargetSystem("jvm")
// we are only testing JMX connection here
.withTestJmx();
}

private static TestAppContainer appContainer() {
return new TestAppContainer()
.withLogConsumer(new Slf4jLogConsumer(appLogger))
.withNetwork(network)
.withNetworkAliases(APP_HOST);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,15 @@ public class JmxScraperContainer extends GenericContainer<JmxScraperContainer> {
private String user;
private String password;
private final List<String> extraJars;
private boolean testJmx;

public JmxScraperContainer(String otlpEndpoint, String baseImage) {
super(baseImage);

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.withCopyFileToContainer(MountableFile.forHostPath(scraperJarPath), "/scraper.jar");

this.endpoint = otlpEndpoint;
this.targetSystems = new HashSet<>();
Expand Down Expand Up @@ -108,6 +106,12 @@ public JmxScraperContainer withCustomYaml(String yamlPath) {
return this;
}

@CanIgnoreReturnValue
public JmxScraperContainer withTestJmx() {
this.testJmx = true;
return this;
}

@Override
public void start() {
// for now only configure through JVM args
Expand Down Expand Up @@ -152,6 +156,15 @@ public void start() {
arguments.add("io.opentelemetry.contrib.jmxscraper.JmxScraper");
}

if (testJmx) {
arguments.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]));

logger().info("Starting scraper with command: " + String.join(" ", arguments));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,6 @@ public TestAppContainer withUserAuth(String login, String pwd) {
return this;
}

/**
* Configures app container for host-to-container access, port will be used as-is from host to
* work-around JMX in docker. This is optional on Linux as there is a network route and the
* container is accessible, but not on Mac where the container runs in an isolated VM.
*
* @param port port to use, must be available on host.
* @return this
*/
@CanIgnoreReturnValue
public TestAppContainer withHostAccessFixedJmxPort(int port) {
// To get host->container JMX connection working docker must expose JMX/RMI port under the same
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[for reviewer] this is no longer necessary as we only rely on container-to-container communication and we don't have to use docker NAT to access the JMX endpoint.

// port number. Because of this testcontainers' standard exposed port randomization approach
// can't be used.
// Explanation:
// https://forums.docker.com/t/exposing-mapped-jmx-ports-from-multiple-containers/5287/6
properties.put("com.sun.management.jmxremote.port", Integer.toString(port));
properties.put("com.sun.management.jmxremote.rmi.port", Integer.toString(port));
properties.put("java.rmi.server.hostname", getHost());
addFixedExposedPort(port, port);
return this;
}

@Override
public void start() {
// TODO: add support for ssl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand All @@ -34,6 +35,7 @@
public class JmxScraper {
private static final Logger logger = Logger.getLogger(JmxScraper.class.getName());
private static final String CONFIG_ARG = "-config";
private static final String TEST_ARG = "-test";

private final JmxConnectorBuilder client;
private final JmxMetricInsight service;
Expand All @@ -52,8 +54,11 @@ public static void main(String[] args) {
// set log format
System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tF %1$tT %4$s %5$s%n");

List<String> effectiveArgs = new ArrayList<>(Arrays.asList(args));
boolean testMode = effectiveArgs.remove(TEST_ARG);

try {
Properties argsConfig = parseArgs(Arrays.asList(args));
Properties argsConfig = parseArgsConfig(effectiveArgs);
propagateToSystemProperties(argsConfig);

// auto-configure and register SDK
Expand All @@ -78,16 +83,21 @@ public static void main(String[] args) {
Optional.ofNullable(scraperConfig.getUsername()).ifPresent(connectorBuilder::withUser);
Optional.ofNullable(scraperConfig.getPassword()).ifPresent(connectorBuilder::withPassword);

JmxScraper jmxScraper = new JmxScraper(connectorBuilder, service, scraperConfig);
jmxScraper.start();
if (testMode) {
System.exit(testConnection(connectorBuilder) ? 0 : 1);
} else {
JmxScraper jmxScraper = new JmxScraper(connectorBuilder, service, scraperConfig);
jmxScraper.start();
}
} catch (ConfigurationException e) {
logger.log(Level.SEVERE, "invalid configuration ", e);
System.exit(1);
} catch (InvalidArgumentException e) {
logger.log(Level.SEVERE, "invalid configuration provided through arguments", e);
logger.info("Usage: java -jar <path_to_jmxscraper.jar> [-test] [-config <conf>]");
logger.info(" -test test JMX connection with provided configuration and exit");
logger.info(
"Usage: java -jar <path_to_jmxscraper.jar> "
+ "-config <path_to_config.properties or - for stdin>");
" -config <conf> provide configuration, where <conf> is - for stdin, or <path_to_config.properties>");
System.exit(1);
} catch (IOException e) {
logger.log(Level.SEVERE, "Unable to connect ", e);
Expand All @@ -98,6 +108,24 @@ public static void main(String[] args) {
}
}

private static boolean testConnection(JmxConnectorBuilder connectorBuilder) {
try (JMXConnector connector = connectorBuilder.build()) {

MBeanServerConnection connection = connector.getMBeanServerConnection();
Integer mbeanCount = connection.getMBeanCount();
if (mbeanCount > 0) {
logger.log(Level.INFO, "JMX connection test OK");
return true;
} else {
logger.log(Level.SEVERE, "JMX connection test ERROR");
return false;
}
} catch (IOException e) {
logger.log(Level.SEVERE, "JMX connection test ERROR", e);
return false;
}
}

// package private for testing
static void propagateToSystemProperties(Properties properties) {
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
Expand All @@ -116,7 +144,7 @@ static void propagateToSystemProperties(Properties properties) {
*
* @param args application commandline arguments
*/
static Properties parseArgs(List<String> args) throws InvalidArgumentException {
static Properties parseArgsConfig(List<String> args) throws InvalidArgumentException {

if (args.isEmpty()) {
// empty properties from stdin or external file
Expand Down
Loading