Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.io.InputStream;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.logging.Level;
Expand All @@ -32,19 +33,24 @@ public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) {
JmxTelemetryBuilder jmx =
JmxTelemetry.builder(GlobalOpenTelemetry.get())
.beanDiscoveryDelay(beanDiscoveryDelay(config));

try {
config.getList("otel.jmx.config").stream().map(Paths::get).forEach(jmx::addCustomRules);
config.getList("otel.jmx.target.system").forEach(jmx::addClassPathRules);
config.getList("otel.jmx.config").stream().map(Paths::get).forEach(jmx::addRules);
config.getList("otel.jmx.target.system").forEach(target -> addClasspathRules(target, jmx));
} catch (RuntimeException e) {
// for now only log JMX errors as they do not prevent agent startup
logger.log(Level.SEVERE, "Error while loading JMX configuration", e);
}

jmx.build().start();
}
}

private static void addClasspathRules(String target, JmxTelemetryBuilder builder) {
ClassLoader classLoader = JmxTelemetryBuilder.class.getClassLoader();
String resource = String.format("jmx/rules/%s.yaml", target);
InputStream input = classLoader.getResourceAsStream(resource);
builder.addRules(input, "classpath " + resource);
}

private static Duration beanDiscoveryDelay(ConfigProperties configProperties) {
Duration discoveryDelay = configProperties.getDuration("otel.jmx.discovery.delay");
if (discoveryDelay != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ void testToVerifyExistingRulesAreValid() throws Exception {
assertThat(filePath).isRegularFile();

// loading rules from direct file access
JmxTelemetry.builder(OpenTelemetry.noop()).addCustomRules(filePath);
JmxTelemetry.builder(OpenTelemetry.noop()).addRules(filePath);
}
}
}
15 changes: 9 additions & 6 deletions instrumentation/jmx-metrics/library/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,24 @@ implementation("io.opentelemetry.instrumentation:opentelemetry-jmx-metrics:OPENT

```java
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.instrumentation.jmx.JmxTelemetry;
import io.opentelemetry.instrumentation.jmx.JmxTelemetryBuilder;

import java.time.Duration;

// Get an OpenTelemetry instance
OpenTelemetry openTelemetry = ...
OpenTelemetry openTelemetry = GlobalOpenTelemetry.get();

JmxTelemetry jmxTelemetry = JmxTelemetry.builder(openTelemetry)
// Configure included metrics (optional)
.addClasspathRules("tomcat")
.addClasspathRules("jetty")
.addRules(JmxTelemetry.class.getClassLoader().getResourceAsStream("jmx/rules/jetty.yaml"), "jetty")
.addRules(JmxTelemetry.class.getClassLoader().getResourceAsStream("jmx/rules/tomcat.yaml"), "tomcat")
// Configure custom metrics (optional)
.addCustomRules("/path/to/custom-jmx.yaml")
.addRules(Paths.get("/path/to/custom-jmx.yaml"))
// delay bean discovery by 5 seconds
.beanDiscoveryDelay(5000)
.beanDiscoveryDelay(Duration.ofSeconds(5))
.build();

jmxTelemetry.startLocal();
jmxTelemetry.start();
```
Original file line number Diff line number Diff line change
Expand Up @@ -49,47 +49,70 @@ public JmxTelemetryBuilder beanDiscoveryDelay(Duration delay) {
}

/**
* Adds built-in JMX rules from classpath resource
* Adds built-in JMX rules from classpath resource.
*
* @param target name of target in /jmx/rules/{target}.yaml classpath resource
* @return builder instance
* @throws IllegalArgumentException when classpath resource does not exist or can't be parsed
*/
// TODO: deprecate this method after 2.23.0 release in favor of addRules
Copy link
Contributor

Choose a reason for hiding this comment

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

why don't you deprecate it immediately?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we deprecate it right now, it means the next update of instrumentation dependency in contrib will require to immediately modify the code of jmx-scraper in contrib to switch to the new API. Even if the changes are minor it's an added constraint on the dependency update.

Doing that after 2.23.0 gives a bit more time with a timeline like this:

  • instrumentation 2.23.0 is released with this new API
  • contrib gets updated to use 2.23.0 dependency
  • we update jmx-scraper to use the new API introduced by this PR
  • we deprecate methods in this API, this will likely be for the 2.24.0 release

@CanIgnoreReturnValue
public JmxTelemetryBuilder addClassPathRules(String target) {
String yamlResource = String.format("jmx/rules/%s.yaml", target);
try (InputStream inputStream =
JmxTelemetryBuilder.class.getClassLoader().getResourceAsStream(yamlResource)) {
if (inputStream == null) {
throw new IllegalArgumentException("JMX rules not found in classpath: " + yamlResource);
}
logger.log(FINE, "Adding JMX config from classpath for {0}", yamlResource);
RuleParser parserInstance = RuleParser.get();
parserInstance.addMetricDefsTo(metricConfiguration, inputStream, target);
} catch (Exception e) {
String resourcePath = String.format("jmx/rules/%s.yaml", target);
ClassLoader classLoader = JmxTelemetryBuilder.class.getClassLoader();
try (InputStream inputStream = classLoader.getResourceAsStream(resourcePath)) {
return addRules(inputStream, resourcePath);
} catch (IOException e) {
throw new IllegalArgumentException(
"Unable to load JMX rules from classpath: " + yamlResource, e);
"Unable to load JMX rules from resource " + resourcePath, e);
}
}

/**
* Adds JMX rules from input stream
*
* @param input input to read rules from
* @param description input description, used for user-friendly logs and parsing error messages
* @throws IllegalArgumentException when input is {@literal null}
*/
@CanIgnoreReturnValue
public JmxTelemetryBuilder addRules(InputStream input, String description) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure it makes sense to ask for description. You have no way of telling whether the provided description is correct or user just copy-pasted "tomcat" everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The description is used:

  • to provide the path of the resource when loading from classpath
  • to provide the path of the file when loading from filesystem

It is only used to provide human-friendly error and log messages, so even if an invalid or constant value is used there is no functional impact.

Copy link
Member

Choose a reason for hiding this comment

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

It does look odd to me in a public API. Can JMX scraper provide friendly message on its own without this arg?

if (input == null) {
throw new IllegalArgumentException("JMX rules not found for " + description);
}
logger.log(FINE, "Adding JMX config from {0}", description);
RuleParser parserInstance = RuleParser.get();
parserInstance.addMetricDefsTo(metricConfiguration, input, description);
return this;
}

