Skip to content

Commit 6c38fcb

Browse files
authored
jmx metrics attribute lowercase modifier (#13385)
1 parent 6775b03 commit 6c38fcb

File tree

6 files changed

+251
-24
lines changed

6 files changed

+251
-24
lines changed

instrumentation/jmx-metrics/javaagent/README.md

+26
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,31 @@ In the particular case where only two values are defined, we can simplify furthe
282282
off: '*'
283283
```
284284

285+
### Metric attributes modifiers
286+
287+
JMX attributes values may require modification or normalization in order to fit semantic conventions.
288+
289+
For example, with JVM memory, the `java.lang:name=*,type=MemoryPool` MBeans have `type` attribute with either `HEAP` or `NON_HEAP` value.
290+
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
291+
apply the `lowercase` metric attribute transformation as follows:
292+
293+
294+
```yaml
295+
---
296+
rules:
297+
- bean: java.lang:name=*,type=MemoryPool
298+
mapping:
299+
Usage.used:
300+
type: updowncounter
301+
metric: jvm.memory.used
302+
unit: By
303+
metricAttribute:
304+
jvm.memory.pool.name : param(name)
305+
jvm.memory.type: lowercase(beanattr(type))
306+
```
307+
308+
For now, only the `lowercase` transformation is supported, other additions might be added in the future if needed.
309+
285310
### General Syntax
286311

287312
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.
@@ -293,6 +318,7 @@ rules: # start of list of configuration rules
293318
metricAttribute: # optional metric attributes, they apply to all metrics below
294319
<ATTRIBUTE1>: param(<PARAM>) # <PARAM> is used as the key to extract value from actual ObjectName
295320
<ATTRIBUTE2>: beanattr(<ATTR>) # <ATTR> is used as the MBean attribute name to extract the value
321+
<ATTRIBUTE3>: const(<CONST>) # <CONST> is used as a constant
296322
prefix: <METRIC_NAME_PREFIX> # optional, useful for avoiding specifying metric names below
297323
unit: <UNIT> # optional, redefines the default unit for the whole rule
298324
type: <TYPE> # optional, redefines the default type for the whole rule

instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttribute.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package io.opentelemetry.instrumentation.jmx.engine;
77

8+
import javax.annotation.Nullable;
89
import javax.management.MBeanServerConnection;
910
import javax.management.ObjectName;
1011

@@ -30,7 +31,8 @@ public String getAttributeName() {
3031
return name;
3132
}
3233

33-
String acquireAttributeValue(MBeanServerConnection connection, ObjectName objectName) {
34+
@Nullable
35+
public String acquireAttributeValue(MBeanServerConnection connection, ObjectName objectName) {
3436
return extractor.extractValue(connection, objectName);
3537
}
3638
}

instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/engine/MetricAttributeExtractor.java

+23-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package io.opentelemetry.instrumentation.jmx.engine;
77

8+
import java.util.Locale;
89
import javax.annotation.Nullable;
910
import javax.management.MBeanServerConnection;
1011
import javax.management.ObjectName;
@@ -35,10 +36,31 @@ static MetricAttributeExtractor fromObjectNameParameter(String parameterKey) {
3536
if (parameterKey.isEmpty()) {
3637
throw new IllegalArgumentException("Empty parameter name");
3738
}
38-
return (dummy, objectName) -> objectName.getKeyProperty(parameterKey);
39+
return (dummy, objectName) -> {
40+
if (objectName == null) {
41+
throw new IllegalArgumentException("Missing object name");
42+
}
43+
return objectName.getKeyProperty(parameterKey);
44+
};
3945
}
4046

4147
static MetricAttributeExtractor fromBeanAttribute(String attributeName) {
4248
return BeanAttributeExtractor.fromName(attributeName);
4349
}
50+
51+
/**
52+
* Provides an extractor that will transform provided string value in lower-case
53+
*
54+
* @param extractor extractor to wrap
55+
* @return lower-case extractor
56+
*/
57+
static MetricAttributeExtractor toLowerCase(MetricAttributeExtractor extractor) {
58+
return (connection, objectName) -> {
59+
String value = extractor.extractValue(connection, objectName);
60+
if (value != null) {
61+
value = value.toLowerCase(Locale.ROOT);
62+
}
63+
return value;
64+
};
65+
}
4466
}

instrumentation/jmx-metrics/library/src/main/java/io/opentelemetry/instrumentation/jmx/yaml/MetricStructure.java

+69-21
Original file line numberDiff line numberDiff line change
@@ -136,36 +136,84 @@ private List<MetricAttribute> addMetricAttributes(Map<String, Object> metricAttr
136136
return list;
137137
}
138138

139-
private static MetricAttribute buildMetricAttribute(String key, String target) {
139+
// package protected for testing
140+
static MetricAttribute buildMetricAttribute(String key, String target) {
141+
String errorMsg =
142+
String.format("Invalid metric attribute expression for '%s' : '%s'", key, target);
143+
144+
String targetExpr = target;
145+
146+
// an optional modifier may wrap the target
147+
// - lowercase(param(STRING))
148+
boolean lowercase = false;
149+
String lowerCaseExpr = tryParseFunction("lowercase", targetExpr, target);
150+
if (lowerCaseExpr != null) {
151+
lowercase = true;
152+
targetExpr = lowerCaseExpr;
153+
}
154+
155+
//
140156
// The recognized forms of target are:
141157
// - param(STRING)
142158
// - beanattr(STRING)
143159
// - const(STRING)
144160
// where STRING is the name of the corresponding parameter key, attribute name,
145-
// or the direct value to use
146-
int k = target.indexOf(')');
147-
148-
// Check for one of the cases as above
149-
if (target.startsWith("param(")) {
150-
if (k > 0) {
151-
String jmxAttribute = target.substring(6, k).trim();
152-
return new MetricAttribute(
153-
key, MetricAttributeExtractor.fromObjectNameParameter(jmxAttribute));
154-
}
155-
} else if (target.startsWith("beanattr(")) {
156-
if (k > 0) {
157-
String jmxAttribute = target.substring(9, k).trim();
158-
return new MetricAttribute(key, MetricAttributeExtractor.fromBeanAttribute(jmxAttribute));
161+
// or the constant value to use
162+
163+
MetricAttributeExtractor extractor = null;
164+
165+
String paramName = tryParseFunction("param", targetExpr, target);
166+
if (paramName != null) {
167+
extractor = MetricAttributeExtractor.fromObjectNameParameter(paramName);
168+
}
169+
170+
if (extractor == null) {
171+
String attributeName = tryParseFunction("beanattr", targetExpr, target);
172+
if (attributeName != null) {
173+
extractor = MetricAttributeExtractor.fromBeanAttribute(attributeName);
159174
}
160-
} else if (target.startsWith("const(")) {
161-
if (k > 0) {
162-
String constantValue = target.substring(6, k).trim();
163-
return new MetricAttribute(key, MetricAttributeExtractor.fromConstant(constantValue));
175+
}
176+
177+
if (extractor == null) {
178+
String constantValue = tryParseFunction("const", targetExpr, target);
179+
if (constantValue != null) {
180+
extractor = MetricAttributeExtractor.fromConstant(constantValue);
164181
}
165182
}
166183

167-
String msg = "Invalid metric attribute specification for '" + key + "': " + target;
168-
throw new IllegalArgumentException(msg);
184+
if (extractor == null) {
185+
// expression did not match any supported syntax
186+
throw new IllegalArgumentException(errorMsg);
187+
}
188+
189+
if (lowercase) {
190+
extractor = MetricAttributeExtractor.toLowerCase(extractor);
191+
}
192+
return new MetricAttribute(key, extractor);
193+
}
194+
195+
/**
196+
* Parses a function expression for metric attributes
197+
*
198+
* @param function function name to attempt parsing from expression
199+
* @param expression expression to parse
200+
* @param errorMsg error message to use when syntax error is present
201+
* @return {@literal null} if expression does not start with function
202+
* @throws IllegalArgumentException if expression syntax is invalid
203+
*/
204+
private static String tryParseFunction(String function, String expression, String errorMsg) {
205+
if (!expression.startsWith(function)) {
206+
return null;
207+
}
208+
String expr = expression.substring(function.length()).trim();
209+
if (expr.charAt(0) != '(' || expr.charAt(expr.length() - 1) != ')') {
210+
throw new IllegalArgumentException(errorMsg);
211+
}
212+
expr = expr.substring(1, expr.length() - 1).trim();
213+
if (expr.isEmpty()) {
214+
throw new IllegalArgumentException(errorMsg);
215+
}
216+
return expr.trim();
169217
}
170218

171219
private MetricAttribute buildStateMetricAttribute(String key, Map<?, ?> stateMap) {

instrumentation/jmx-metrics/library/src/test/java/io/opentelemetry/instrumentation/jmx/engine/RuleParserTest.java

+33-1
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,6 @@ void testConf8() throws Exception {
379379
@Test
380380
void testStateMetricConf() throws Exception {
381381
JmxConfig config = parseConf(CONF9);
382-
assertThat(config).isNotNull();
383382

384383
List<JmxRule> rules = config.getRules();
385384
assertThat(rules).hasSize(1);
@@ -452,6 +451,39 @@ void testStateMetricConf() throws Exception {
452451
});
453452
}
454453

454+
private static final String CONF10 =
455+
"--- # keep stupid spotlessJava at bay\n"
456+
+ "rules:\n"
457+
+ " - bean: my-test:type=10_Hello\n"
458+
+ " mapping:\n"
459+
+ " jmxAttribute:\n"
460+
+ " type: counter\n"
461+
+ " metric: my_metric\n"
462+
+ " metricAttribute:\n"
463+
+ " to_lower_const: lowercase(const(Hello))\n"
464+
+ " to_lower_attribute: lowercase(beanattr(beanAttribute))\n"
465+
+ " to_lower_param: lowercase(param(type))\n";
466+
467+
@Test
468+
void attributeValueLowercase() {
469+
470+
JmxConfig config = parseConf(CONF10);
471+
472+
List<JmxRule> rules = config.getRules();
473+
assertThat(rules).hasSize(1);
474+
JmxRule jmxRule = rules.get(0);
475+
476+
assertThat(jmxRule.getBean()).isEqualTo("my-test:type=10_Hello");
477+
Metric metric = jmxRule.getMapping().get("jmxAttribute");
478+
assertThat(metric.getMetricType()).isEqualTo(MetricInfo.Type.COUNTER);
479+
assertThat(metric.getMetric()).isEqualTo("my_metric");
480+
assertThat(metric.getMetricAttribute())
481+
.hasSize(3)
482+
.containsEntry("to_lower_const", "lowercase(const(Hello))")
483+
.containsEntry("to_lower_attribute", "lowercase(beanattr(beanAttribute))")
484+
.containsEntry("to_lower_param", "lowercase(param(type))");
485+
}
486+
455487
@Test
456488
void testEmptyConf() {
457489
JmxConfig config = parseConf(EMPTY_CONF);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.jmx.yaml;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
import static org.mockito.Mockito.mock;
11+
import static org.mockito.Mockito.when;
12+
13+
import io.opentelemetry.instrumentation.jmx.engine.MetricAttribute;
14+
import javax.management.MBeanAttributeInfo;
15+
import javax.management.MBeanInfo;
16+
import javax.management.MBeanServerConnection;
17+
import javax.management.ObjectName;
18+
import org.junit.jupiter.params.ParameterizedTest;
19+
import org.junit.jupiter.params.provider.CsvSource;
20+
import org.junit.jupiter.params.provider.ValueSource;
21+
22+
public class MetricStructureTest {
23+
24+
@ParameterizedTest
25+
@CsvSource({"const(Hello),Hello", "lowercase(const(Hello)),hello"})
26+
void metricAttribute_constant(String target, String expectedValue) {
27+
MetricAttribute ma = MetricStructure.buildMetricAttribute("name", target);
28+
assertThat(ma.getAttributeName()).isEqualTo("name");
29+
assertThat(ma.isStateAttribute()).isFalse();
30+
assertThat(ma.acquireAttributeValue(null, null)).isEqualTo(expectedValue);
31+
}
32+
33+
@ParameterizedTest
34+
@CsvSource({
35+
"beanattr(beanAttribute),Hello,Hello",
36+
"lowercase(beanattr(beanAttribute)),Hello,hello",
37+
})
38+
void metricAttribute_beanAttribute(String target, String value, String expectedValue)
39+
throws Exception {
40+
MetricAttribute ma = MetricStructure.buildMetricAttribute("name", target);
41+
assertThat(ma.getAttributeName()).isEqualTo("name");
42+
assertThat(ma.isStateAttribute()).isFalse();
43+
44+
ObjectName objectName = new ObjectName("test:name=_beanAttribute");
45+
MBeanServerConnection mockConnection = mock(MBeanServerConnection.class);
46+
47+
MBeanInfo mockBeanInfo = mock(MBeanInfo.class);
48+
when(mockBeanInfo.getAttributes())
49+
.thenReturn(
50+
new MBeanAttributeInfo[] {
51+
new MBeanAttributeInfo("beanAttribute", "java.lang.String", "", true, false, false)
52+
});
53+
when(mockConnection.getMBeanInfo(objectName)).thenReturn(mockBeanInfo);
54+
when(mockConnection.getAttribute(objectName, "beanAttribute")).thenReturn(value);
55+
56+
assertThat(ma.acquireAttributeValue(mockConnection, objectName)).isEqualTo(expectedValue);
57+
}
58+
59+
@ParameterizedTest
60+
@CsvSource({
61+
"param(name),Hello,Hello",
62+
"lowercase(param(name)),Hello,hello",
63+
})
64+
void metricAttribute_beanParam(String target, String value, String expectedValue)
65+
throws Exception {
66+
MetricAttribute ma = MetricStructure.buildMetricAttribute("name", target);
67+
assertThat(ma.getAttributeName()).isEqualTo("name");
68+
assertThat(ma.isStateAttribute()).isFalse();
69+
70+
ObjectName objectName = new ObjectName("test:name=" + value);
71+
MBeanServerConnection mockConnection = mock(MBeanServerConnection.class);
72+
73+
assertThat(ma.acquireAttributeValue(mockConnection, objectName)).isEqualTo(expectedValue);
74+
}
75+
76+
@ParameterizedTest
77+
@ValueSource(
78+
strings = {
79+
"missing(name)", // non-existing target
80+
"param()", // missing parameter
81+
"param( )", // missing parameter with empty string
82+
"param(name)a", // something after parenthesis
83+
"lowercase()", // misng target in modifier
84+
"lowercase(param(name)", // missing parenthesis for modifier
85+
"lowercase(missing(name))", // non-existing target within modifier
86+
"lowercase(param())", // missing parameter in modifier
87+
"lowercase(param( ))", // missing parameter in modifier with empty string
88+
"lowercase(param))", // missing parenthesis within modifier
89+
})
90+
void invalidTargetSyntax(String target) {
91+
assertThatThrownBy(() -> MetricStructure.buildMetricAttribute("metric_attribute", target))
92+
.isInstanceOf(IllegalArgumentException.class)
93+
.describedAs(
94+
"exception should be thrown with original expression to help end-user understand the syntax error")
95+
.hasMessageContaining(target);
96+
}
97+
}

0 commit comments

Comments
 (0)