Skip to content

Commit 298319f

Browse files
SylvainJugelauritotelbot[bot]
authored
document & test jmx metric implicit aggregation (#14111)
Co-authored-by: Lauri Tulmin <[email protected]> Co-authored-by: otelbot <[email protected]>
1 parent 5b67c8b commit 298319f

File tree

2 files changed

+280
-0
lines changed

2 files changed

+280
-0
lines changed

instrumentation/jmx-metrics/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,40 @@ rules:
371371
desc: Recent CPU utilization for the process as reported by the JVM.
372372
```
373373

374+
### Aggregation over multiple MBean instances
375+
376+
Sometimes, multiple MBean instances are registered with distinct names and we need to capture the aggregate value over all the instances.
377+
378+
For example, the JVM exposes the number of GC executions in the `CollectionCount` attribute of the MBean instances returned by `java.lang:name=*,type=GarbageCollector` query,
379+
there are multiple instances each with a distinct value for the `name` parameter.
380+
381+
To capture the total number of GC executions across all those instances in a single metric, we can use the following configuration
382+
where the `name` parameter in the MBean name is NOT mapped to a metric attribute.
383+
384+
```yaml
385+
- bean: java.lang:name=*,type=GarbageCollector
386+
mapping:
387+
CollectionCount:
388+
metric: custom.jvm.gc.count
389+
unit: '{collection}'
390+
type: counter
391+
desc: JVM GC execution count
392+
```
393+
394+
When two or more MBean parameters are used, it is also possible to perform a partial aggregation:
395+
- parameters not mapped as metric attributes are discarded
396+
- parameters mapped as metric attributes with `param(<mbeanParam>)` are preserved
397+
- values are aggregated with mapped metric attributes
398+
399+
The applied aggregation depends on the metric type:
400+
- `counter` or `updowncounter`: sum aggregation
401+
- `gauge`: last-value aggregation
402+
403+
As a consequence, it is not recommended to use it for `gauge` metrics when querying more than one MBean instance as it would produce unpredictable results.
404+
405+
When there is only a single MBean instance, using a `gauge` metric produces the expected value, hence allowing to avoid mapping all the MBean parameters
406+
to metric attributes.
407+
374408
### General Syntax
375409

376410
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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.jmx.engine;
7+
8+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
9+
import static org.junit.jupiter.params.ParameterizedInvocationConstants.ARGUMENTS_PLACEHOLDER;
10+
11+
import io.opentelemetry.api.common.AttributeKey;
12+
import io.opentelemetry.api.common.Attributes;
13+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
14+
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
15+
import io.opentelemetry.sdk.testing.assertj.LongPointAssert;
16+
import io.opentelemetry.sdk.testing.assertj.MetricAssert;
17+
import java.util.ArrayList;
18+
import java.util.Arrays;
19+
import java.util.Collections;
20+
import java.util.List;
21+
import java.util.Locale;
22+
import java.util.concurrent.atomic.AtomicInteger;
23+
import java.util.function.Consumer;
24+
import javax.annotation.Nullable;
25+
import javax.management.InstanceNotFoundException;
26+
import javax.management.MBeanRegistrationException;
27+
import javax.management.MBeanServer;
28+
import javax.management.MBeanServerFactory;
29+
import javax.management.MalformedObjectNameException;
30+
import javax.management.ObjectName;
31+
import org.junit.jupiter.api.AfterAll;
32+
import org.junit.jupiter.api.AfterEach;
33+
import org.junit.jupiter.api.BeforeAll;
34+
import org.junit.jupiter.api.extension.RegisterExtension;
35+
import org.junit.jupiter.params.ParameterizedTest;
36+
import org.junit.jupiter.params.provider.MethodSource;
37+
38+
public class MetricAggregationTest {
39+
40+
@SuppressWarnings({"unused", "checkstyle:AbbreviationAsWordInName"})
41+
public interface HelloMBean {
42+
43+
int getValue();
44+
}
45+
46+
public static class Hello implements HelloMBean {
47+
48+
private final int value;
49+
50+
public Hello(int value) {
51+
this.value = value;
52+
}
53+
54+
@Override
55+
public int getValue() {
56+
return value;
57+
}
58+
}
59+
60+
@RegisterExtension
61+
static final InstrumentationExtension testing = LibraryInstrumentationExtension.create();
62+
63+
// used to generate non-conflicting metric names at runtime
64+
private static final AtomicInteger metricCounter = new AtomicInteger(0);
65+
66+
private static final String DOMAIN = "otel.jmx.test";
67+
private static MBeanServer theServer;
68+
69+
@BeforeAll
70+
static void setUp() {
71+
theServer = MBeanServerFactory.createMBeanServer(DOMAIN);
72+
}
73+
74+
@AfterAll
75+
static void tearDown() {
76+
MBeanServerFactory.releaseMBeanServer(theServer);
77+
}
78+
79+
@AfterEach
80+
void after() throws Exception {
81+
ObjectName objectName = new ObjectName(DOMAIN + ":type=" + Hello.class.getSimpleName() + ",*");
82+
theServer
83+
.queryMBeans(objectName, null)
84+
.forEach(
85+
instance -> {
86+
try {
87+
theServer.unregisterMBean(instance.getObjectName());
88+
} catch (InstanceNotFoundException | MBeanRegistrationException e) {
89+
throw new RuntimeException(e);
90+
}
91+
});
92+
}
93+
94+
private static ObjectName getObjectName(@Nullable String a, @Nullable String b)
95+
throws MalformedObjectNameException {
96+
StringBuilder parts = new StringBuilder();
97+
parts.append("otel.jmx.test:type=").append(Hello.class.getSimpleName());
98+
if (a != null) {
99+
parts.append(",a=").append(a);
100+
}
101+
if (b != null) {
102+
parts.append(",b=").append(b);
103+
}
104+
return new ObjectName(parts.toString());
105+
}
106+
107+
static List<MetricInfo.Type> metricTypes() {
108+
return Arrays.asList(
109+
MetricInfo.Type.COUNTER, MetricInfo.Type.UPDOWNCOUNTER, MetricInfo.Type.GAUGE);
110+
}
111+
112+
@ParameterizedTest(name = ARGUMENTS_PLACEHOLDER)
113+
@MethodSource("metricTypes")
114+
void singleInstance(MetricInfo.Type metricType) throws Exception {
115+
ObjectName bean = getObjectName(null, null);
116+
theServer.registerMBean(new Hello(42), bean);
117+
118+
String metricName = generateMetricName(metricType);
119+
startTestMetric(metricName, bean.toString(), Collections.emptyList(), metricType);
120+
waitAndAssertMetric(
121+
metricName, metricType, point -> point.hasValue(42).hasAttributes(Attributes.empty()));
122+
}
123+
124+
@ParameterizedTest(name = ARGUMENTS_PLACEHOLDER)
125+
@MethodSource("metricTypes")
126+
void aggregateOneParam(MetricInfo.Type metricType) throws Exception {
127+
theServer.registerMBean(new Hello(42), getObjectName("value1", null));
128+
theServer.registerMBean(new Hello(37), getObjectName("value2", null));
129+
130+
String bean = getObjectName("*", null).toString();
131+
String metricName = generateMetricName(metricType);
132+
startTestMetric(metricName, bean, Collections.emptyList(), metricType);
133+
134+
// last-value aggregation produces an unpredictable result unless a single mbean instance is
135+
// used
136+
// test here is only used as a way to document behavior and should not be considered a feature
137+
long expected = metricType == MetricInfo.Type.GAUGE ? 37 : 79;
138+
waitAndAssertMetric(
139+
metricName,
140+
metricType,
141+
point -> point.hasValue(expected).hasAttributes(Attributes.empty()));
142+
}
143+
144+
@ParameterizedTest(name = ARGUMENTS_PLACEHOLDER)
145+
@MethodSource("metricTypes")
146+
void aggregateMultipleParams(MetricInfo.Type metricType) throws Exception {
147+
theServer.registerMBean(new Hello(1), getObjectName("1", "x"));
148+
theServer.registerMBean(new Hello(2), getObjectName("2", "y"));
149+
theServer.registerMBean(new Hello(3), getObjectName("3", "x"));
150+
theServer.registerMBean(new Hello(4), getObjectName("4", "y"));
151+
152+
String bean = getObjectName("*", "*").toString();
153+
String metricName = generateMetricName(metricType);
154+
startTestMetric(metricName, bean, Collections.emptyList(), metricType);
155+
156+
// last-value aggregation produces an unpredictable result unless a single mbean instance is
157+
// used
158+
// test here is only used as a way to document behavior and should not be considered a feature
159+
long expected = metricType == MetricInfo.Type.GAUGE ? 1 : 10;
160+
waitAndAssertMetric(
161+
metricName,
162+
metricType,
163+
point -> point.hasValue(expected).hasAttributes(Attributes.empty()));
164+
}
165+
166+
@ParameterizedTest(name = ARGUMENTS_PLACEHOLDER)
167+
@MethodSource("metricTypes")
168+
void partialAggregateMultipleParams(MetricInfo.Type metricType) throws Exception {
169+
theServer.registerMBean(new Hello(1), getObjectName("1", "x"));
170+
theServer.registerMBean(new Hello(2), getObjectName("2", "y"));
171+
theServer.registerMBean(new Hello(3), getObjectName("3", "x"));
172+
theServer.registerMBean(new Hello(4), getObjectName("4", "y"));
173+
174+
String bean = getObjectName("*", "*").toString();
175+
176+
List<MetricAttribute> attributes =
177+
Collections.singletonList(
178+
new MetricAttribute(
179+
"test.metric.param", MetricAttributeExtractor.fromObjectNameParameter("b")));
180+
String metricName = generateMetricName(metricType);
181+
startTestMetric(metricName, bean, attributes, metricType);
182+
183+
AttributeKey<String> metricAttribute = AttributeKey.stringKey("test.metric.param");
184+
if (metricType == MetricInfo.Type.GAUGE) {
185+
waitAndAssertMetric(
186+
metricName,
187+
metricType,
188+
point -> point.hasValue(1).hasAttribute(metricAttribute, "x"),
189+
point -> point.hasValue(4).hasAttribute(metricAttribute, "y"));
190+
} else {
191+
waitAndAssertMetric(
192+
metricName,
193+
metricType,
194+
point -> point.hasValue(4).hasAttribute(metricAttribute, "x"),
195+
point -> point.hasValue(6).hasAttribute(metricAttribute, "y"));
196+
}
197+
}
198+
199+
private static String generateMetricName(MetricInfo.Type metricType) {
200+
// generate a sequential metric name that prevents naming conflicts and unexpected behaviors
201+
return "test.metric"
202+
+ metricCounter.incrementAndGet()
203+
+ "."
204+
+ metricType.name().toLowerCase(Locale.ROOT);
205+
}
206+
207+
@SafeVarargs
208+
@SuppressWarnings("varargs")
209+
private static void waitAndAssertMetric(
210+
String metricName, MetricInfo.Type metricType, Consumer<LongPointAssert>... pointAsserts) {
211+
212+
testing.waitAndAssertMetrics(
213+
"io.opentelemetry.jmx",
214+
metricName,
215+
metrics ->
216+
metrics.anySatisfy(
217+
metricData -> {
218+
MetricAssert metricAssert =
219+
assertThat(metricData).hasDescription("description").hasUnit("1");
220+
if (metricType == MetricInfo.Type.GAUGE) {
221+
metricAssert.hasLongGaugeSatisfying(
222+
gauge -> gauge.hasPointsSatisfying(pointAsserts));
223+
} else {
224+
metricAssert.hasLongSumSatisfying(sum -> sum.hasPointsSatisfying(pointAsserts));
225+
}
226+
}));
227+
}
228+
229+
private static void startTestMetric(
230+
String metricName, String mbean, List<MetricAttribute> attributes, MetricInfo.Type metricType)
231+
throws MalformedObjectNameException {
232+
JmxMetricInsight metricInsight = JmxMetricInsight.createService(testing.getOpenTelemetry(), 0);
233+
MetricConfiguration metricConfiguration = new MetricConfiguration();
234+
List<MetricExtractor> extractors = new ArrayList<>();
235+
236+
MetricInfo metricInfo = new MetricInfo(metricName, "description", null, "1", metricType);
237+
BeanAttributeExtractor beanExtractor = BeanAttributeExtractor.fromName("Value");
238+
MetricExtractor extractor = new MetricExtractor(beanExtractor, metricInfo, attributes);
239+
extractors.add(extractor);
240+
MetricDef metricDef =
241+
new MetricDef(BeanGroup.forBeans(Collections.singletonList(mbean)), extractors);
242+
243+
metricConfiguration.addMetricDef(metricDef);
244+
metricInsight.startLocal(metricConfiguration);
245+
}
246+
}

0 commit comments

Comments
 (0)