/**
* Adds custom JMX rules from file system path
* Adds JMX rules from file system path
*
* @param path path to yaml file
* @return builder instance
* @throws IllegalArgumentException when classpath resource does not exist or can't be parsed
* @throws IllegalArgumentException in case of parsing errors or when file does not exist
*/
@CanIgnoreReturnValue
public JmxTelemetryBuilder addCustomRules(Path path) {
logger.log(FINE, "Adding JMX config from file: {0}", path);
RuleParser parserInstance = RuleParser.get();
public JmxTelemetryBuilder addRules(Path path) {
try (InputStream inputStream = Files.newInputStream(path)) {
parserInstance.addMetricDefsTo(metricConfiguration, inputStream, path.toString());
return addRules(inputStream, "file " + path);
} catch (IOException e) {
throw new IllegalArgumentException("Unable to load JMX rules in path: " + path, e);
throw new IllegalArgumentException("Unable to load JMX rules from: " + path, e);
}
return this;
}

/**
* Adds custom JMX rules from file system path
*
* @param path path to yaml file
* @return builder instance
* @throws IllegalArgumentException when classpath resource does not exist or can't be parsed
*/
// TODO: deprecate this method after 2.23.0 release in favor of addRules
@CanIgnoreReturnValue
public JmxTelemetryBuilder addCustomRules(Path path) {
return addRules(path);
}

public JmxTelemetry build() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,21 +162,24 @@ private static void failOnExtraKeys(Map<String, Object> yaml) {
*
* @param conf the metric configuration
* @param is the InputStream with the YAML rules
* @param id identifier of the YAML ruleset, such as a filename
* @param description description of the YAML ruleset like file name or classpath resource
* @throws IllegalArgumentException when unable to parse YAML
*/
public void addMetricDefsTo(MetricConfiguration conf, InputStream is, String id) {
public void addMetricDefsTo(MetricConfiguration conf, InputStream is, String description) {
try {
JmxConfig config = loadConfig(is);
logger.log(INFO, "{0}: found {1} metric rules", new Object[] {id, config.getRules().size()});
logger.log(
INFO,
"{0}: found {1} metric rules",
new Object[] {description, config.getRules().size()});
config.addMetricDefsTo(conf);
} catch (Exception exception) {
// It is essential that the parser exception is made visible to the user.
// It contains contextual information about any syntax issues found by the parser.
String msg =
String.format(
"Failed to parse YAML rules from %s: %s %s",
id, rootCause(exception), exception.getMessage());
description, rootCause(exception), exception.getMessage());
throw new IllegalArgumentException(msg, exception);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import io.opentelemetry.api.OpenTelemetry;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -27,29 +28,41 @@ void createDefault() {
@Test
void missingClasspathTarget() {
JmxTelemetryBuilder builder = JmxTelemetry.builder(OpenTelemetry.noop());
assertThatThrownBy(() -> builder.addClassPathRules("should-not-exist"))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> builder.addRules(null, "something is missing"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("not found")
.hasMessageContaining("something is missing");
}

@Test
void invalidClasspathTarget() {
JmxTelemetryBuilder builder = JmxTelemetry.builder(OpenTelemetry.noop());
assertThatThrownBy(() -> builder.addClassPathRules("invalid"))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> addClasspathRules(builder, "jmx/rules/invalid.yaml"))
.isInstanceOf(IllegalArgumentException.class)
.describedAs("must have an exception message including the invalid resource path")
.hasMessageContaining("jmx/rules/invalid.yaml");
}

@Test
void knownClassPathTarget() {
JmxTelemetry.builder(OpenTelemetry.noop()).addClassPathRules("jvm").build();
void knownValidYaml() {
JmxTelemetryBuilder jmxtelemetry = JmxTelemetry.builder(OpenTelemetry.noop());
addClasspathRules(jmxtelemetry, "jmx/rules/jvm.yaml");
assertThat(jmxtelemetry.build()).isNotNull();
}

private static void addClasspathRules(JmxTelemetryBuilder builder, String path) {
InputStream input = JmxTelemetryTest.class.getClassLoader().getResourceAsStream(path);
builder.addRules(input, path);
}

@Test
void invalidExternalYaml(@TempDir Path dir) throws Exception {
Path invalid = Files.createTempFile(dir, "invalid", ".yaml");
Files.write(invalid, ":this !is /not YAML".getBytes(StandardCharsets.UTF_8));
JmxTelemetryBuilder builder = JmxTelemetry.builder(OpenTelemetry.noop());
assertThatThrownBy(() -> builder.addCustomRules(invalid))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> builder.addRules(invalid))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining(invalid.toString());
}

@Test
Expand Down
Loading