Skip to content

Allow producing complex jmx based metrics#18782

Open
laurit wants to merge 16 commits into
open-telemetry:mainfrom
laurit:jmx-handler
Open

Allow producing complex jmx based metrics#18782
laurit wants to merge 16 commits into
open-telemetry:mainfrom
laurit:jmx-handler

Conversation

@laurit
Copy link
Copy Markdown
Contributor

@laurit laurit commented May 18, 2026

Resolves #17433
Some metrics are too complex to be expressed in yaml. This PR allows implementing such metrics in code.

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Breaking Change Documentation Required

This PR has been labeled as a breaking change. Please ensure you provide the following information:

Migration Notes Required

Please add detailed migration notes to help users understand:

  • What is changing and why
  • How to update their code/configuration
  • Any alternative approaches if applicable
  • Code examples showing before/after usage

Checklist

  • Migration notes added to the PR description
  • Breaking change is documented in the "Unreleased" section of the Changelog, preferably as part of this PR.
  • Consider if this change requires a major version bump

Your migration notes will be included in the release notes to help users upgrade smoothly. The more detailed and helpful they are, the better the user experience will be.


This comment was automatically generated because the breaking change label was applied to this PR.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the JMX metrics library to support “complex” metrics produced by custom, user-provided logic via an SPI (JmxMetricHandler) referenced from YAML rules, and wires handler discovery through an injectable ComponentLoader/service classloader.

Changes:

  • Add JmxMetricHandler SPI and YAML support (handler / handlers) to bind rules to custom metric producers.
  • Plumb a ComponentLoader from JmxTelemetryBuilder into the engine so handlers can be discovered via SPI at runtime.
  • Add lifecycle management for created instruments (returning AutoCloseable handles and closing registered instruments).

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
instrumentation/jmx-metrics/library/src/test/resources/jmx/rules/handler.yaml Adds a test rule referencing a custom handler by name.
instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/rules/HandlerTest.java New test validating handler discovery via SPI and metric emission.
instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/internal/engine/MetricAggregationTest.java Updates MetricDef construction for the new handlers parameter.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/JmxTelemetryBuilder.java Adds service classloader injection and passes a ComponentLoader into JmxTelemetry.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/JmxTelemetry.java Passes ComponentLoader into the engine and changes start* to return AutoCloseable.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/JmxMetricHandler.java Introduces the public SPI for custom metric creation.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/yaml/RuleParser.java Parses handler(s) keys and allows rules with handlers but no mapping.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/yaml/JmxRule.java Stores handler references and builds MetricDef with handler names.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/engine/MetricRegistrar.java Tracks created instruments and closes them; adds handler enrollment.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/engine/MetricHandlerHolder.java New holder for handler + detection status to support (intended) refresh.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/engine/MetricExtractor.java Makes status updates synchronized and returns “first enrollment” flag.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/engine/MetricDef.java Adds handler list to metric definitions.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/engine/JmxMetricInsight.java Loads handlers via ComponentLoader and returns an AutoCloseable lifecycle handle.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/engine/BeanFinder.java Validates handler names and enrolls handlers when beans are found.
instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/engine/BeanAttributeExtractor.java Minor modernization (toArray(new String[0])) and annotation cleanup.
Comments suppressed due to low confidence (1)

instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/engine/BeanFinder.java:205

  • resolveHandlers() creates a new MetricHandlerHolder on every refreshState() run. Since refreshState() is rescheduled indefinitely, this makes MetricRegistrar.enrollHandler() treat every run as a first enrollment, repeatedly creating/registering the same handler instruments and leaking resources/duplicating metrics. Persist the holder(s) across refresh cycles (e.g., store MetricHandlerHolder instances in MetricDef or in BeanFinder keyed by rule+handlerName) and only create instruments once, updating status on subsequent refreshes (similar to MetricExtractor).
  private void resolveHandlers(
      Set<ObjectName> objectNames, MBeanServerConnection connection, MetricDef metricDef) {
    for (String handlerName : metricDef.getHandlers()) {
      JmxMetricHandler handler = handlers.get(handlerName);
      // we remove rules that reference unknown handlers during construction, so this should never
      // happen
      if (handler == null) {
        throw new IllegalStateException("Unknown handler " + handlerName);
      }
      MetricHandlerHolder holder = new MetricHandlerHolder(handler);
      registrar.enrollHandler(connection, objectNames, holder);
    }

Comment on lines 43 to 55
/** Starts JMX metrics collection on current JVM */
public AutoCloseable start() {
return start(() -> MBeanServerFactory.findMBeanServer(null));
}

/**
* Starts JMX metrics collection on provided (local or remote) connections
*
* @param connections connection provider
* @return this
*/
@CanIgnoreReturnValue
public JmxTelemetry start(Supplier<List<? extends MBeanServerConnection>> connections) {
service.start(metricConfiguration, connections);
return this;
public AutoCloseable start(Supplier<List<? extends MBeanServerConnection>> connections) {
return service.start(metricConfiguration, connections, componentLoader);
}
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/engine/MetricHandlerHolder.java:27

  • [Naming] handlerClassName / getHandlerClassName() is misleading: the YAML handler value is matched against JmxMetricHandler.getName(), not a class name. Renaming this to handlerName (and corresponding accessor) would make the intent clearer and reduce confusion for future maintenance.
  private final String handlerClassName;
  @Nullable private JmxMetricHandler handler;
  @Nullable private volatile DetectionStatus status;

  public MetricHandlerHolder(String handlerClassName) {
    this.handlerClassName = handlerClassName;
  }

  String getHandlerClassName() {
    return handlerClassName;
  }

Comment thread CHANGELOG.md Outdated
@laurit laurit requested a review from Copilot May 19, 2026 14:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Comment on lines +15 to +31
public class MetricHandlerHolder {

private final String handlerName;
@Nullable private JmxMetricHandler handler;
@Nullable private volatile DetectionStatus status;

public MetricHandlerHolder(String handlerName) {
this.handlerName = handlerName;
}

String getHandlerName() {
return handlerName;
}

JmxMetricHandler getHandler() {
return handler;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

MetricHandlerHolder is used from a different package and can't be package private

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/internal/engine/MetricRegistrar.java:255

  • close() catches all Exceptions from instrument cleanup but doesn’t preserve the thread interrupt status when an InterruptedException is caught (it will be swallowed and the interrupt flag cleared). Consider special-casing InterruptedException to re-interrupt the thread after logging.
  @Override
  public void close() {
    for (AutoCloseable instrument : instruments) {
      try {
        instrument.close();
      } catch (Exception e) {
        logger.log(WARNING, "Failed to close metric instrument", e);
      }
    }
    instruments.clear();

laurit and others added 2 commits May 20, 2026 09:59
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.

Comment on lines +34 to +37
void setHandler(JmxMetricHandler handler) {
if (this.handler != null) {
throw new IllegalStateException("Handler is already set");
}
Copy link
Copy Markdown
Contributor Author

@laurit laurit May 20, 2026

Choose a reason for hiding this comment

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

This isn't enough to make calling start multiple times work.

@laurit laurit marked this pull request as ready for review May 20, 2026 07:54
@laurit laurit requested a review from a team as a code owner May 20, 2026 07:54
@laurit laurit added this to the v2.29.0 milestone May 26, 2026
@SylvainJuge
Copy link
Copy Markdown
Contributor

I think that it would be better to make the JmxMetricHandler SPI interface a generic way to capture JMX metrics, without even having to use YAML to use them.

Then, all the available JmxMetricHandler are registered by name and we should be able to use them in otel.jmx.target.system. We could then refactor all the existing YAML file definitions to each have their own JmxMetricHandler implementation and be loaded using this SPI.

@laurit
Copy link
Copy Markdown
Contributor Author

laurit commented May 26, 2026

I think that it would be better to make the JmxMetricHandler SPI interface a generic way to capture JMX metrics, without even having to use YAML to use them.

I chose to do it the way it is under the assumption that the vast majority of metrics can be expressed in yaml and there are going to be only a few that are too complicated. Since we have one yaml for each target that describes all the metrics I thought it would nice to have a reference from the yaml to these complex metrics.
If we don't want to use the yaml I guess then we'd need to add something like Collection<String> getJmxObjectNames() to know which beans to monitor.

We could then refactor all the existing YAML file definitions to each have their own JmxMetricHandler implementation and be loaded using this SPI.

Currently the JmxMetricHandler operates on the bean defined in yaml so you'd most likely need to create multiple handler for each yaml.

Would it help if I named JmxMetricHandler to ExperimentalJmxMetricHandler and perhaps moved it to an internal package?

@SylvainJuge
Copy link
Copy Markdown
Contributor

I chose to do it the way it is under the assumption that the vast majority of metrics can be expressed in yaml and there are going to be only a few that are too complicated

I think this is fine to keep it this way, I was just thinking about the possibility to have such an abstraction. The only use-case I would have for this is to provide an alternate YAML format, for example using the one used by weaver/semconv.

Would it help if I named JmxMetricHandler to ExperimentalJmxMetricHandler and perhaps moved it to an internal package?

Yes, given we likely need to validate in a few iterations the design of this API with a few user-provided implementations then it would be a good idea to make it internal and have "Experimental" in its name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for iterating and aggregating over list-valued MBean attributes in YAML rules

3 participants