Skip to content

Commit b84a73e

Browse files
authored
Merge pull request #2 from SylvainJuge/jmx-scraper-it
basic JMX client implementation + tests
2 parents 352202f + 7df9862 commit b84a73e

File tree

5 files changed

+486
-0
lines changed

5 files changed

+486
-0
lines changed

jmx-scraper/build.gradle.kts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ dependencies {
2323
testImplementation("org.junit-pioneer:junit-pioneer")
2424
}
2525

26+
testing {
27+
suites {
28+
val integrationTest by registering(JvmTestSuite::class) {
29+
dependencies {
30+
implementation("org.testcontainers:junit-jupiter")
31+
implementation("org.slf4j:slf4j-simple")
32+
}
33+
}
34+
}
35+
}
36+
2637
tasks {
2738
shadowJar {
2839
mergeServiceFiles()
@@ -40,7 +51,9 @@ tasks {
4051

4152
withType<Test>().configureEach {
4253
dependsOn(shadowJar)
54+
dependsOn(named("appJar"))
4355
systemProperty("shadow.jar.path", shadowJar.get().archiveFile.get().asFile.absolutePath)
56+
systemProperty("app.jar.path", named<Jar>("appJar").get().archiveFile.get().asFile.absolutePath)
4457
systemProperty("gradle.project.version", "${project.version}")
4558
}
4659

@@ -52,6 +65,14 @@ tasks {
5265
}
5366
}
5467

68+
tasks.register<Jar>("appJar") {
69+
from(sourceSets.get("integrationTest").output)
70+
archiveClassifier.set("app")
71+
manifest {
72+
attributes["Main-Class"] = "io.opentelemetry.contrib.jmxscraper.TestApp"
73+
}
74+
}
75+
5576
// Don't publish non-shadowed jar (shadowJar is in shadowRuntimeElements)
5677
with(components["java"] as AdhocComponentWithVariants) {
5778
configurations.forEach {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jmxscraper;
7+
8+
import java.lang.management.ManagementFactory;
9+
import javax.management.MBeanServer;
10+
import javax.management.ObjectName;
11+
12+
@SuppressWarnings("all")
13+
public class TestApp implements TestAppMXBean {
14+
15+
public static final String APP_STARTED_MSG = "app started";
16+
public static final String OBJECT_NAME = "io.opentelemetry.test:name=TestApp";
17+
18+
private volatile boolean running;
19+
20+
public static void main(String[] args) {
21+
TestApp app = TestApp.start();
22+
while (app.isRunning()) {
23+
try {
24+
Thread.sleep(100);
25+
} catch (InterruptedException e) {
26+
throw new RuntimeException(e);
27+
}
28+
}
29+
}
30+
31+
private TestApp() {}
32+
33+
static TestApp start() {
34+
TestApp app = new TestApp();
35+
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
36+
try {
37+
ObjectName objectName = new ObjectName(OBJECT_NAME);
38+
mbs.registerMBean(app, objectName);
39+
} catch (Exception e) {
40+
throw new RuntimeException(e);
41+
}
42+
app.running = true;
43+
System.out.println(APP_STARTED_MSG);
44+
return app;
45+
}
46+
47+
@Override
48+
public int getIntValue() {
49+
return 42;
50+
}
51+
52+
@Override
53+
public void stopApp() {
54+
running = false;
55+
}
56+
57+
boolean isRunning() {
58+
return running;
59+
}
60+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jmxscraper;
7+
8+
@SuppressWarnings("unused")
9+
public interface TestAppMXBean {
10+
11+
int getIntValue();
12+
13+
void stopApp();
14+
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.contrib.jmxscraper.client;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
import io.opentelemetry.contrib.jmxscraper.TestApp;
11+
import java.io.Closeable;
12+
import java.io.IOException;
13+
import java.nio.charset.StandardCharsets;
14+
import java.nio.file.Files;
15+
import java.nio.file.Path;
16+
import java.nio.file.Paths;
17+
import java.time.Duration;
18+
import java.util.ArrayList;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.stream.Collectors;
23+
import javax.management.ObjectName;
24+
import javax.management.remote.JMXConnector;
25+
import org.junit.jupiter.api.AfterAll;
26+
import org.junit.jupiter.api.BeforeAll;
27+
import org.junit.jupiter.api.Test;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
import org.testcontainers.containers.GenericContainer;
31+
import org.testcontainers.containers.Network;
32+
import org.testcontainers.containers.output.Slf4jLogConsumer;
33+
import org.testcontainers.containers.wait.strategy.Wait;
34+
import org.testcontainers.shaded.com.google.errorprone.annotations.CanIgnoreReturnValue;
35+
import org.testcontainers.utility.MountableFile;
36+
37+
public class JmxRemoteClientTest {
38+
39+
private static final Logger logger = LoggerFactory.getLogger(JmxRemoteClientTest.class);
40+
41+
private static Network network;
42+
43+
private static final List<AutoCloseable> toClose = new ArrayList<>();
44+
45+
@BeforeAll
46+
static void beforeAll() {
47+
network = Network.newNetwork();
48+
toClose.add(network);
49+
}
50+
51+
@AfterAll
52+
static void afterAll() {
53+
for (AutoCloseable item : toClose) {
54+
try {
55+
item.close();
56+
} catch (Exception e) {
57+
logger.warn("Error closing " + item, e);
58+
}
59+
}
60+
}
61+
62+
@Test
63+
void noAuth() {
64+
try (AppContainer app = new AppContainer().withJmxPort(9990).start()) {
65+
testConnector(() -> JmxRemoteClient.createNew(app.getHost(), app.getPort()).connect());
66+
}
67+
}
68+
69+
@Test
70+
void loginPwdAuth() {
71+
String login = "user";
72+
String pwd = "t0p!Secret";
73+
try (AppContainer app = new AppContainer().withJmxPort(9999).withUserAuth(login, pwd).start()) {
74+
testConnector(
75+
() ->
76+
JmxRemoteClient.createNew(app.getHost(), app.getPort())
77+
.userCredentials(login, pwd)
78+
.connect());
79+
}
80+
}
81+
82+
@Test
83+
void serverSSL() {
84+
// TODO: test with SSL enabled as RMI registry seems to work differently with SSL
85+
86+
// create keypair (public,private)
87+
// create server keystore with private key
88+
// configure server keystore
89+
//
90+
// create client truststore with public key
91+
// can we configure to use a custom truststore ???
92+
// connect to server
93+
}
94+
95+
private static void testConnector(ConnectorSupplier connectorSupplier) {
96+
try (JMXConnector connector = connectorSupplier.get()) {
97+
assertThat(connector.getMBeanServerConnection())
98+
.isNotNull()
99+
.satisfies(
100+
connection -> {
101+
try {
102+
ObjectName name = new ObjectName(TestApp.OBJECT_NAME);
103+
Object value = connection.getAttribute(name, "IntValue");
104+
assertThat(value).isEqualTo(42);
105+
} catch (Exception e) {
106+
throw new RuntimeException(e);
107+
}
108+
});
109+
110+
} catch (IOException e) {
111+
throw new RuntimeException(e);
112+
}
113+
}
114+
115+
private interface ConnectorSupplier {
116+
JMXConnector get() throws IOException;
117+
}
118+
119+
private static class AppContainer implements Closeable {
120+
121+
private final GenericContainer<?> appContainer;
122+
private final Map<String, String> properties;
123+
private int port;
124+
private String login;
125+
private String pwd;
126+
127+
private AppContainer() {
128+
this.properties = new HashMap<>();
129+
130+
properties.put("com.sun.management.jmxremote.ssl", "false"); // TODO :
131+
132+
// SSL registry : com.sun.management.jmxremote.registry.ssl
133+
// client side ssl auth: com.sun.management.jmxremote.ssl.need.client.auth
134+
135+
String appJar = System.getProperty("app.jar.path");
136+
assertThat(Paths.get(appJar)).isNotEmptyFile().isReadable();
137+
138+
this.appContainer =
139+
new GenericContainer<>("openjdk:8u272-jre-slim")
140+
.withCopyFileToContainer(MountableFile.forHostPath(appJar), "/app.jar")
141+
.withLogConsumer(new Slf4jLogConsumer(logger))
142+
.withNetwork(network)
143+
.waitingFor(
144+
Wait.forLogMessage(TestApp.APP_STARTED_MSG + "\\n", 1)
145+
.withStartupTimeout(Duration.ofSeconds(5)))
146+
.withCommand("java", "-jar", "/app.jar");
147+
}
148+
149+
@CanIgnoreReturnValue
150+
public AppContainer withJmxPort(int port) {
151+
this.port = port;
152+
properties.put("com.sun.management.jmxremote.port", Integer.toString(port));
153+
appContainer.withExposedPorts(port);
154+
return this;
155+
}
156+
157+
@CanIgnoreReturnValue
158+
public AppContainer withUserAuth(String login, String pwd) {
159+
this.login = login;
160+
this.pwd = pwd;
161+
return this;
162+
}
163+
164+
@CanIgnoreReturnValue
165+
AppContainer start() {
166+
if (pwd == null) {
167+
properties.put("com.sun.management.jmxremote.authenticate", "false");
168+
} else {
169+
properties.put("com.sun.management.jmxremote.authenticate", "true");
170+
171+
Path pwdFile = createPwdFile(login, pwd);
172+
appContainer.withCopyFileToContainer(MountableFile.forHostPath(pwdFile), "/jmx.password");
173+
properties.put("com.sun.management.jmxremote.password.file", "/jmx.password");
174+
175+
Path accessFile = createAccessFile(login);
176+
appContainer.withCopyFileToContainer(MountableFile.forHostPath(accessFile), "/jmx.access");
177+
properties.put("com.sun.management.jmxremote.access.file", "/jmx.access");
178+
}
179+
180+
String confArgs =
181+
properties.entrySet().stream()
182+
.map(
183+
e -> {
184+
String s = "-D" + e.getKey();
185+
if (!e.getValue().isEmpty()) {
186+
s += "=" + e.getValue();
187+
}
188+
return s;
189+
})
190+
.collect(Collectors.joining(" "));
191+
192+
appContainer.withEnv("JAVA_TOOL_OPTIONS", confArgs).start();
193+
194+
logger.info("Test application JMX port mapped to {}:{}", getHost(), getPort());
195+
196+
toClose.add(this);
197+
return this;
198+
}
199+
200+
int getPort() {
201+
return appContainer.getMappedPort(port);
202+
}
203+
204+
String getHost() {
205+
return appContainer.getHost();
206+
}
207+
208+
@Override
209+
public void close() {
210+
if (appContainer.isRunning()) {
211+
appContainer.stop();
212+
}
213+
}
214+
215+
private static Path createPwdFile(String login, String pwd) {
216+
try {
217+
Path path = Files.createTempFile("test", ".pwd");
218+
writeLine(path, String.format("%s %s", login, pwd));
219+
return path;
220+
} catch (IOException e) {
221+
throw new RuntimeException(e);
222+
}
223+
}
224+
225+
private static Path createAccessFile(String login) {
226+
try {
227+
Path path = Files.createTempFile("test", ".pwd");
228+
writeLine(path, String.format("%s %s", login, "readwrite"));
229+
return path;
230+
} catch (IOException e) {
231+
throw new RuntimeException(e);
232+
}
233+
}
234+
235+
private static void writeLine(Path path, String line) throws IOException {
236+
line = line + "\n";
237+
Files.write(path, line.getBytes(StandardCharsets.UTF_8));
238+
}
239+
}
240+
}

0 commit comments

Comments
 (0)