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 all 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
34 changes: 34 additions & 0 deletions instrumentation/jmx-metrics/javaagent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,40 @@ rules:

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

### Unit conversions

Sometimes JMX attribute values are reported in units that are 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 an optional `sourceUnit` metric property together with the `unit` metric property.
`sourceUnit` defines the native unit of value retrieved from JMX attribute, while `unit` defines the unit of the metric reported to the backend.
If a 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 |
| us | 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 @@ -20,6 +20,7 @@
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 +62,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 +71,12 @@ void enrollExtractor(
? metricInfo.getDescription()
: attributeInfo.getDescription();
String unit = metricInfo.getUnit();
String sourceUnit = metricInfo.getSourceUnit();

UnitConverter unitConverter = UnitConverter.getInstance(sourceUnit, unit);
if (unitConverter != null) {
recordDoubleValue = true;
}

switch (instrumentType) {
// CHECKSTYLE:OFF
Expand All @@ -77,10 +85,10 @@ 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));
}
Expand All @@ -94,10 +102,10 @@ 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));
}
Expand All @@ -111,10 +119,10 @@ 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));
}
Expand All @@ -133,8 +141,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 +155,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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.jmx.engine;

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

/**
* This class is responsible for converting a value between metric units using defined conversion
* algorithms.
*/
class UnitConverter {
private static final Map<String, UnitConverter> conversionMappings = new HashMap<>();

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

private final Function<Number, Number> convertingFunction;

/**
* Get an instance of converter that is able to convert a value from a given source to a target
* unit.
*
* @param sourceUnit a source unit supported by requested converter
* @param targetUnit a target unit supported by requested converter
* @return an instance of converter, or {@literal null} if {@code sourceUnit} is {@literal null}
* or empty, which means that there is no conversion needed.
* @throws IllegalArgumentException if {@code targetUnit} is empty, or matching converter was not
* found for provided units.
*/
@Nullable
public static UnitConverter getInstance(@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 = buildConverterKey(sourceUnit, targetUnit);
UnitConverter converter = conversionMappings.get(converterKey);
if (converter == null) {
throw new IllegalArgumentException(
"Unsupported conversion from [" + sourceUnit + "] to [" + targetUnit + "]");
}

return converter;
}

/**
* Register new converter instance that can then be retrieved with {@link #getInstance(String,
* String)}.
*
* @param sourceUnit a source unit supported by the converter
* @param targetUnit a target unit supported by the converter
* @param convertingFunction a function that implements algorithm of conversion between {@code
* sourceUnit} and {@code targetUnit}
* @throws IllegalArgumentException if source or target unit is empty, or when there is converter
* already registered for given {@code sourceUnit} and {@code targetUnit}
*/
// visible for testing
static void registerConversion(
String sourceUnit, String targetUnit, Function<Number, Number> convertingFunction) {
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 = buildConverterKey(sourceUnit, targetUnit);

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

private static String buildConverterKey(String sourceUnit, String targetUnit) {
return sourceUnit + "->" + targetUnit;
}

/**
* Create an instance of converter
*
* @param convertingFunction an algorithm applied when converting value
*/
UnitConverter(Function<Number, Number> convertingFunction) {
this.convertingFunction = convertingFunction;
}

public Number convert(Number value) {
return convertingFunction.apply(value);
}
}
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class Metric extends MetricStructure {
@Nullable private String metric;
@Nullable private String desc;

@Nullable
public String getMetric() {
return metric;
}
Expand All @@ -35,6 +36,7 @@ private String validateMetricName(String name) {
return name;
}

@Nullable
public String getDesc() {
return desc;
}
Expand All @@ -47,6 +49,7 @@ public void setDesc(String desc) {
MetricInfo buildMetricInfo(
@Nullable String prefix,
String attributeName,
String defaultSourceUnit,
String defaultUnit,
MetricInfo.Type defaultType) {
String metricName;
Expand All @@ -61,11 +64,16 @@ MetricInfo buildMetricInfo(
metricType = defaultType;
}

String sourceUnit = getSourceUnit();
if (sourceUnit == null) {
sourceUnit = defaultSourceUnit;
}

String unit = getUnit();
if (unit == null) {
unit = defaultUnit;
}

return new MetricInfo(metricName, desc, unit, metricType);
return new MetricInfo(metricName, desc, sourceUnit, unit, metricType);
}
}
Loading
Loading