Skip to content

Commit b373e2e

Browse files
authored
jmx-scraper implement stable service.instance.id (#2270)
1 parent 8a27e01 commit b373e2e

File tree

5 files changed

+144
-33
lines changed

5 files changed

+144
-33
lines changed

jmx-scraper/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ dependencies {
2626

2727
implementation("io.opentelemetry.instrumentation:opentelemetry-jmx-metrics")
2828

29+
implementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating")
30+
2931
testImplementation("org.junit-pioneer:junit-pioneer")
3032
testImplementation("io.opentelemetry:opentelemetry-sdk-testing")
3133
testImplementation("org.awaitility:awaitility")

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import java.nio.file.Path;
1111
import java.security.cert.X509Certificate;
12+
import java.util.UUID;
1213
import java.util.concurrent.TimeUnit;
1314
import java.util.function.Function;
1415
import org.junit.jupiter.api.AfterAll;
@@ -131,7 +132,7 @@ private void testServerSsl(
131132
}
132133

133134
@ParameterizedTest
134-
@EnumSource(value = JmxScraperContainer.ConfigSource.class)
135+
@EnumSource
135136
void serverSslClientSsl(JmxScraperContainer.ConfigSource configSource) {
136137
// Note: this could have been made simpler by relying on the fact that keystore could be used
137138
// as a trust store, but having clear split provides also some extra clarity
@@ -175,6 +176,42 @@ void serverSslClientSsl(JmxScraperContainer.ConfigSource configSource) {
175176
.withConfigSource(configSource));
176177
}
177178

179+
@Test
180+
void stableServiceInstanceServiceId() {
181+
// start a single app, connect twice to it and check that the service id is the same
182+
try (TestAppContainer app = appContainer().withJmxPort(JMX_PORT)) {
183+
app.start();
184+
185+
UUID firstId = startScraperAndGetServiceId();
186+
UUID secondId = startScraperAndGetServiceId();
187+
188+
assertThat(firstId)
189+
.describedAs(
190+
"connecting twice to the same JVM should return the same service instance ID")
191+
.isEqualTo(secondId);
192+
}
193+
}
194+
195+
private static UUID startScraperAndGetServiceId() {
196+
try (JmxScraperContainer scraper =
197+
scraperContainer()
198+
.withRmiServiceUrl(APP_HOST, JMX_PORT)
199+
// does not need to be tested on all config sources
200+
.withConfigSource(JmxScraperContainer.ConfigSource.SYSTEM_PROPERTIES)) {
201+
scraper.start();
202+
waitTerminated(scraper);
203+
String[] logLines = scraper.getLogs().split("\n");
204+
UUID serviceId = null;
205+
for (String logLine : logLines) {
206+
if (logLine.contains("remote service instance ID")) {
207+
serviceId = UUID.fromString(logLine.substring(logLine.lastIndexOf(":") + 1).trim());
208+
}
209+
}
210+
assertThat(serviceId).describedAs("unable to get service instance ID from logs").isNotNull();
211+
return serviceId;
212+
}
213+
}
214+
178215
private static void connectionTest(
179216
Function<TestAppContainer, TestAppContainer> customizeApp,
180217
Function<JmxScraperContainer, JmxScraperContainer> customizeScraper) {

jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/JmxScraper.java

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,47 @@
55

66
package io.opentelemetry.contrib.jmxscraper;
77

8+
import static io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes.SERVICE_INSTANCE_ID;
89
import static java.util.Arrays.asList;
910
import static java.util.Collections.singletonList;
10-
import static java.util.Optional.ofNullable;
1111
import static java.util.logging.Level.INFO;
1212
import static java.util.logging.Level.SEVERE;
1313

14-
import io.opentelemetry.api.GlobalOpenTelemetry;
14+
import io.opentelemetry.api.OpenTelemetry;
15+
import io.opentelemetry.api.common.Attributes;
1516
import io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig;
1617
import io.opentelemetry.contrib.jmxscraper.config.PropertiesCustomizer;
1718
import io.opentelemetry.contrib.jmxscraper.config.PropertiesSupplier;
1819
import io.opentelemetry.instrumentation.jmx.engine.JmxMetricInsight;
1920
import io.opentelemetry.instrumentation.jmx.engine.MetricConfiguration;
2021
import io.opentelemetry.instrumentation.jmx.yaml.RuleParser;
2122
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
23+
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
2224
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
25+
import io.opentelemetry.sdk.resources.Resource;
2326
import java.io.DataInputStream;
2427
import java.io.IOException;
2528
import java.io.InputStream;
29+
import java.nio.charset.StandardCharsets;
2630
import java.nio.file.Files;
2731
import java.nio.file.Path;
2832
import java.nio.file.Paths;
2933
import java.util.ArrayList;
34+
import java.util.Arrays;
3035
import java.util.List;
3136
import java.util.Map;
3237
import java.util.Properties;
38+
import java.util.UUID;
3339
import java.util.concurrent.atomic.AtomicBoolean;
40+
import java.util.function.BiFunction;
3441
import java.util.logging.Logger;
42+
import javax.annotation.Nullable;
3543
import javax.management.MBeanServerConnection;
44+
import javax.management.ObjectName;
3645
import javax.management.remote.JMXConnector;
3746

3847
public final class JmxScraper {
48+
3949
private static final Logger logger = Logger.getLogger(JmxScraper.class.getName());
4050
private static final String CONFIG_ARG = "-config";
4151
private static final String TEST_ARG = "-test";
@@ -64,36 +74,41 @@ public static void main(String[] args) {
6474
Properties argsConfig = argsToConfig(effectiveArgs);
6575
propagateToSystemProperties(argsConfig);
6676

67-
// auto-configure and register SDK
6877
PropertiesCustomizer configCustomizer = new PropertiesCustomizer();
69-
AutoConfiguredOpenTelemetrySdk.builder()
70-
.addPropertiesSupplier(new PropertiesSupplier(argsConfig))
71-
.addPropertiesCustomizer(configCustomizer)
72-
.setResultAsGlobal()
73-
.build();
7478

79+
// we rely on the config customizer to be executed first to get effective config.
80+
BiFunction<Resource, ConfigProperties, Resource> resourceCustomizer =
81+
(resource, configProperties) -> {
82+
UUID instanceId = getRemoteServiceInstanceId(configCustomizer.getConnectorBuilder());
83+
if (resource.getAttribute(SERVICE_INSTANCE_ID) != null || instanceId == null) {
84+
return resource;
85+
}
86+
logger.log(INFO, "remote service instance ID: " + instanceId);
87+
return resource.merge(
88+
Resource.create(Attributes.of(SERVICE_INSTANCE_ID, instanceId.toString())));
89+
};
90+
91+
// auto-configure SDK
92+
OpenTelemetry openTelemetry =
93+
AutoConfiguredOpenTelemetrySdk.builder()
94+
.addPropertiesSupplier(new PropertiesSupplier(argsConfig))
95+
.addPropertiesCustomizer(configCustomizer)
96+
.addResourceCustomizer(resourceCustomizer)
97+
.build()
98+
.getOpenTelemetrySdk();
99+
100+
// scraper configuration and connector builder are built using effective SDK configuration
101+
// thus we have to get it after the SDK is built
75102
JmxScraperConfig scraperConfig = configCustomizer.getScraperConfig();
76-
77-
long exportSeconds = scraperConfig.getSamplingInterval().toMillis() / 1000;
78-
logger.log(INFO, "metrics export interval (seconds) = " + exportSeconds);
79-
80-
JmxMetricInsight service =
81-
JmxMetricInsight.createService(
82-
GlobalOpenTelemetry.get(), scraperConfig.getSamplingInterval().toMillis());
83-
JmxConnectorBuilder connectorBuilder =
84-
JmxConnectorBuilder.createNew(scraperConfig.getServiceUrl());
85-
86-
ofNullable(scraperConfig.getUsername()).ifPresent(connectorBuilder::withUser);
87-
ofNullable(scraperConfig.getPassword()).ifPresent(connectorBuilder::withPassword);
88-
89-
if (scraperConfig.isRegistrySsl()) {
90-
connectorBuilder.withSslRegistry();
91-
}
103+
JmxConnectorBuilder connectorBuilder = configCustomizer.getConnectorBuilder();
92104

93105
if (testMode) {
94106
System.exit(testConnection(connectorBuilder) ? 0 : 1);
95107
} else {
96-
JmxScraper jmxScraper = new JmxScraper(connectorBuilder, service, scraperConfig);
108+
JmxMetricInsight jmxInsight =
109+
JmxMetricInsight.createService(
110+
openTelemetry, scraperConfig.getSamplingInterval().toMillis());
111+
JmxScraper jmxScraper = new JmxScraper(connectorBuilder, jmxInsight, scraperConfig);
97112
jmxScraper.start();
98113
}
99114
} catch (ConfigurationException e) {
@@ -117,7 +132,6 @@ public static void main(String[] args) {
117132

118133
private static boolean testConnection(JmxConnectorBuilder connectorBuilder) {
119134
try (JMXConnector connector = connectorBuilder.build()) {
120-
121135
MBeanServerConnection connection = connector.getMBeanServerConnection();
122136
Integer mbeanCount = connection.getMBeanCount();
123137
if (mbeanCount > 0) {
@@ -133,6 +147,30 @@ private static boolean testConnection(JmxConnectorBuilder connectorBuilder) {
133147
}
134148
}
135149

150+
@Nullable
151+
private static UUID getRemoteServiceInstanceId(JmxConnectorBuilder connectorBuilder) {
152+
try (JMXConnector jmxConnector = connectorBuilder.build()) {
153+
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
154+
155+
StringBuilder id = new StringBuilder();
156+
try {
157+
ObjectName objectName = new ObjectName("java.lang:type=Runtime");
158+
for (String attribute : Arrays.asList("StartTime", "Name")) {
159+
Object value = connection.getAttribute(objectName, attribute);
160+
if (id.length() > 0) {
161+
id.append(" ");
162+
}
163+
id.append(value);
164+
}
165+
return UUID.nameUUIDFromBytes(id.toString().getBytes(StandardCharsets.UTF_8));
166+
} catch (Exception e) {
167+
throw new IllegalStateException(e);
168+
}
169+
} catch (IOException e) {
170+
return null;
171+
}
172+
}
173+
136174
// package private for testing
137175
static void propagateToSystemProperties(Properties properties) {
138176
for (Map.Entry<Object, Object> entry : properties.entrySet()) {

jmx-scraper/src/main/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesCustomizer.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.JMX_INTERVAL_LEGACY;
99
import static io.opentelemetry.contrib.jmxscraper.config.JmxScraperConfig.METRIC_EXPORT_INTERVAL;
1010

11+
import io.opentelemetry.contrib.jmxscraper.JmxConnectorBuilder;
1112
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
1213
import java.util.HashMap;
1314
import java.util.Map;
15+
import java.util.Optional;
1416
import java.util.function.Function;
17+
import java.util.logging.Level;
1518
import java.util.logging.Logger;
1619
import javax.annotation.Nullable;
1720

@@ -24,6 +27,8 @@ public final class PropertiesCustomizer implements Function<ConfigProperties, Ma
2427

2528
@Nullable private JmxScraperConfig scraperConfig;
2629

30+
@Nullable private JmxConnectorBuilder connectorBuilder;
31+
2732
@Override
2833
public Map<String, String> apply(ConfigProperties config) {
2934
Map<String, String> result = new HashMap<>();
@@ -44,10 +49,28 @@ public Map<String, String> apply(ConfigProperties config) {
4449
result.put(METRIC_EXPORT_INTERVAL, intervalLegacy + "ms");
4550
}
4651

52+
// scraper config and connector builder must be initialized with the effective SDK configuration
53+
// thus we need to initialize them here and then rely on getter being called after this method.
4754
scraperConfig = JmxScraperConfig.fromConfig(config);
55+
connectorBuilder = createConnectorBuilder(scraperConfig);
56+
57+
long exportSeconds = scraperConfig.getSamplingInterval().toMillis() / 1000;
58+
logger.log(Level.INFO, "metrics export interval (seconds) = " + exportSeconds);
59+
4860
return result;
4961
}
5062

63+
private static JmxConnectorBuilder createConnectorBuilder(JmxScraperConfig scraperConfig) {
64+
JmxConnectorBuilder connectorBuilder =
65+
JmxConnectorBuilder.createNew(scraperConfig.getServiceUrl());
66+
Optional.ofNullable(scraperConfig.getUsername()).ifPresent(connectorBuilder::withUser);
67+
Optional.ofNullable(scraperConfig.getPassword()).ifPresent(connectorBuilder::withPassword);
68+
if (scraperConfig.isRegistrySsl()) {
69+
connectorBuilder.withSslRegistry();
70+
}
71+
return connectorBuilder;
72+
}
73+
5174
/**
5275
* Get scraper configuration from the previous call to {@link #apply(ConfigProperties)}
5376
*
@@ -60,4 +83,11 @@ public JmxScraperConfig getScraperConfig() {
6083
}
6184
return scraperConfig;
6285
}
86+
87+
public JmxConnectorBuilder getConnectorBuilder() {
88+
if (connectorBuilder == null) {
89+
throw new IllegalStateException("apply() must be called before getConnectorBuilder()");
90+
}
91+
return connectorBuilder;
92+
}
6393
}

jmx-scraper/src/test/java/io/opentelemetry/contrib/jmxscraper/config/PropertiesCustomizerTest.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,20 @@
1717

1818
class PropertiesCustomizerTest {
1919

20+
private static final String DUMMY_URL = "service:jmx:rmi:///jndi/rmi://host:999/jmxrmi";
21+
2022
@Test
21-
void tryGetConfigBeforeApply() {
23+
void tryGetBeforeApply() {
2224
assertThatThrownBy(() -> new PropertiesCustomizer().getScraperConfig())
2325
.isInstanceOf(IllegalStateException.class);
26+
assertThatThrownBy(() -> new PropertiesCustomizer().getConnectorBuilder())
27+
.isInstanceOf(IllegalStateException.class);
2428
}
2529

2630
@Test
2731
void defaultOtlpExporter() {
2832
Map<String, String> map = new HashMap<>();
29-
map.put("otel.jmx.service.url", "dummy-url");
33+
map.put("otel.jmx.service.url", DUMMY_URL);
3034
map.put("otel.jmx.target.system", "jvm");
3135
ConfigProperties config = DefaultConfigProperties.createFromMap(map);
3236

@@ -37,7 +41,7 @@ void defaultOtlpExporter() {
3741
@Test
3842
void explicitExporterSet() {
3943
Map<String, String> map = new HashMap<>();
40-
map.put("otel.jmx.service.url", "dummy-url");
44+
map.put("otel.jmx.service.url", DUMMY_URL);
4145
map.put("otel.jmx.target.system", "jvm");
4246
map.put("otel.metrics.exporter", "otlp,logging");
4347
ConfigProperties config = DefaultConfigProperties.createFromMap(map);
@@ -49,7 +53,7 @@ void explicitExporterSet() {
4953
@Test
5054
void getSomeConfiguration() {
5155
Map<String, String> map = new HashMap<>();
52-
map.put("otel.jmx.service.url", "dummy-url");
56+
map.put("otel.jmx.service.url", DUMMY_URL);
5357
map.put("otel.jmx.target.system", "jvm");
5458
map.put("otel.metrics.exporter", "otlp");
5559
ConfigProperties config = DefaultConfigProperties.createFromMap(map);
@@ -67,7 +71,7 @@ void getSomeConfiguration() {
6771
@Test
6872
void setSdkMetricExportFromJmxInterval() {
6973
Map<String, String> map = new HashMap<>();
70-
map.put("otel.jmx.service.url", "dummy-url");
74+
map.put("otel.jmx.service.url", DUMMY_URL);
7175
map.put("otel.jmx.target.system", "jvm");
7276
map.put("otel.metrics.exporter", "otlp");
7377
map.put("otel.jmx.interval.milliseconds", "10000");
@@ -83,7 +87,7 @@ void setSdkMetricExportFromJmxInterval() {
8387
@Test
8488
void sdkMetricExportIntervalPriority() {
8589
Map<String, String> map = new HashMap<>();
86-
map.put("otel.jmx.service.url", "dummy-url");
90+
map.put("otel.jmx.service.url", DUMMY_URL);
8791
map.put("otel.jmx.target.system", "jvm");
8892
map.put("otel.metrics.exporter", "otlp");
8993
map.put("otel.jmx.interval.milliseconds", "10000");

0 commit comments

Comments
 (0)