Skip to content
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

jmx metrics attribute lowercase modifier #13385

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
26 changes: 26 additions & 0 deletions instrumentation/jmx-metrics/javaagent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,31 @@ In the particular case where only two values are defined, we can simplify furthe
off: '*'
```

### Metric attributes modifiers

JMX attributes values may require modification or normalization in order to fit semantic conventions.

For example, with JVM memory, the `java.lang:name=*,type=MemoryPool` MBeans have `type` attribute with either `HEAP` or `NON_HEAP` value.
However, in the semantic conventions the metric attribute `jvm.memory.type` should be lower-cased to fit the `jvm.memory.used` definition, in this case we can
apply the `lowercase` metric attribute transformation as follows:


```yaml
---
rules:
- bean: java.lang:name=*,type=MemoryPool
mapping:
Usage.used:
type: updowncounter
metric: jvm.memory.used
unit: By
metricAttribute:
jvm.memory.pool.name : param(name)
jvm.memory.type: lowercase(beanattr(type))
```

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

### 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 All @@ -293,6 +318,7 @@ rules: # start of list of configuration rules
metricAttribute: # optional metric attributes, they apply to all metrics below
<ATTRIBUTE1>: param(<PARAM>) # <PARAM> is used as the key to extract value from actual ObjectName
<ATTRIBUTE2>: beanattr(<ATTR>) # <ATTR> is used as the MBean attribute name to extract the value
<ATTRIBUTE3>: const(<CONST>) # <CONST> is used as a constant
prefix: <METRIC_NAME_PREFIX> # optional, useful for avoiding specifying metric names below
unit: <UNIT> # optional, redefines the default unit for the whole rule
type: <TYPE> # optional, redefines the default type for the whole rule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package io.opentelemetry.instrumentation.jmx.engine;

import javax.annotation.Nullable;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;

Expand All @@ -30,7 +31,8 @@ public String getAttributeName() {
return name;
}

String acquireAttributeValue(MBeanServerConnection connection, ObjectName objectName) {
@Nullable
public String acquireAttributeValue(MBeanServerConnection connection, ObjectName objectName) {
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] extractor return value can be null

return extractor.extractValue(connection, objectName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package io.opentelemetry.instrumentation.jmx.engine;

import java.util.Locale;
import javax.annotation.Nullable;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
Expand Down Expand Up @@ -35,10 +36,31 @@ static MetricAttributeExtractor fromObjectNameParameter(String parameterKey) {
if (parameterKey.isEmpty()) {
throw new IllegalArgumentException("Empty parameter name");
}
return (dummy, objectName) -> objectName.getKeyProperty(parameterKey);
return (dummy, objectName) -> {
if (objectName == null) {
throw new IllegalArgumentException("missing object name");
}
return objectName.getKeyProperty(parameterKey);
};
}

static MetricAttributeExtractor fromBeanAttribute(String attributeName) {
return BeanAttributeExtractor.fromName(attributeName);
}

/**
* Provides an extractor that will transform provided string value in lower-case
*
* @param extractor extractor to wrap
* @return lower-case extractor
*/
static MetricAttributeExtractor toLowerCase(MetricAttributeExtractor extractor) {
return (a, b) -> {
String value = extractor.extractValue(a, b);
if (value != null) {
value = value.toLowerCase(Locale.ROOT);
}
return value;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,18 @@ private List<MetricAttribute> addMetricAttributes(Map<String, Object> metricAttr
return list;
}

private static MetricAttribute buildMetricAttribute(String key, String target) {
// package protected for testing
static MetricAttribute buildMetricAttribute(String key, String target) {
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] making this package-private makes unit-testing easier in MetricStructureTest (see below).


// an optional modifier may wrap the target
// - lowercase(param(STRING))
boolean lowercase = false;
if (target.startsWith("lowercase(")) {
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 hesitating if it should be ''lowercase' or 'lowerCase'.
Currently it looks like we have no consistent convention in the yaml file. We already have camelcase and lowercase naming used, for example metricAttribute and beanattr.
I understand that you followed existing attribute extractor yaml naming pattern, but this is inconsistent with the rest of yaml names and maybe this is one more item to discuss on the SIG meeting

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I really don't have any strong opinion on this, there might be a few inconsistencies but keeping it in line with param, beanattr and const should be a safe choice for now.

If we aim to fix those inconsistencies, I would suggest to do it in a separate PR and it would be relevant to also check what is the convention in the declarative configuration because at some point the JMX yaml configuration will be nested in it.

lowercase = true;
target = target.substring(10, target.lastIndexOf(')'));
}

//
// The recognized forms of target are:
// - param(STRING)
// - beanattr(STRING)
Expand All @@ -145,24 +156,32 @@ private static MetricAttribute buildMetricAttribute(String key, String target) {
// or the direct value to use
int k = target.indexOf(')');

MetricAttributeExtractor extractor = null;

// Check for one of the cases as above
if (target.startsWith("param(")) {
if (k > 0) {
String jmxAttribute = target.substring(6, k).trim();
return new MetricAttribute(
key, MetricAttributeExtractor.fromObjectNameParameter(jmxAttribute));
extractor = MetricAttributeExtractor.fromObjectNameParameter(jmxAttribute);
}
} else if (target.startsWith("beanattr(")) {
if (k > 0) {
String jmxAttribute = target.substring(9, k).trim();
return new MetricAttribute(key, MetricAttributeExtractor.fromBeanAttribute(jmxAttribute));
extractor = MetricAttributeExtractor.fromBeanAttribute(jmxAttribute);
}
} else if (target.startsWith("const(")) {
if (k > 0) {
String constantValue = target.substring(6, k).trim();
return new MetricAttribute(key, MetricAttributeExtractor.fromConstant(constantValue));
extractor = MetricAttributeExtractor.fromConstant(constantValue);
}
}
if (extractor != null) {
if (lowercase) {
extractor = MetricAttributeExtractor.toLowerCase(extractor);
}

return new MetricAttribute(key, extractor);
}

String msg = "Invalid metric attribute specification for '" + key + "': " + target;
throw new IllegalArgumentException(msg);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,6 @@ void testConf8() throws Exception {
@Test
void testStateMetricConf() throws Exception {
JmxConfig config = parseConf(CONF9);
assertThat(config).isNotNull();

List<JmxRule> rules = config.getRules();
assertThat(rules).hasSize(1);
Expand Down Expand Up @@ -452,6 +451,39 @@ void testStateMetricConf() throws Exception {
});
}

private static final String CONF10 =
"--- # keep stupid spotlessJava at bay\n"
+ "rules:\n"
+ " - bean: my-test:type=10_Hello\n"
+ " mapping:\n"
+ " jmxAttribute:\n"
+ " type: counter\n"
+ " metric: my_metric\n"
+ " metricAttribute:\n"
+ " to_lower_const: lowercase(const(Hello))\n"
+ " to_lower_attribute: lowercase(beanattr(beanAttribute))\n"
+ " to_lower_param: lowercase(param(type))\n";

@Test
void attributeValueLowercase() {

JmxConfig config = parseConf(CONF10);
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] here we only test the parsing as we can rely on unit tests in MetricStructureTest to validate the expected value modifications.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, however it would be nice to add also some negative test to verify parsing errors related to use of lowercase(...)


List<JmxRule> rules = config.getRules();
assertThat(rules).hasSize(1);
JmxRule jmxRule = rules.get(0);

assertThat(jmxRule.getBean()).isEqualTo("my-test:type=10_Hello");
Metric metric = jmxRule.getMapping().get("jmxAttribute");
assertThat(metric.getMetricType()).isEqualTo(MetricInfo.Type.COUNTER);
assertThat(metric.getMetric()).isEqualTo("my_metric");
assertThat(metric.getMetricAttribute())
.hasSize(3)
.containsEntry("to_lower_const", "lowercase(const(Hello))")
.containsEntry("to_lower_attribute", "lowercase(beanattr(beanAttribute))")
.containsEntry("to_lower_param", "lowercase(param(type))");
}

@Test
void testEmptyConf() {
JmxConfig config = parseConf(EMPTY_CONF);
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

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

It's great you added this test !

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.jmx.yaml;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import io.opentelemetry.instrumentation.jmx.engine.MetricAttribute;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

public class MetricStructureTest {

@ParameterizedTest
@CsvSource({"const(Hello),Hello", "lowercase(const(Hello)),hello"})
void metricAttribute_constant(String target, String expectedValue) {
MetricAttribute ma = MetricStructure.buildMetricAttribute("name", target);
assertThat(ma.getAttributeName()).isEqualTo("name");
assertThat(ma.isStateAttribute()).isFalse();
assertThat(ma.acquireAttributeValue(null, null)).isEqualTo(expectedValue);
}

@ParameterizedTest
@CsvSource({
"beanattr(beanAttribute),Hello,Hello",
"lowercase(beanattr(beanAttribute)),Hello,hello",
})
void metricAttribute_beanAttribute(String target, String value, String expectedValue)
throws Exception {
MetricAttribute ma = MetricStructure.buildMetricAttribute("name", target);
assertThat(ma.getAttributeName()).isEqualTo("name");
assertThat(ma.isStateAttribute()).isFalse();

ObjectName objectName = new ObjectName("test:name=_beanAttribute");
MBeanServerConnection mockConnection = mock(MBeanServerConnection.class);

MBeanInfo mockBeanInfo = mock(MBeanInfo.class);
when(mockBeanInfo.getAttributes())
.thenReturn(
new MBeanAttributeInfo[] {
new MBeanAttributeInfo("beanAttribute", "java.lang.String", "", true, false, false)
});
when(mockConnection.getMBeanInfo(objectName)).thenReturn(mockBeanInfo);
when(mockConnection.getAttribute(objectName, "beanAttribute")).thenReturn(value);

assertThat(ma.acquireAttributeValue(mockConnection, objectName)).isEqualTo(expectedValue);
}

@ParameterizedTest
@CsvSource({
"param(name),Hello,Hello",
"lowercase(param(name)),Hello,hello",
})
void metricAttribute_beanParam(String target, String value, String expectedValue)
throws Exception {
MetricAttribute ma = MetricStructure.buildMetricAttribute("name", target);
assertThat(ma.getAttributeName()).isEqualTo("name");
assertThat(ma.isStateAttribute()).isFalse();

ObjectName objectName = new ObjectName("test:name=" + value);
MBeanServerConnection mockConnection = mock(MBeanServerConnection.class);

assertThat(ma.acquireAttributeValue(mockConnection, objectName)).isEqualTo(expectedValue);
}
}
Loading