diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollector.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollector.java index b167dc770bd5..12264d51df8c 100644 --- a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollector.java +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/main/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollector.java @@ -5,16 +5,18 @@ package io.opentelemetry.instrumentation.runtimemetrics.java8; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; import com.sun.management.GarbageCollectionNotificationInfo; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleHistogram; -import io.opentelemetry.api.metrics.DoubleHistogramBuilder; import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.extension.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.instrumentation.api.internal.SemconvStability; import io.opentelemetry.instrumentation.runtimemetrics.java8.internal.JmxRuntimeMetricsUtil; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; @@ -24,6 +26,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.logging.Logger; +import javax.annotation.Nullable; import javax.management.Notification; import javax.management.NotificationEmitter; import javax.management.NotificationFilter; @@ -38,6 +41,23 @@ *
{@code
  * GarbageCollector.registerObservers(GlobalOpenTelemetry.get());
  * }
+ * + *

Example metrics being exported: + * + *

+ *   process.runtime.jvm.gc.duration{gc="G1 Young Generation",action="end of minor GC"} 0.022
+ * 
+ * + *

In case you enable the preview of stable JVM semantic conventions (e.g. by setting the {@code + * otel.semconv-stability.opt-in} system property to {@code jvm}), the metrics being exported will + * follow the + * most recent JVM semantic conventions. This is how the example above looks when stable JVM + * semconv is enabled: + * + *

