Skip to content

Commit 7400025

Browse files
author
Mateusz Rzeszutek
authored
Stable JVM semconv implementation: GC (#9890)
1 parent 4bde25f commit 7400025

File tree

2 files changed

+208
-27
lines changed
  • instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src

2 files changed

+208
-27
lines changed

instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollector.java

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55

66
package io.opentelemetry.instrumentation.runtimemetrics.java8;
77

8+
import static io.opentelemetry.api.common.AttributeKey.stringKey;
9+
import static java.util.Arrays.asList;
810
import static java.util.Collections.emptyList;
11+
import static java.util.Collections.unmodifiableList;
912

1013
import com.sun.management.GarbageCollectionNotificationInfo;
1114
import io.opentelemetry.api.OpenTelemetry;
1215
import io.opentelemetry.api.common.AttributeKey;
1316
import io.opentelemetry.api.common.Attributes;
1417
import io.opentelemetry.api.metrics.DoubleHistogram;
15-
import io.opentelemetry.api.metrics.DoubleHistogramBuilder;
1618
import io.opentelemetry.api.metrics.Meter;
17-
import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder;
19+
import io.opentelemetry.instrumentation.api.internal.SemconvStability;
1820
import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil;
1921
import java.lang.management.GarbageCollectorMXBean;
2022
import java.lang.management.ManagementFactory;
@@ -24,6 +26,7 @@
2426
import java.util.concurrent.TimeUnit;
2527
import java.util.function.Function;
2628
import java.util.logging.Logger;
29+
import javax.annotation.Nullable;
2730
import javax.management.Notification;
2831
import javax.management.NotificationEmitter;
2932
import javax.management.NotificationFilter;
@@ -38,15 +41,37 @@
3841
* <pre>{@code
3942
* GarbageCollector.registerObservers(GlobalOpenTelemetry.get());
4043
* }</pre>
44+
*
45+
* <p>Example metrics being exported:
46+
*
47+
* <pre>
48+
* process.runtime.jvm.gc.duration{gc="G1 Young Generation",action="end of minor GC"} 0.022
49+
* </pre>
50+
*
51+
* <p>In case you enable the preview of stable JVM semantic conventions (e.g. by setting the {@code
52+
* otel.semconv-stability.opt-in} system property to {@code jvm}), the metrics being exported will
53+
* follow <a
54+
* href="https://github.com/open-telemetry/semantic-conventions/blob/main/docs/runtime/jvm-metrics.md">the
55+
* most recent JVM semantic conventions</a>. This is how the example above looks when stable JVM
56+
* semconv is enabled:
57+
*
58+
* <pre>
59+
* jvm.gc.duration{jvm.gc.name="G1 Young Generation",jvm.gc.action="end of minor GC"} 0.022
60+
* </pre>
4161
*/
4262
public final class GarbageCollector {
4363

4464
private static final Logger logger = Logger.getLogger(GarbageCollector.class.getName());
4565

4666
private static final double MILLIS_PER_S = TimeUnit.SECONDS.toMillis(1);
4767

48-
private static final AttributeKey<String> GC_KEY = AttributeKey.stringKey("gc");
49-
private static final AttributeKey<String> ACTION_KEY = AttributeKey.stringKey("action");
68+
private static final AttributeKey<String> GC_KEY = stringKey("gc");
69+
private static final AttributeKey<String> ACTION_KEY = stringKey("action");
70+
71+
// TODO: use the opentelemetry-semconv classes once we have metrics attributes there
72+
private static final AttributeKey<String> JVM_GC_NAME = stringKey("jvm.gc.name");
73+
private static final AttributeKey<String> JVM_GC_ACTION = stringKey("jvm.gc.action");
74+
static final List<Double> GC_DURATION_BUCKETS = unmodifiableList(asList(0.01, 0.1, 1., 10.));
5075

5176
private static final NotificationFilter GC_FILTER =
5277
notification ->
@@ -76,13 +101,27 @@ static List<AutoCloseable> registerObservers(
76101
Function<Notification, GarbageCollectionNotificationInfo> notificationInfoExtractor) {
77102
Meter meter = JmxRuntimeMetricsUtil.getMeter(openTelemetry);
78103

79-
DoubleHistogramBuilder gcDurationBuilder =
80-
meter
81-
.histogramBuilder("process.runtime.jvm.gc.duration")
82-
.setDescription("Duration of JVM garbage collection actions")
83-
.setUnit("s");
84-
setGcDurationBuckets(gcDurationBuilder);
85-
DoubleHistogram gcDuration = gcDurationBuilder.build();
104+
DoubleHistogram oldGcDuration = null;
105+
DoubleHistogram stableGcDuration = null;
106+
107+
if (SemconvStability.emitOldJvmSemconv()) {
108+
oldGcDuration =
109+
meter
110+
.histogramBuilder("process.runtime.jvm.gc.duration")
111+
.setDescription("Duration of JVM garbage collection actions")
112+
.setUnit("s")
113+
.setExplicitBucketBoundariesAdvice(emptyList())
114+
.build();
115+
}
116+
if (SemconvStability.emitStableJvmSemconv()) {
117+
stableGcDuration =
118+
meter
119+
.histogramBuilder("jvm.gc.duration")
120+
.setDescription("Duration of JVM garbage collection actions.")
121+
.setUnit("s")
122+
.setExplicitBucketBoundariesAdvice(GC_DURATION_BUCKETS)
123+
.build();
124+
}
86125

87126
List<AutoCloseable> result = new ArrayList<>();
88127
for (GarbageCollectorMXBean gcBean : gcBeans) {
@@ -91,42 +130,45 @@ static List<AutoCloseable> registerObservers(
91130
}
92131
NotificationEmitter notificationEmitter = (NotificationEmitter) gcBean;
93132
GcNotificationListener listener =
94-
new GcNotificationListener(gcDuration, notificationInfoExtractor);
133+
new GcNotificationListener(oldGcDuration, stableGcDuration, notificationInfoExtractor);
95134
notificationEmitter.addNotificationListener(listener, GC_FILTER, null);
96135
result.add(() -> notificationEmitter.removeNotificationListener(listener));
97136
}
98137
return result;
99138
}
100139

101-
private static void setGcDurationBuckets(DoubleHistogramBuilder builder) {
102-
if (!(builder instanceof ExtendedDoubleHistogramBuilder)) {
103-
// that shouldn't really happen
104-
return;
105-
}
106-
((ExtendedDoubleHistogramBuilder) builder).setExplicitBucketBoundariesAdvice(emptyList());
107-
}
108-
109140
private static final class GcNotificationListener implements NotificationListener {
110141

111-
private final DoubleHistogram gcDuration;
142+
@Nullable private final DoubleHistogram oldGcDuration;
143+
@Nullable private final DoubleHistogram stableGcDuration;
112144
private final Function<Notification, GarbageCollectionNotificationInfo>
113145
notificationInfoExtractor;
114146

115147
private GcNotificationListener(
116-
DoubleHistogram gcDuration,
148+
@Nullable DoubleHistogram oldGcDuration,
149+
@Nullable DoubleHistogram stableGcDuration,
117150
Function<Notification, GarbageCollectionNotificationInfo> notificationInfoExtractor) {
118-
this.gcDuration = gcDuration;
151+
this.oldGcDuration = oldGcDuration;
152+
this.stableGcDuration = stableGcDuration;
119153
this.notificationInfoExtractor = notificationInfoExtractor;
120154
}
121155

122156
@Override
123157
public void handleNotification(Notification notification, Object unused) {
124158
GarbageCollectionNotificationInfo notificationInfo =
125159
notificationInfoExtractor.apply(notification);
126-
gcDuration.record(
127-
notificationInfo.getGcInfo().getDuration() / MILLIS_PER_S,
128-
Attributes.of(
129-
GC_KEY, notificationInfo.getGcName(), ACTION_KEY, notificationInfo.getGcAction()));
160+
161+
String gcName = notificationInfo.getGcName();
162+
String gcAction = notificationInfo.getGcAction();
163+
double duration = notificationInfo.getGcInfo().getDuration() / MILLIS_PER_S;
164+
165+
if (oldGcDuration != null) {
166+
oldGcDuration.record(duration, Attributes.of(GC_KEY, gcName, ACTION_KEY, gcAction));
167+
}
168+
if (stableGcDuration != null) {
169+
stableGcDuration.record(
170+
duration, Attributes.of(JVM_GC_NAME, gcName, JVM_GC_ACTION, gcAction));
171+
}
130172
}
131173
}
132174

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.runtimemetrics.java8;
7+
8+
import static io.opentelemetry.instrumentation.runtimemetrics.java8.ScopeUtil.EXPECTED_SCOPE;
9+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
10+
import static java.util.Collections.singletonList;
11+
import static org.mockito.ArgumentMatchers.any;
12+
import static org.mockito.Mockito.mock;
13+
import static org.mockito.Mockito.verify;
14+
import static org.mockito.Mockito.when;
15+
16+
import com.sun.management.GarbageCollectionNotificationInfo;
17+
import com.sun.management.GcInfo;
18+
import io.opentelemetry.api.common.Attributes;
19+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
20+
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
21+
import java.lang.management.GarbageCollectorMXBean;
22+
import java.util.concurrent.atomic.AtomicLong;
23+
import javax.management.Notification;
24+
import javax.management.NotificationEmitter;
25+
import javax.management.NotificationListener;
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.api.extension.ExtendWith;
28+
import org.junit.jupiter.api.extension.RegisterExtension;
29+
import org.mockito.ArgumentCaptor;
30+
import org.mockito.Captor;
31+
import org.mockito.Mock;
32+
import org.mockito.junit.jupiter.MockitoExtension;
33+
import org.mockito.junit.jupiter.MockitoSettings;
34+
import org.mockito.quality.Strictness;
35+
36+
@ExtendWith(MockitoExtension.class)
37+
@MockitoSettings(strictness = Strictness.LENIENT)
38+
class GarbageCollectorTest {
39+
40+
static final double[] GC_DURATION_BUCKETS =
41+
GarbageCollector.GC_DURATION_BUCKETS.stream().mapToDouble(d -> d).toArray();
42+
43+
@RegisterExtension
44+
static final InstrumentationExtension testing = LibraryInstrumentationExtension.create();
45+
46+
@Mock(extraInterfaces = NotificationEmitter.class)
47+
private GarbageCollectorMXBean gcBean;
48+
49+
@Captor private ArgumentCaptor<NotificationListener> listenerCaptor;
50+
51+
@Test
52+
void registerObservers() {
53+
GarbageCollector.registerObservers(
54+
testing.getOpenTelemetry(),
55+
singletonList(gcBean),
56+
GarbageCollectorTest::getGcNotificationInfo);
57+
58+
NotificationEmitter notificationEmitter = (NotificationEmitter) gcBean;
59+
verify(notificationEmitter).addNotificationListener(listenerCaptor.capture(), any(), any());
60+
NotificationListener listener = listenerCaptor.getValue();
61+
62+
listener.handleNotification(
63+
createTestNotification("G1 Young Generation", "end of minor GC", 10), null);
64+
listener.handleNotification(
65+
createTestNotification("G1 Young Generation", "end of minor GC", 12), null);
66+
listener.handleNotification(
67+
createTestNotification("G1 Old Generation", "end of major GC", 11), null);
68+
69+
testing.waitAndAssertMetrics(
70+
"io.opentelemetry.runtime-telemetry-java8",
71+
"jvm.gc.duration",
72+
metrics ->
73+
metrics.anySatisfy(
74+
metricData ->
75+
assertThat(metricData)
76+
.hasInstrumentationScope(EXPECTED_SCOPE)
77+
.hasDescription("Duration of JVM garbage collection actions.")
78+
.hasUnit("s")
79+
.hasHistogramSatisfying(
80+
histogram ->
81+
histogram.hasPointsSatisfying(
82+
point ->
83+
point
84+
.hasCount(2)
85+
.hasSum(0.022)
86+
.hasAttributes(
87+
Attributes.builder()
88+
.put("jvm.gc.name", "G1 Young Generation")
89+
.put("jvm.gc.action", "end of minor GC")
90+
.build())
91+
.hasBucketBoundaries(GC_DURATION_BUCKETS),
92+
point ->
93+
point
94+
.hasCount(1)
95+
.hasSum(0.011)
96+
.hasAttributes(
97+
Attributes.builder()
98+
.put("jvm.gc.name", "G1 Old Generation")
99+
.put("jvm.gc.action", "end of major GC")
100+
.build())
101+
.hasBucketBoundaries(GC_DURATION_BUCKETS)))));
102+
}
103+
104+
private static Notification createTestNotification(
105+
String gcName, String gcAction, long duration) {
106+
GarbageCollectionNotificationInfo gcNotificationInfo =
107+
mock(GarbageCollectionNotificationInfo.class);
108+
when(gcNotificationInfo.getGcName()).thenReturn(gcName);
109+
when(gcNotificationInfo.getGcAction()).thenReturn(gcAction);
110+
GcInfo gcInfo = mock(GcInfo.class);
111+
when(gcInfo.getDuration()).thenReturn(duration);
112+
when(gcNotificationInfo.getGcInfo()).thenReturn(gcInfo);
113+
return new TestNotification(gcNotificationInfo);
114+
}
115+
116+
private static GarbageCollectionNotificationInfo getGcNotificationInfo(
117+
Notification notification) {
118+
return ((TestNotification) notification).gcNotificationInfo;
119+
}
120+
121+
/**
122+
* A {@link Notification} when is initialized with a mock {@link
123+
* GarbageCollectionNotificationInfo}.
124+
*/
125+
private static class TestNotification extends Notification {
126+
127+
private static final AtomicLong sequence = new AtomicLong(0);
128+
129+
private final GarbageCollectionNotificationInfo gcNotificationInfo;
130+
131+
private TestNotification(GarbageCollectionNotificationInfo gcNotificationInfo) {
132+
super(
133+
GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION,
134+
"test",
135+
sequence.incrementAndGet());
136+
this.gcNotificationInfo = gcNotificationInfo;
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)