Skip to content

Commit 4ce7187

Browse files
authored
jmx-scraper add ssl support (#1710)
1 parent 572575e commit 4ce7187

File tree

10 files changed

+533
-70
lines changed

10 files changed

+533
-70
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ feature or via instrumentation, this project is hopefully for you.
1515
## Provided Libraries
1616

1717
| Status* | Library |
18-
| ------- |-------------------------------------------------------------------|
18+
|---------|-------------------------------------------------------------------|
1919
| beta | [AWS Resources](./aws-resources/README.md) |
2020
| stable | [AWS X-Ray SDK Support](./aws-xray/README.md) |
2121
| alpha | [AWS X-Ray Propagator](./aws-xray-propagator/README.md) |
22-
| alpha | [Baggage Processors](./baggage-processor/README.md) |
22+
| alpha | [Baggage Processors](./baggage-processor/README.md) |
2323
| alpha | [zstd Compressor](./compressors/compressor-zstd/README.md) |
2424
| alpha | [Consistent Sampling](./consistent-sampling/README.md) |
2525
| alpha | [Disk Buffering](./disk-buffering/README.md) |
@@ -29,6 +29,7 @@ feature or via instrumentation, this project is hopefully for you.
2929
| alpha | [JFR Connection](./jfr-connection/README.md) |
3030
| alpha | [JFR Events](./jfr-events/README.md) |
3131
| alpha | [JMX Metric Gatherer](./jmx-metrics/README.md) |
32+
| alpha | [JMX Metric Scraper](./jmx-scraper/README.md) |
3233
| alpha | [Kafka Support](./kafka-exporter/README.md) |
3334
| alpha | [OpenTelemetry Maven Extension](./maven-extension/README.md) |
3435
| alpha | [Micrometer MeterProvider](./micrometer-meter-provider/README.md) |

jmx-scraper/README.md

+17-8
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,29 @@ Configuration can be provided through:
2929
`otel.jmx.service.url=service:jmx:rmi:///jndi/rmi://tomcat:9010/jmxrmi` is written to stdin.
3030
- environment variables: `OTEL_JMX_TARGET_SYSTEM=tomcat OTEL_JMX_SERVICE_URL=service:jmx:rmi:///jndi/rmi://tomcat:9010/jmxrmi java -jar scraper.jar`
3131

32-
SDK auto-configuration is being used, so all the configuration options can be set using the java
32+
SDK autoconfiguration is being used, so all the configuration options can be set using the java
3333
properties syntax or the corresponding environment variables.
3434

3535
For example the `otel.jmx.service.url` option can be set with the `OTEL_JMX_SERVICE_URL` environment variable.
3636

3737
## Configuration reference
3838

39-
| config option | description |
40-
|--------------------------|---------------------------------------------------------------------------------------------------------------------|
41-
| `otel.jmx.service.url` | mandatory JMX URL to connect to the remote JVM |
42-
| `otel.jmx.target.system` | comma-separated list of systems to monitor, mandatory unless a custom configuration is used |
43-
| `otel.jmx.config` | comma-separated list of paths to custom YAML metrics definition, mandatory when `otel.jmx.target.system` is not set |
44-
| `otel.jmx.username` | user name for JMX connection, mandatory when JMX authentication is enabled on target JVM |
45-
| `otel.jmx.password` | password for JMX connection, mandatory when JMX authentication is enabled on target JVM |
39+
| config option | default value | description |
40+
|--------------------------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------|
41+
| `otel.jmx.service.url` | - | mandatory JMX URL to connect to the remote JVM |
42+
| `otel.jmx.target.system` | - | comma-separated list of systems to monitor, mandatory unless `otel.jmx.config` is set |
43+
| `otel.jmx.config` | empty | comma-separated list of paths to custom YAML metrics definition, mandatory when `otel.jmx.target.system` is not set |
44+
| `otel.jmx.username` | - | user name for JMX connection, mandatory when JMX authentication is set on target JVM with`com.sun.management.jmxremote.authenticate=true` |
45+
| `otel.jmx.password` | - | password for JMX connection, mandatory when JMX authentication is set on target JVM with `com.sun.management.jmxremote.authenticate=true` |
46+
| `otel.jmx.remote.registry.ssl` | `false` | connect to an SSL-protected registry when enabled on target JVM with `com.sun.management.jmxremote.registry.ssl=true` |
47+
48+
When both `otel.jmx.target.system` and `otel.jmx.config` configuration options are used at the same time:
49+
50+
- `otel.jmx.target.system` provides ready-to-use metrics and `otel.jmx.config` allows to add custom definitions.
51+
- The metrics definitions will be the aggregation of both.
52+
- There is no guarantee on the priority or any ability to override the definitions.
53+
54+
If there is a need to override existing ready-to-use metrics or to keep control on the metrics definitions, using a custom YAML definition with `otel.jmx.config` is the recommended option.
4655

4756
Supported values for `otel.jmx.target.system`:
4857

jmx-scraper/build.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ testing {
3737
implementation("com.linecorp.armeria:armeria-junit5")
3838
implementation("com.linecorp.armeria:armeria-grpc")
3939
implementation("io.opentelemetry.proto:opentelemetry-proto:1.5.0-alpha")
40+
implementation("org.bouncycastle:bcprov-jdk18on:1.80")
41+
implementation("org.bouncycastle:bcpkix-jdk18on:1.80")
4042
}
4143
}
4244
}

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