+ *   jvm.gc.duration{jvm.gc.name="G1 Young Generation",jvm.gc.action="end of minor GC"} 0.022
+ * 
*/ public final class GarbageCollector { @@ -45,8 +65,12 @@ public final class GarbageCollector { private static final double MILLIS_PER_S = TimeUnit.SECONDS.toMillis(1); - private static final AttributeKey GC_KEY = AttributeKey.stringKey("gc"); - private static final AttributeKey ACTION_KEY = AttributeKey.stringKey("action"); + private static final AttributeKey GC_KEY = stringKey("gc"); + private static final AttributeKey ACTION_KEY = stringKey("action"); + + private static final AttributeKey JVM_GC_NAME = stringKey("jvm.gc.name"); + private static final AttributeKey JVM_GC_ACTION = stringKey("jvm.gc.action"); + static final List GC_DURATION_BUCKETS = unmodifiableList(asList(0.01, 0.1, 1., 10.)); private static final NotificationFilter GC_FILTER = notification -> @@ -76,13 +100,27 @@ static List registerObservers( Function notificationInfoExtractor) { Meter meter = JmxRuntimeMetricsUtil.getMeter(openTelemetry); - DoubleHistogramBuilder gcDurationBuilder = - meter - .histogramBuilder("process.runtime.jvm.gc.duration") - .setDescription("Duration of JVM garbage collection actions") - .setUnit("s"); - setGcDurationBuckets(gcDurationBuilder); - DoubleHistogram gcDuration = gcDurationBuilder.build(); + DoubleHistogram oldGcDuration = null; + DoubleHistogram stableGcDuration = null; + + if (SemconvStability.emitOldJvmSemconv()) { + oldGcDuration = + meter + .histogramBuilder("process.runtime.jvm.gc.duration") + .setDescription("Duration of JVM garbage collection actions") + .setUnit("s") + .setExplicitBucketBoundariesAdvice(emptyList()) + .build(); + } + if (SemconvStability.emitStableJvmSemconv()) { + stableGcDuration = + meter + .histogramBuilder("jvm.gc.duration") + .setDescription("Duration of JVM garbage collection actions.") + .setUnit("s") + .setExplicitBucketBoundariesAdvice(GC_DURATION_BUCKETS) + .build(); + } List result = new ArrayList<>(); for (GarbageCollectorMXBean gcBean : gcBeans) { @@ -91,31 +129,26 @@ static List registerObservers( } NotificationEmitter notificationEmitter = (NotificationEmitter) gcBean; GcNotificationListener listener = - new GcNotificationListener(gcDuration, notificationInfoExtractor); + new GcNotificationListener(oldGcDuration, stableGcDuration, notificationInfoExtractor); notificationEmitter.addNotificationListener(listener, GC_FILTER, null); result.add(() -> notificationEmitter.removeNotificationListener(listener)); } return result; } - private static void setGcDurationBuckets(DoubleHistogramBuilder builder) { - if (!(builder instanceof ExtendedDoubleHistogramBuilder)) { - // that shouldn't really happen - return; - } - ((ExtendedDoubleHistogramBuilder) builder).setExplicitBucketBoundariesAdvice(emptyList()); - } - private static final class GcNotificationListener implements NotificationListener { - private final DoubleHistogram gcDuration; + @Nullable private final DoubleHistogram oldGcDuration; + @Nullable private final DoubleHistogram stableGcDuration; private final Function notificationInfoExtractor; private GcNotificationListener( - DoubleHistogram gcDuration, + @Nullable DoubleHistogram oldGcDuration, + @Nullable DoubleHistogram stableGcDuration, Function notificationInfoExtractor) { - this.gcDuration = gcDuration; + this.oldGcDuration = oldGcDuration; + this.stableGcDuration = stableGcDuration; this.notificationInfoExtractor = notificationInfoExtractor; } @@ -123,10 +156,18 @@ private GcNotificationListener( public void handleNotification(Notification notification, Object unused) { GarbageCollectionNotificationInfo notificationInfo = notificationInfoExtractor.apply(notification); - gcDuration.record( - notificationInfo.getGcInfo().getDuration() / MILLIS_PER_S, - Attributes.of( - GC_KEY, notificationInfo.getGcName(), ACTION_KEY, notificationInfo.getGcAction())); + + String gcName = notificationInfo.getGcName(); + String gcAction = notificationInfo.getGcAction(); + double duration = notificationInfo.getGcInfo().getDuration() / MILLIS_PER_S; + + if (oldGcDuration != null) { + oldGcDuration.record(duration, Attributes.of(GC_KEY, gcName, ACTION_KEY, gcAction)); + } + if (stableGcDuration != null) { + stableGcDuration.record( + duration, Attributes.of(JVM_GC_NAME, gcName, JVM_GC_ACTION, gcAction)); + } } } diff --git a/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/testStableSemconv/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollectorTest.java b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/testStableSemconv/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollectorTest.java new file mode 100644 index 000000000000..92bf23d10748 --- /dev/null +++ b/instrumentation/runtime-telemetry/runtime-telemetry-java8/library/src/testStableSemconv/java/io/opentelemetry/instrumentation/runtimemetrics/java8/GarbageCollectorTest.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.runtimemetrics.java8; + +import static io.opentelemetry.instrumentation.runtimemetrics.java8.ScopeUtil.EXPECTED_SCOPE; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.singletonList; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.sun.management.GarbageCollectionNotificationInfo; +import com.sun.management.GcInfo; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import java.lang.management.GarbageCollectorMXBean; +import java.util.concurrent.atomic.AtomicLong; +import javax.management.Notification; +import javax.management.NotificationEmitter; +import javax.management.NotificationListener; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class GarbageCollectorTest { + + static final double[] GC_DURATION_BUCKETS = + GarbageCollector.GC_DURATION_BUCKETS.stream().mapToDouble(d -> d).toArray(); + + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Mock(extraInterfaces = NotificationEmitter.class) + private GarbageCollectorMXBean gcBean; + + @Captor private ArgumentCaptor listenerCaptor; + + @Test + void registerObservers() { + GarbageCollector.registerObservers( + testing.getOpenTelemetry(), + singletonList(gcBean), + GarbageCollectorTest::getGcNotificationInfo); + + NotificationEmitter notificationEmitter = (NotificationEmitter) gcBean; + verify(notificationEmitter).addNotificationListener(listenerCaptor.capture(), any(), any()); + NotificationListener listener = listenerCaptor.getValue(); + + listener.handleNotification( + createTestNotification("G1 Young Generation", "end of minor GC", 10), null); + listener.handleNotification( + createTestNotification("G1 Young Generation", "end of minor GC", 12), null); + listener.handleNotification( + createTestNotification("G1 Old Generation", "end of major GC", 11), null); + + testing.waitAndAssertMetrics( + "io.opentelemetry.runtime-telemetry-java8", + "jvm.gc.duration", + metrics -> + metrics.anySatisfy( + metricData -> + assertThat(metricData) + .hasInstrumentationScope(EXPECTED_SCOPE) + .hasDescription("Duration of JVM garbage collection actions.") + .hasUnit("s") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasCount(2) + .hasSum(0.022) + .hasAttributes( + Attributes.builder() + .put("jvm.gc.name", "G1 Young Generation") + .put("jvm.gc.action", "end of minor GC") + .build()) + .hasBucketBoundaries(GC_DURATION_BUCKETS), + point -> + point + .hasCount(1) + .hasSum(0.011) + .hasAttributes( + Attributes.builder() + .put("jvm.gc.name", "G1 Old Generation") + .put("jvm.gc.action", "end of major GC") + .build()) + .hasBucketBoundaries(GC_DURATION_BUCKETS))))); + } + + private static Notification createTestNotification( + String gcName, String gcAction, long duration) { + GarbageCollectionNotificationInfo gcNotificationInfo = + mock(GarbageCollectionNotificationInfo.class); + when(gcNotificationInfo.getGcName()).thenReturn(gcName); + when(gcNotificationInfo.getGcAction()).thenReturn(gcAction); + GcInfo gcInfo = mock(GcInfo.class); + when(gcInfo.getDuration()).thenReturn(duration); + when(gcNotificationInfo.getGcInfo()).thenReturn(gcInfo); + return new TestNotification(gcNotificationInfo); + } + + private static GarbageCollectionNotificationInfo getGcNotificationInfo( + Notification notification) { + return ((TestNotification) notification).gcNotificationInfo; + } + + /** + * A {@link Notification} when is initialized with a mock {@link + * GarbageCollectionNotificationInfo}. + */ + private static class TestNotification extends Notification { + + private static final AtomicLong sequence = new AtomicLong(0); + + private final GarbageCollectionNotificationInfo gcNotificationInfo; + + private TestNotification(GarbageCollectionNotificationInfo gcNotificationInfo) { + super( + GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION, + "test", + sequence.incrementAndGet()); + this.gcNotificationInfo = gcNotificationInfo; + } + } +}