Skip to content

JMX metrics unit conversion #13448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions instrumentation/jmx-metrics/javaagent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,39 @@ rules:

For now, only the `lowercase` transformation is supported, other additions might be added in the future if needed.

### Unit conversions

Sometimes JMX attributes values are reported in units not aligned with semantic conventions.
For example duration values are usually reported as milliseconds while semantic conventions recommend using seconds.

This issue can be solved by providing optional `sourceUnit` metric property together with `unit` metric property.
`sourceUnit` defines native unit of value retrieved from JMX attribute, while `unit` defines a semantic convention compatible metric unit that will be reported to the backend.
If conversion between `sourceUnit` and `unit` is available then it is automatically applied before reporting the metric.
If such a conversion is not available then an error is reported during JMX metrics processing.

Currently available unit conversions:

| `sourceUnit` | `unit` |
|-------------|-------|
| ms | s |
| ns | s |

Example of defining unit conversion in yaml file:
```yaml
rules:
- beans:
- Catalina:type=GlobalRequestProcessor,name=*
prefix: http.server.tomcat.
mapping:
maxTime:
metric: maxTime
type: gauge
sourceUnit: ms
unit: s
desc: The longest request processing time
```
`sourceUnit` can also be defined on rule level (see [Making shortcuts](#making-shortcuts))

### General Syntax

Here is the general description of the accepted configuration file syntax. The whole contents of the file is case-sensitive, with exception for `type` as described in the table below.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@
// new MetricInfo(
// "my.own.jvm.memory.pool.used",
// "Pool memory currently used",
// null,
// "By",
// MetricInfo.Type.UPDOWNCOUNTER);
// MetricInfo poolLimitInfo =
// new MetricInfo(
// "my.own.jvm.memory.pool.limit",
// "Maximum obtainable memory pool size",
// null,
// "By",
// MetricInfo.Type.UPDOWNCOUNTER);
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,28 @@ public enum Type {
// How to report the metric using OpenTelemetry API
private final String metricName; // used as Instrument name
@Nullable private final String description;
@Nullable private final String unit;
@Nullable private final String sourceUnit;
private final String unit;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

[for reviewer] unit is required by semconv, so it should not be nullable

private final Type type;

/**
* Constructor for MetricInfo.
*
* @param metricName a String that will be used as a metric name, it should be unique
* @param description a human readable description of the metric
* @param sourceUnit a human readable unit of measurement that is received from metric source
* @param unit a human readable unit of measurement
* @param type the instrument typ to be used for the metric
*/
public MetricInfo(
String metricName, @Nullable String description, String unit, @Nullable Type type) {
String metricName,
@Nullable String description,
@Nullable String sourceUnit,
String unit,
@Nullable Type type) {
this.metricName = metricName;
this.description = description;
this.sourceUnit = sourceUnit;
this.unit = unit;
this.type = type == null ? Type.GAUGE : type;
}
Expand All @@ -56,6 +63,10 @@ public String getDescription() {
}

@Nullable
public String getSourceUnit() {
return sourceUnit;
}

public String getUnit() {
return unit;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.metrics.ObservableDoubleMeasurement;
import io.opentelemetry.api.metrics.ObservableLongMeasurement;
import io.opentelemetry.instrumentation.jmx.engine.unit.UnitConverter;
import io.opentelemetry.instrumentation.jmx.engine.unit.UnitConverterFactory;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;

Expand Down Expand Up @@ -61,6 +64,7 @@ void enrollExtractor(
return;
}

boolean recordDoubleValue = attributeInfo.usesDoubleValues();
MetricInfo metricInfo = extractor.getInfo();
String metricName = metricInfo.getMetricName();
MetricInfo.Type instrumentType = metricInfo.getType();
Expand All @@ -69,6 +73,12 @@ void enrollExtractor(
? metricInfo.getDescription()
: attributeInfo.getDescription();
String unit = metricInfo.getUnit();
String sourceUnit = metricInfo.getSourceUnit();

UnitConverter unitConverter = UnitConverterFactory.getConverter(sourceUnit, unit);
if (unitConverter != null) {
recordDoubleValue = unitConverter.isConvertingToDouble();
}

switch (instrumentType) {
// CHECKSTYLE:OFF
Expand All @@ -77,12 +87,12 @@ void enrollExtractor(
// CHECKSTYLE:ON
LongCounterBuilder builder = meter.counterBuilder(metricName);
Optional.ofNullable(description).ifPresent(builder::setDescription);
Optional.ofNullable(unit).ifPresent(builder::setUnit);
builder.setUnit(unit);

if (attributeInfo.usesDoubleValues()) {
builder.ofDoubles().buildWithCallback(doubleTypeCallback(extractor));
if (recordDoubleValue) {
builder.ofDoubles().buildWithCallback(doubleTypeCallback(extractor, unitConverter));
} else {
builder.buildWithCallback(longTypeCallback(extractor));
builder.buildWithCallback(longTypeCallback(extractor, unitConverter));
}
logger.log(INFO, "Created Counter for {0}", metricName);
}
Expand All @@ -94,12 +104,12 @@ void enrollExtractor(
// CHECKSTYLE:ON
LongUpDownCounterBuilder builder = meter.upDownCounterBuilder(metricName);
Optional.ofNullable(description).ifPresent(builder::setDescription);
Optional.ofNullable(unit).ifPresent(builder::setUnit);
builder.setUnit(unit);

if (attributeInfo.usesDoubleValues()) {
builder.ofDoubles().buildWithCallback(doubleTypeCallback(extractor));
if (recordDoubleValue) {
builder.ofDoubles().buildWithCallback(doubleTypeCallback(extractor, unitConverter));
} else {
builder.buildWithCallback(longTypeCallback(extractor));
builder.buildWithCallback(longTypeCallback(extractor, unitConverter));
}
logger.log(INFO, "Created UpDownCounter for {0}", metricName);
}
Expand All @@ -111,12 +121,12 @@ void enrollExtractor(
// CHECKSTYLE:ON
DoubleGaugeBuilder builder = meter.gaugeBuilder(metricName);
Optional.ofNullable(description).ifPresent(builder::setDescription);
Optional.ofNullable(unit).ifPresent(builder::setUnit);
builder.setUnit(unit);

if (attributeInfo.usesDoubleValues()) {
builder.buildWithCallback(doubleTypeCallback(extractor));
if (recordDoubleValue) {
builder.buildWithCallback(doubleTypeCallback(extractor, unitConverter));
} else {
builder.ofLongs().buildWithCallback(longTypeCallback(extractor));
builder.ofLongs().buildWithCallback(longTypeCallback(extractor, unitConverter));
}
logger.log(INFO, "Created Gauge for {0}", metricName);
}
Expand All @@ -133,8 +143,10 @@ void enrollExtractor(
/*
* A method generating metric collection callback for asynchronous Measurement
* of Double type.
* If unit converter is provided then conversion is applied before metric is recorded.
*/
static Consumer<ObservableDoubleMeasurement> doubleTypeCallback(MetricExtractor extractor) {
static Consumer<ObservableDoubleMeasurement> doubleTypeCallback(
MetricExtractor extractor, @Nullable UnitConverter unitConverter) {
return measurement -> {
DetectionStatus status = extractor.getStatus();
if (status != null) {
Expand All @@ -145,6 +157,10 @@ static Consumer<ObservableDoubleMeasurement> doubleTypeCallback(MetricExtractor
if (metricValue != null) {
// get the metric attributes
Attributes attr = createMetricAttributes(connection, objectName, extractor);

if (unitConverter != null) {
metricValue = unitConverter.convert(metricValue);
}
measurement.record(metricValue.doubleValue(), attr);
}
}
Expand All @@ -155,8 +171,10 @@ static Consumer<ObservableDoubleMeasurement> doubleTypeCallback(MetricExtractor
/*
* A method generating metric collection callback for asynchronous Measurement
* of Long type.
* If unit converter is provided then conversion is applied before metric is recorded.
*/
static Consumer<ObservableLongMeasurement> longTypeCallback(MetricExtractor extractor) {
static Consumer<ObservableLongMeasurement> longTypeCallback(
MetricExtractor extractor, @Nullable UnitConverter unitConverter) {
return measurement -> {
DetectionStatus status = extractor.getStatus();
if (status != null) {
Expand All @@ -167,6 +185,10 @@ static Consumer<ObservableLongMeasurement> longTypeCallback(MetricExtractor extr
if (metricValue != null) {
// get the metric attributes
Attributes attr = createMetricAttributes(connection, objectName, extractor);

if (unitConverter != null) {
metricValue = unitConverter.convert(metricValue);
}
measurement.record(metricValue.longValue(), attr);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.jmx.engine.unit;

import java.util.function.Function;

/** This class is responsible for converting a value using provided algorithm. */
public class UnitConverter {
private final Function<Number, Number> convertingFunction;
private final boolean convertToDouble;

/**
* Create an instance of converter
*
* @param convertingFunction an algorithm applied when converting value
* @param convertToDouble indicates of algorithm will return floating point result. This must be
* in-sync with algorithm implementation.
*/
UnitConverter(Function<Number, Number> convertingFunction, boolean convertToDouble) {
this.convertingFunction = convertingFunction;
this.convertToDouble = convertToDouble;
}

public Number convert(Number value) {
return convertingFunction.apply(value);
}

public boolean isConvertingToDouble() {
return convertToDouble;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.jmx.engine.unit;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import javax.annotation.Nullable;

public class UnitConverterFactory {
private static final Map<String, UnitConverter> conversionMappings = new HashMap<>();

static {
registerConverter("ms", "s", value -> value.doubleValue() / TimeUnit.SECONDS.toMillis(1), true);
registerConverter("ns", "s", value -> value.doubleValue() / TimeUnit.SECONDS.toNanos(1), true);
}

private UnitConverterFactory() {}

@Nullable
public static UnitConverter getConverter(@Nullable String sourceUnit, String targetUnit) {
if (targetUnit.isEmpty()) {
throw new IllegalArgumentException("Non empty targetUnit must be provided");
}

if (sourceUnit == null || sourceUnit.isEmpty()) {
// No conversion is needed
return null;
}

String converterKey = getConverterKey(sourceUnit, targetUnit);
UnitConverter converter = conversionMappings.get(converterKey);
if (converter == null) {
throw new IllegalStateException(
"No [" + sourceUnit + "] to [" + targetUnit + "] unit converter");
}

return converter;
}

// visible for testing
static void registerConverter(
String sourceUnit,
String targetUnit,
Function<Number, Number> convertingFunction,
boolean convertToDouble) {
if (sourceUnit.isEmpty()) {
throw new IllegalArgumentException("Non empty sourceUnit must be provided");
}
if (targetUnit.isEmpty()) {
throw new IllegalArgumentException("Non empty targetUnit must be provided");
}

String converterKey = getConverterKey(sourceUnit, targetUnit);

if (conversionMappings.containsKey(converterKey)) {
throw new IllegalArgumentException(
"Converter from [" + sourceUnit + "] to [" + targetUnit + "] already registered");
}
conversionMappings.put(converterKey, new UnitConverter(convertingFunction, convertToDouble));
}

private static String getConverterKey(String sourceUnit, String targetUnit) {
return sourceUnit + "->" + targetUnit;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class JmxRule extends MetricStructure {
@Nullable private String prefix;
private Map<String, Metric> mapping;

@Nullable
public String getBean() {
return bean;
}
Expand Down Expand Up @@ -90,6 +91,7 @@ private String validatePrefix(String prefix) {
return prefix;
}

@Nullable
public String getPrefix() {
return prefix;
}
Expand Down Expand Up @@ -152,10 +154,13 @@ public MetricDef buildMetricDef() throws Exception {
new MetricInfo(
prefix == null ? niceAttributeName : (prefix + niceAttributeName),
null,
getSourceUnit(),
getUnit(),
getMetricType());
} else {
metricInfo = m.buildMetricInfo(prefix, niceAttributeName, getUnit(), getMetricType());
metricInfo =
m.buildMetricInfo(
prefix, niceAttributeName, getSourceUnit(), getUnit(), getMetricType());
}

List<MetricAttribute> ownAttributes = getAttributeList();
Expand Down Expand Up @@ -226,6 +231,7 @@ protected Number extractNumericalAttribute(
new MetricInfo(
metricInfo.getMetricName(),
metricInfo.getDescription(),
metricInfo.getSourceUnit(),
metricInfo.getUnit(),
MetricInfo.Type.UPDOWNCOUNTER);

Expand Down
Loading
Loading