+102-11
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77

88
import static org.assertj.core.api.Assertions.assertThat;
99

10+
import java.nio.file.Path;
11+
import java.security.cert.X509Certificate;
1012
import java.util.function.Function;
1113
import org.junit.jupiter.api.AfterAll;
1214
import org.junit.jupiter.api.BeforeAll;
1315
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.io.TempDir;
1417
import org.slf4j.Logger;
1518
import org.slf4j.LoggerFactory;
1619
import org.testcontainers.containers.GenericContainer;
@@ -31,6 +34,10 @@ public class JmxConnectionTest {
3134
private static final int JMX_PORT = 9999;
3235
private static final String APP_HOST = "app";
3336

37+
// key/trust stores passwords
38+
private static final String CLIENT_PASSWORD = "client";
39+
private static final String SERVER_PASSWORD = "server";
40+
3441
private static final Logger jmxScraperLogger = LoggerFactory.getLogger("JmxScraperContainer");
3542
private static final Logger appLogger = LoggerFactory.getLogger("TestAppContainer");
3643

@@ -70,6 +77,84 @@ void userPassword() {
7077
scraper -> scraper.withRmiServiceUrl(APP_HOST, JMX_PORT).withUser(login).withPassword(pwd));
7178
}
7279

80+
@Test
81+
void serverSsl(@TempDir Path tempDir) {
82+
testServerSsl(tempDir, /* sslRmiRegistry= */ false);
83+
}
84+
85+
@Test
86+
void serverSslWithSslRmiRegistry(@TempDir Path tempDir) {
87+
testServerSsl(tempDir, /* sslRmiRegistry= */ true);
88+
}
89+
90+
private static void testServerSsl(Path tempDir, boolean sslRmiRegistry) {
91+
// two keystores:
92+
// server keystore with public/private key pair
93+
// client trust store with certificate from server
94+
95+
TestKeyStore serverKeyStore =
96+
TestKeyStore.newKeyStore(tempDir.resolve("server.jks"), SERVER_PASSWORD);
97+
TestKeyStore clientTrustStore =
98+
TestKeyStore.newKeyStore(tempDir.resolve("client.jks"), CLIENT_PASSWORD);
99+
100+
X509Certificate serverCertificate = serverKeyStore.addKeyPair();
101+
clientTrustStore.addTrustedCertificate(serverCertificate);
102+
103+
connectionTest(
104+
app ->
105+
(sslRmiRegistry ? app.withSslRmiRegistry(4242) : app)
106+
.withJmxPort(JMX_PORT)
107+
.withJmxSsl()
108+
.withKeyStore(serverKeyStore),
109+
scraper ->
110+
(sslRmiRegistry ? scraper.withSslRmiRegistry() : scraper)
111+
.withRmiServiceUrl(APP_HOST, JMX_PORT)
112+
.withTrustStore(clientTrustStore));
113+
}
114+
115+
@Test
116+
void serverSslClientSsl(@TempDir Path tempDir) {
117+
// Note: this could have been made simpler by relying on the fact that keystore could be used
118+
// as a trust store, but having clear split provides also some extra clarity
119+
//
120+
// 4 keystores:
121+
// server keystore with public/private key pair
122+
// server truststore with client certificate
123+
// client key store with public/private key pair
124+
// client trust store with certificate from server
125+
126+
TestKeyStore serverKeyStore =
127+
TestKeyStore.newKeyStore(tempDir.resolve("server-keystore.jks"), SERVER_PASSWORD);
128+
TestKeyStore serverTrustStore =
129+
TestKeyStore.newKeyStore(tempDir.resolve("server-truststore.jks"), SERVER_PASSWORD);
130+
131+
X509Certificate serverCertificate = serverKeyStore.addKeyPair();
132+
133+
TestKeyStore clientKeyStore =
134+
TestKeyStore.newKeyStore(tempDir.resolve("client-keystore.jks"), CLIENT_PASSWORD);
135+
TestKeyStore clientTrustStore =
136+
TestKeyStore.newKeyStore(tempDir.resolve("client-truststore.jks"), CLIENT_PASSWORD);
137+
138+
X509Certificate clientCertificate = clientKeyStore.addKeyPair();
139+
140+
// adding certificates in trust stores
141+
clientTrustStore.addTrustedCertificate(serverCertificate);
142+
serverTrustStore.addTrustedCertificate(clientCertificate);
143+
144+
connectionTest(
145+
app ->
146+
app.withJmxPort(JMX_PORT)
147+
.withJmxSsl()
148+
.withClientSslCertificate()
149+
.withKeyStore(serverKeyStore)
150+
.withTrustStore(serverTrustStore),
151+
scraper ->
152+
scraper
153+
.withRmiServiceUrl(APP_HOST, JMX_PORT)
154+
.withKeyStore(clientKeyStore)
155+
.withTrustStore(clientTrustStore));
156+
}
157+
73158
private static void connectionTest(
74159
Function<TestAppContainer, TestAppContainer> customizeApp,
75160
Function<JmxScraperContainer, JmxScraperContainer> customizeScraper) {
@@ -86,17 +171,23 @@ private static void connectionTest(
86171
private static void checkConnectionLogs(JmxScraperContainer scraper, boolean expectedOk) {
87172

88173
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-
}
174+
175+
// usually only the last line can be checked, however when it fails with an exception
176+
// the stack trace is last in the output, so it's simpler to check all lines of log output
177+
178+
assertThat(logLines)
179+
.anySatisfy(
180+
line -> {
181+
if (expectedOk) {
182+
assertThat(line)
183+
.describedAs("should log connection success")
184+
.contains("JMX connection test OK");
185+
} else {
186+
assertThat(line)
187+
.describedAs("should log connection failure")
188+
.contains("JMX connection test ERROR");
189+
}
190+
});
100191
}
101192

102193
private static void waitTerminated(GenericContainer<?> container) {

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

+85-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
import static org.assertj.core.api.Assertions.assertThat;
99

1010
import com.google.errorprone.annotations.CanIgnoreReturnValue;
11+
import java.nio.file.Path;
1112
import java.time.Duration;
1213
import java.util.ArrayList;
14+
import java.util.Arrays;
15+
import java.util.Collections;
1316
import java.util.HashSet;
1417
import java.util.List;
1518
import java.util.Locale;
@@ -29,6 +32,9 @@ public class JmxScraperContainer extends GenericContainer<JmxScraperContainer> {
2932
private String password;
3033
private final List<String> extraJars;
3134
private boolean testJmx;
35+
private TestKeyStore keyStore;
36+
private TestKeyStore trustStore;
37+
private boolean sslRmiRegistry;
3238

3339
public JmxScraperContainer(String otlpEndpoint, String baseImage) {
3440
super(baseImage);
@@ -44,20 +50,38 @@ public JmxScraperContainer(String otlpEndpoint, String baseImage) {
4450
this.extraJars = new ArrayList<>();
4551
}
4652

53+
/**
54+
* Adds a target system
55+
*
56+
* @param targetSystem target system
57+
* @return this
58+
*/
4759
@CanIgnoreReturnValue
4860
public JmxScraperContainer withTargetSystem(String targetSystem) {
4961
targetSystems.add(targetSystem);
5062
return this;
5163
}
5264

65+
/**
66+
* Set connection to a standard JMX service URL
67+
*
68+
* @param host JMX host
69+
* @param port JMX port
70+
* @return this
71+
*/
5372
@CanIgnoreReturnValue
5473
public JmxScraperContainer withRmiServiceUrl(String host, int port) {
55-
// TODO: adding a way to provide 'host:port' syntax would make this easier for end users
5674
return withServiceUrl(
5775
String.format(
5876
Locale.getDefault(), "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi", host, port));
5977
}
6078

79+
/**
80+
* Set connection to a JMX service URL
81+
*
82+
* @param serviceUrl service URL
83+
* @return this
84+
*/
6185
@CanIgnoreReturnValue
6286
public JmxScraperContainer withServiceUrl(String serviceUrl) {
6387
this.serviceUrl = serviceUrl;
@@ -112,12 +136,52 @@ public JmxScraperContainer withCustomYaml(String yamlPath) {
112136
return this;
113137
}
114138

139+
/**
140+
* Configure the scraper JVM to only test connection with the JMX endpoint
141+
*
142+
* @return this
143+
*/
115144
@CanIgnoreReturnValue
116145
public JmxScraperContainer withTestJmx() {
117146
this.testJmx = true;
118147
return this;
119148
}
120149

150+
/**
151+
* Configure key store for the scraper JVM
152+
*
153+
* @param keyStore key store
154+
* @return this
155+
*/
156+
@CanIgnoreReturnValue
157+
public JmxScraperContainer withKeyStore(TestKeyStore keyStore) {
158+
this.keyStore = keyStore;
159+
return this;
160+
}
161+
162+
/**
163+
* Configure trust store for the scraper JVM
164+
*
165+
* @param trustStore trust store
166+
* @return this
167+
*/
168+
@CanIgnoreReturnValue
169+
public JmxScraperContainer withTrustStore(TestKeyStore trustStore) {
170+
this.trustStore = trustStore;
171+
return this;
172+
}
173+
174+
/**
175+
* Enables connection to an SSL-protected RMI registry
176+
*
177+
* @return this
178+
*/
179+
@CanIgnoreReturnValue
180+
public JmxScraperContainer withSslRmiRegistry() {
181+
this.sslRmiRegistry = true;
182+
return this;
183+
}
184+
121185
@Override
122186
public void start() {
123187
// for now only configure through JVM args
@@ -144,6 +208,13 @@ public void start() {
144208
arguments.add("-Dotel.jmx.password=" + password);
145209
}
146210

211+
arguments.addAll(addSecureStore(keyStore, /* isKeyStore= */ true));
212+
arguments.addAll(addSecureStore(trustStore, /* isKeyStore= */ false));
213+
214+
if (sslRmiRegistry) {
215+
arguments.add("-Dotel.jmx.remote.registry.ssl=true");
216+
}
217+
147218
if (!customYamlFiles.isEmpty()) {
148219
for (String yaml : customYamlFiles) {
149220
this.withCopyFileToContainer(MountableFile.forClasspathResource(yaml), yaml);
@@ -177,4 +248,17 @@ public void start() {
177248

178249
super.start();
179250
}
251+
252+
private List<String> addSecureStore(TestKeyStore keyStore, boolean isKeyStore) {
253+
if (keyStore == null) {
254+
return Collections.emptyList();
255+
}
256+
Path path = keyStore.getPath();
257+
String containerPath = "/" + path.getFileName().toString();
258+
this.withCopyFileToContainer(MountableFile.forHostPath(path), containerPath);
259+
260+
String prefix = String.format("-Djavax.net.ssl.%sStore", isKeyStore ? "key" : "trust");
261+
return Arrays.asList(
262+
prefix + "=" + containerPath, prefix + "Password=" + keyStore.getPassword());
263+
}
180264
}

0 commit comments

Comments
 (0)