Skip to content

Commit 37cf457

Browse files
committed
update documentation
1 parent a939b59 commit 37cf457

File tree

2 files changed

+196
-0
lines changed

2 files changed

+196
-0
lines changed

instrumentation/jmx-metrics/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,26 @@ 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 of 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` key.
380+
381+
In order 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` key 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+
374394
### General Syntax
375395

376396
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.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
10+
import io.opentelemetry.sdk.OpenTelemetrySdk;
11+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
12+
import io.opentelemetry.sdk.metrics.data.LongPointData;
13+
import io.opentelemetry.sdk.metrics.data.MetricData;
14+
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
15+
import java.util.ArrayList;
16+
import java.util.Collection;
17+
import java.util.Collections;
18+
import java.util.List;
19+
import javax.annotation.Nullable;
20+
import javax.management.MBeanServer;
21+
import javax.management.MBeanServerFactory;
22+
import javax.management.MalformedObjectNameException;
23+
import javax.management.ObjectName;
24+
import org.junit.jupiter.api.AfterAll;
25+
import org.junit.jupiter.api.AfterEach;
26+
import org.junit.jupiter.api.BeforeAll;
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Test;
29+
30+
public class MetricAggregationTest {
31+
32+
@SuppressWarnings("unused")
33+
public interface HelloMBean {
34+
35+
int getValue();
36+
}
37+
38+
public static class Hello implements HelloMBean {
39+
40+
private final int value;
41+
42+
public Hello(int value) {
43+
this.value = value;
44+
}
45+
46+
@Override
47+
public int getValue() {
48+
return value;
49+
}
50+
}
51+
52+
private static final String DOMAIN = "otel.jmx.test";
53+
private static MBeanServer theServer;
54+
55+
@BeforeAll
56+
static void setUp() {
57+
theServer = MBeanServerFactory.createMBeanServer(DOMAIN);
58+
}
59+
60+
@AfterAll
61+
static void tearDown() {
62+
MBeanServerFactory.releaseMBeanServer(theServer);
63+
}
64+
65+
@BeforeEach
66+
void before() {
67+
reader = InMemoryMetricReader.createDelta();
68+
sdk =
69+
OpenTelemetrySdk.builder()
70+
.setMeterProvider(SdkMeterProvider.builder().registerMetricReader(reader).build())
71+
.build();
72+
}
73+
74+
@AfterEach
75+
void after() {
76+
if (sdk != null) {
77+
sdk.getSdkMeterProvider().close();
78+
}
79+
}
80+
81+
private InMemoryMetricReader reader;
82+
private OpenTelemetrySdk sdk;
83+
84+
private static ObjectName getObjectName(@Nullable String a, @Nullable String b) {
85+
StringBuilder parts = new StringBuilder();
86+
parts.append("otel.jmx.test:type=").append(Hello.class.getSimpleName());
87+
if (a != null) {
88+
parts.append(",a=").append(a);
89+
}
90+
if (b != null) {
91+
parts.append(",b=").append(b);
92+
}
93+
try {
94+
return new ObjectName(parts.toString());
95+
} catch (MalformedObjectNameException e) {
96+
throw new RuntimeException(e);
97+
}
98+
}
99+
100+
@Test
101+
void singleInstance() throws Exception {
102+
ObjectName bean = getObjectName(null, null);
103+
theServer.registerMBean(new Hello(42), bean);
104+
105+
testMetric(bean.toString(), 42);
106+
}
107+
108+
@Test
109+
void multipleInstancesAggregated_twoInstances() throws Exception {
110+
theServer.registerMBean(new Hello(42), getObjectName("value1", null));
111+
theServer.registerMBean(new Hello(37), getObjectName("value2", null));
112+
113+
String bean = getObjectName("*", null).toString();
114+
testMetric(bean, 79);
115+
}
116+
117+
@Test
118+
void multipleInstancesAggregated_fourInstances() throws Exception {
119+
theServer.registerMBean(new Hello(1), getObjectName("1", "a"));
120+
theServer.registerMBean(new Hello(2), getObjectName("2", "b"));
121+
theServer.registerMBean(new Hello(3), getObjectName("3", "a"));
122+
theServer.registerMBean(new Hello(5), getObjectName("4", "b"));
123+
124+
String bean = getObjectName("*", "*").toString();
125+
testMetric(bean, 11);
126+
}
127+
128+
void testMetric(String mbean, int expectedValue) throws MalformedObjectNameException {
129+
JmxMetricInsight metricInsight = JmxMetricInsight.createService(sdk, 0);
130+
MetricConfiguration metricConfiguration = new MetricConfiguration();
131+
List<MetricExtractor> extractors = new ArrayList<>();
132+
133+
MetricInfo metricInfo =
134+
new MetricInfo("test.metric", "description", null, "1", MetricInfo.Type.COUNTER);
135+
BeanAttributeExtractor beanExtractor = BeanAttributeExtractor.fromName("Value");
136+
MetricExtractor extractor =
137+
new MetricExtractor(beanExtractor, metricInfo, Collections.emptyList());
138+
extractors.add(extractor);
139+
MetricDef metricDef =
140+
new MetricDef(BeanGroup.forBeans(Collections.singletonList(mbean)), extractors);
141+
142+
metricConfiguration.addMetricDef(metricDef);
143+
metricInsight.startLocal(metricConfiguration);
144+
145+
Collection<MetricData> data = waitMetricsReceived();
146+
147+
assertThat(data)
148+
.isNotEmpty()
149+
.satisfiesExactlyInAnyOrder(
150+
metric -> {
151+
assertThat(metric.getName()).isEqualTo("test.metric");
152+
Collection<LongPointData> points = metric.getLongSumData().getPoints();
153+
assertThat(points).hasSize(1);
154+
LongPointData pointData = points.stream().findFirst().get();
155+
assertThat(pointData.getValue()).isEqualTo(expectedValue);
156+
assertThat(pointData.getAttributes()).isEmpty();
157+
});
158+
}
159+
160+
private Collection<MetricData> waitMetricsReceived() {
161+
int retries = 100;
162+
Collection<MetricData> data;
163+
do {
164+
try {
165+
Thread.sleep(100);
166+
} catch (InterruptedException e) {
167+
throw new RuntimeException(e);
168+
}
169+
data = reader.collectAllMetrics();
170+
} while (data.isEmpty() && retries-- > 0);
171+
if (data.isEmpty()) {
172+
throw new RuntimeException("timed out waiting for metrics received");
173+
}
174+
return data;
175+
}
176+
}

0 commit comments

Comments
 (0)