Skip to content

Commit 4ed5a14

Browse files
authored
jmx scraper connection test (#1684)
1 parent e0db18e commit 4ed5a14

File tree

7 files changed

+205
-138
lines changed

7 files changed

+205
-138
lines changed

jmx-scraper/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,19 @@ be set through the standard `JAVA_TOOL_OPTIONS` environment variable using the `
8181

8282
## Troubleshooting
8383

84+
### Exported metrics
85+
8486
In order to investigate when and what metrics are being captured and sent, setting the `otel.metrics.exporter`
85-
configuration option to include `logging` exporter provides log messages when metrics are being exported.
87+
configuration option to include `logging` exporter provides log messages when metrics are being exported
88+
89+
### JMX connection test
90+
91+
Connection to the remote JVM through the JMX can be tested by adding the `-test` argument.
92+
When doing so, the JMX Scraper will only test the connection to the remote JVM with provided configuration
93+
and exit.
94+
95+
- Connection OK: `JMX connection test OK` message is written to standard output and exit status = `0`
96+
- Connection ERROR: `JMX connection test ERROR` message is written to standard output and exit status = `1`
8697

8798
## Extra libraries in classpath
8899

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jmxscraper;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
import java.util.function.Function;
11+
import org.junit.jupiter.api.AfterAll;
12+
import org.junit.jupiter.api.BeforeAll;
13+
import org.junit.jupiter.api.Test;
14+
import org.slf4j.Logger;
15+
import org.slf4j.LoggerFactory;
16+
import org.testcontainers.containers.GenericContainer;
17+
import org.testcontainers.containers.Network;
18+
import org.testcontainers.containers.output.Slf4jLogConsumer;
19+
20+
/**
21+
* Tests all supported ways to connect to remote JMX interface. This indirectly tests
22+
* JmxConnectionBuilder and relies on containers to minimize the JMX/RMI network complications which
23+
* are not NAT-friendly.
24+
*/
25+
public class JmxConnectionTest {
26+
27+
// OTLP endpoint is not used in test mode, but still has to be provided
28+
private static final String DUMMY_OTLP_ENDPOINT = "http://dummy-otlp-endpoint:8080/";
29+
private static final String SCRAPER_BASE_IMAGE = "openjdk:8u342-jre-slim";
30+
31+
private static final int JMX_PORT = 9999;
32+
private static final String APP_HOST = "app";
33+
34+
private static final Logger jmxScraperLogger = LoggerFactory.getLogger("JmxScraperContainer");
35+
private static final Logger appLogger = LoggerFactory.getLogger("TestAppContainer");
36+
37+
private static Network network;
38+
39+
@BeforeAll
40+
static void beforeAll() {
41+
network = Network.newNetwork();
42+
}
43+
44+
@AfterAll
45+
static void afterAll() {
46+
network.close();
47+
}
48+
49+
@Test
50+
void connectionError() {
51+
try (JmxScraperContainer scraper = scraperContainer().withRmiServiceUrl("unknown_host", 1234)) {
52+
scraper.start();
53+
waitTerminated(scraper);
54+
checkConnectionLogs(scraper, /* expectedOk= */ false);
55+
}
56+
}
57+
58+
@Test
59+
void connectNoAuth() {
60+
connectionTest(
61+
app -> app.withJmxPort(JMX_PORT), scraper -> scraper.withRmiServiceUrl(APP_HOST, JMX_PORT));
62+
}
63+
64+
@Test
65+
void userPassword() {
66+
String login = "user";
67+
String pwd = "t0p!Secret";
68+
connectionTest(
69+
app -> app.withJmxPort(JMX_PORT).withUserAuth(login, pwd),
70+
scraper -> scraper.withRmiServiceUrl(APP_HOST, JMX_PORT).withUser(login).withPassword(pwd));
71+
}
72+
73+
private static void connectionTest(
74+
Function<TestAppContainer, TestAppContainer> customizeApp,
75+
Function<JmxScraperContainer, JmxScraperContainer> customizeScraper) {
76+
try (TestAppContainer app = customizeApp.apply(appContainer())) {
77+
app.start();
78+
try (JmxScraperContainer scraper = customizeScraper.apply(scraperContainer())) {
79+
scraper.start();
80+
waitTerminated(scraper);
81+
checkConnectionLogs(scraper, /* expectedOk= */ true);
82+
}
83+
}
84+
}
85+
86+
private static void checkConnectionLogs(JmxScraperContainer scraper, boolean expectedOk) {
87+
88+
String[] logLines = scraper.getLogs().split("\n");
89+
String lastLine = logLines[logLines.length - 1];
90+
91+
if (expectedOk) {
92+
assertThat(lastLine)
93+
.describedAs("should log connection success")
94+
.endsWith("JMX connection test OK");
95+
} else {
96+
assertThat(lastLine)
97+
.describedAs("should log connection failure")
98+
.endsWith("JMX connection test ERROR");
99+
}
100+
}
101+
102+
private static void waitTerminated(GenericContainer<?> container) {
103+
int retries = 10;
104+
while (retries > 0 && container.isRunning()) {
105+
retries--;
106+
try {
107+
Thread.sleep(100);
108+
} catch (InterruptedException e) {
109+
throw new RuntimeException(e);
110+
}
111+
}
112+
assertThat(retries)
113+
.describedAs("container should stop when testing connection")
114+
.isNotEqualTo(0);
115+
}
116+
117+
private static JmxScraperContainer scraperContainer() {
118+
return new JmxScraperContainer(DUMMY_OTLP_ENDPOINT, SCRAPER_BASE_IMAGE)
119+
.withLogConsumer(new Slf4jLogConsumer(jmxScraperLogger))
120+
.withNetwork(network)
121+
// mandatory to have a target system even if we don't collect metrics
122+
.withTargetSystem("jvm")
123+
// we are only testing JMX connection here
124+
.withTestJmx();
125+
}
126+
127+
private static TestAppContainer appContainer() {
128+
return new TestAppContainer()
129+
.withLogConsumer(new Slf4jLogConsumer(appLogger))
130+
.withNetwork(network)
131+
.withNetworkAliases(APP_HOST);
132+
}
133+
}

jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxConnectorBuilderTest.java

Lines changed: 0 additions & 95 deletions
This file was deleted.

jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/JmxScraperContainer.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,15 @@ public class JmxScraperContainer extends GenericContainer<JmxScraperContainer> {
2828
private String user;
2929
private String password;
3030
private final List<String> extraJars;
31+
private boolean testJmx;
3132

3233
public JmxScraperContainer(String otlpEndpoint, String baseImage) {
3334
super(baseImage);
3435

3536
String scraperJarPath = System.getProperty("shadow.jar.path");
3637
assertThat(scraperJarPath).isNotNull();
3738

38-
this.withCopyFileToContainer(MountableFile.forHostPath(scraperJarPath), "/scraper.jar")
39-
.waitingFor(
40-
Wait.forLogMessage(".*JMX scraping started.*", 1)
41-
.withStartupTimeout(Duration.ofSeconds(10)));
39+
this.withCopyFileToContainer(MountableFile.forHostPath(scraperJarPath), "/scraper.jar");
4240

4341
this.endpoint = otlpEndpoint;
4442
this.targetSystems = new HashSet<>();
@@ -108,6 +106,12 @@ public JmxScraperContainer withCustomYaml(String yamlPath) {
108106
return this;
109107
}
110108

109+
@CanIgnoreReturnValue
110+
public JmxScraperContainer withTestJmx() {
111+
this.testJmx = true;
112+
return this;
113+
}
114+
111115
@Override
112116
public void start() {
113117
// for now only configure through JVM args
@@ -152,6 +156,15 @@ public void start() {
152156
arguments.add("io.opentelemetry.contrib.jmxscraper.JmxScraper");
153157
}
154158

159+
if (testJmx) {
160+
arguments.add("-test");
161+
this.waitingFor(Wait.forLogMessage(".*JMX connection test.*", 1));
162+
} else {
163+
this.waitingFor(
164+
Wait.forLogMessage(".*JMX scraping started.*", 1)
165+
.withStartupTimeout(Duration.ofSeconds(10)));
166+
}
167+
155168
this.withCommand(arguments.toArray(new String[0]));
156169

157170
logger().info("Starting scraper with command: " + String.join(" ", arguments));

jmx-scraper/src/integrationTest/java/io/opentelemetry/contrib/jmxscraper/TestAppContainer.java

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,6 @@ public TestAppContainer withUserAuth(String login, String pwd) {
6161
return this;
6262
}
6363

64-
/**
65-
* Configures app container for host-to-container access, port will be used as-is from host to
66-
* work-around JMX in docker. This is optional on Linux as there is a network route and the
67-
* container is accessible, but not on Mac where the container runs in an isolated VM.
68-
*
69-
* @param port port to use, must be available on host.
70-
* @return this
71-
*/
72-
@CanIgnoreReturnValue
73-
public TestAppContainer withHostAccessFixedJmxPort(int port) {
74-
// To get host->container JMX connection working docker must expose JMX/RMI port under the same
75-
// port number. Because of this testcontainers' standard exposed port randomization approach
76-
// can't be used.
77-
// Explanation:
78-
// https://forums.docker.com/t/exposing-mapped-jmx-ports-from-multiple-containers/5287/6
79-
properties.put("com.sun.management.jmxremote.port", Integer.toString(port));
80-
properties.put("com.sun.management.jmxremote.rmi.port", Integer.toString(port));
81-
properties.put("java.rmi.server.hostname", getHost());
82-
addFixedExposedPort(port, port);
83-
return this;
84-
}
85-
8664
@Override
8765
public void start() {
8866
// TODO: add support for ssl

0 commit comments

Comments
 (0)