However, if you want only the {@link JvmGarbageCollectorMetrics} you can also register them * directly: * *
{@code
* JvmGarbageCollectorMetrics.builder().register();
* }
*
- * Example metrics being exported:
+ * Example metrics being exported: * *
* # HELP jvm_gc_collection_seconds Time spent in a given JVM garbage collector in seconds.
@@ -40,6 +44,7 @@
public class JvmGarbageCollectorMetrics {
private static final String JVM_GC_COLLECTION_SECONDS = "jvm_gc_collection_seconds";
+ private static final String JVM_GC_DURATION = "jvm.gc.duration";
private final PrometheusProperties config;
private final List garbageCollectorBeans;
@@ -55,7 +60,14 @@ private JvmGarbageCollectorMetrics(
}
private void register(PrometheusRegistry registry) {
+ if (config.useOtelSemconv(JVM_GC_DURATION)) {
+ registerOtel(registry);
+ } else {
+ registerPrometheus(registry);
+ }
+ }
+ private void registerPrometheus(PrometheusRegistry registry) {
SummaryWithCallback.builder(config)
.name(JVM_GC_COLLECTION_SECONDS)
.help("Time spent in a given JVM garbage collector in seconds.")
@@ -75,6 +87,54 @@ private void register(PrometheusRegistry registry) {
.register(registry);
}
+ private void registerOtel(PrometheusRegistry registry) {
+ double[] buckets = {0.01, 0.1, 1, 10};
+
+ Histogram gcDurationHistogram =
+ Histogram.builder(config)
+ .name(JVM_GC_DURATION)
+ .unit(Unit.SECONDS)
+ .help("Duration of JVM garbage collection actions.")
+ .labelNames("jvm.gc.action", "jvm.gc.name", "jvm.gc.cause")
+ .classicUpperBounds(buckets)
+ .register(registry);
+
+ registerNotificationListener(gcDurationHistogram);
+ }
+
+ private void registerNotificationListener(Histogram gcDurationHistogram) {
+ for (GarbageCollectorMXBean gcBean : garbageCollectorBeans) {
+
+ if (!(gcBean instanceof NotificationEmitter)) {
+ continue;
+ }
+
+ ((NotificationEmitter) gcBean)
+ .addNotificationListener(
+ (notification, handback) -> {
+ if (!GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION.equals(
+ notification.getType())) {
+ return;
+ }
+
+ GarbageCollectionNotificationInfo info =
+ GarbageCollectionNotificationInfo.from(
+ (CompositeData) notification.getUserData());
+
+ observe(gcDurationHistogram, info);
+ },
+ null,
+ null);
+ }
+ }
+
+ private void observe(Histogram gcDurationHistogram, GarbageCollectionNotificationInfo info) {
+ double observedDuration = Unit.millisToSeconds(info.getGcInfo().getDuration());
+ gcDurationHistogram
+ .labelValues(info.getGcAction(), info.getGcName(), info.getGcCause())
+ .observe(observedDuration);
+ }
+
public static Builder builder() {
return new Builder(PrometheusProperties.get());
}
diff --git a/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetricsTest.java b/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetricsTest.java
index 0f928ef34..e8c114db5 100644
--- a/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetricsTest.java
+++ b/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetricsTest.java
@@ -1,26 +1,38 @@
package io.prometheus.metrics.instrumentation.jvm;
+import static com.sun.management.GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION;
import static io.prometheus.metrics.instrumentation.jvm.TestUtil.convertToOpenMetricsFormat;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentCaptor.forClass;
+import static org.mockito.Mockito.*;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import io.prometheus.metrics.config.MetricsProperties;
+import io.prometheus.metrics.config.PrometheusProperties;
import io.prometheus.metrics.model.registry.MetricNameFilter;
import io.prometheus.metrics.model.registry.PrometheusRegistry;
import io.prometheus.metrics.model.snapshots.MetricSnapshots;
import java.io.IOException;
import java.lang.management.GarbageCollectorMXBean;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
+import javax.management.Notification;
+import javax.management.NotificationEmitter;
+import javax.management.NotificationListener;
+import javax.management.openmbean.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
+import org.mockito.ArgumentCaptor;
class JvmGarbageCollectorMetricsTest {
- private final GarbageCollectorMXBean mockGcBean1 = Mockito.mock(GarbageCollectorMXBean.class);
- private final GarbageCollectorMXBean mockGcBean2 = Mockito.mock(GarbageCollectorMXBean.class);
+ private final GarbageCollectorMXBean mockGcBean1 = mock(GarbageCollectorMXBean.class);
+ private final GarbageCollectorMXBean mockGcBean2 = mock(GarbageCollectorMXBean.class);
@BeforeEach
void setUp() {
@@ -58,7 +70,9 @@ void testGoodCase() throws IOException {
@Test
void testIgnoredMetricNotScraped() {
MetricNameFilter filter =
- MetricNameFilter.builder().nameMustNotBeEqualTo("jvm_gc_collection_seconds").build();
+ MetricNameFilter.builder()
+ .nameMustNotBeEqualTo("jvm_gc_collection_seconds", "jvm_gc_duration")
+ .build();
PrometheusRegistry registry = new PrometheusRegistry();
JvmGarbageCollectorMetrics.builder()
@@ -70,4 +84,170 @@ void testIgnoredMetricNotScraped() {
verify(mockGcBean1, times(0)).getCollectionCount();
assertThat(snapshots.size()).isZero();
}
+
+ @Test
+ public void testNonOtelMetricsAbsentWhenUseOtelEnabled() {
+
+ PrometheusRegistry registry = new PrometheusRegistry();
+ PrometheusProperties properties =
+ PrometheusProperties.builder()
+ .defaultMetricsProperties(MetricsProperties.builder().useOtelSemconv("*").build())
+ .build();
+ JvmGarbageCollectorMetrics.builder(properties)
+ .garbageCollectorBeans(Arrays.asList(mockGcBean1, mockGcBean2))
+ .register(registry);
+ registry.scrape();
+
+ verify(mockGcBean1, times(0)).getCollectionTime();
+ verify(mockGcBean1, times(0)).getCollectionCount();
+ }
+
+ @Test
+ @SuppressWarnings("rawtypes")
+ public void testGCDurationHistogramLabels() throws Exception {
+ GarbageCollectorMXBean mockGcBean =
+ mock(
+ GarbageCollectorMXBean.class,
+ withSettings().extraInterfaces(NotificationEmitter.class));
+ when(mockGcBean.getName()).thenReturn("MyGC");
+
+ PrometheusProperties properties =
+ PrometheusProperties.builder()
+ .defaultMetricsProperties(MetricsProperties.builder().useOtelSemconv("*").build())
+ .build();
+
+ PrometheusRegistry registry = new PrometheusRegistry();
+ JvmGarbageCollectorMetrics.builder(properties)
+ .garbageCollectorBeans(Collections.singletonList(mockGcBean))
+ .register(registry);
+
+ NotificationListener listener;
+ ArgumentCaptor captor = forClass(NotificationListener.class);
+ verify((NotificationEmitter) mockGcBean)
+ .addNotificationListener(captor.capture(), isNull(), isNull());
+ listener = captor.getValue();
+
+ CompositeData notificationData = getNotificationData();
+
+ Notification notification =
+ new Notification(
+ GARBAGE_COLLECTION_NOTIFICATION, mockGcBean, 1, System.currentTimeMillis(), "gc");
+ notification.setUserData(notificationData);
+
+ listener.handleNotification(notification, null);
+
+ MetricSnapshots snapshots = registry.scrape();
+
+ String expected =
+ """
+ {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="0.01"} 0
+ {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="0.1"} 1
+ {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="1.0"} 1
+ {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="10.0"} 1
+ {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="+Inf"} 1
+ {"jvm.gc.duration_seconds_count","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC"} 1
+ {"jvm.gc.duration_seconds_sum","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC"} 0.1
+ """;
+
+ String metrics = convertToOpenMetricsFormat(snapshots);
+
+ assertThat(metrics).contains(expected);
+ }
+
+ private CompositeData getNotificationData() throws OpenDataException {
+ TabularType memoryTabularType = getMemoryTabularType();
+ TabularData memoryBefore = new TabularDataSupport(memoryTabularType);
+ TabularData memoryAfter = new TabularDataSupport(memoryTabularType);
+
+ CompositeData notificationData =
+ getGCNotificationData(memoryTabularType, memoryBefore, memoryAfter);
+ return notificationData;
+ }
+
+ private CompositeData getGCNotificationData(
+ TabularType memoryTabularType, TabularData memoryBefore, TabularData memoryAfter)
+ throws OpenDataException {
+ CompositeType gcInfoType = getGCInfoCompositeType(memoryTabularType);
+
+ Map gcInfoMap = getGcInfoMap(memoryBefore, memoryAfter);
+
+ CompositeData notificationData = getGcNotificationData(gcInfoType, gcInfoMap);
+ return notificationData;
+ }
+
+ private Map getGcInfoMap(TabularData memoryBefore, TabularData memoryAfter) {
+ Map gcInfoMap = new HashMap<>();
+ gcInfoMap.put("id", 0L);
+ gcInfoMap.put("startTime", 100L);
+ gcInfoMap.put("endTime", 200L);
+ gcInfoMap.put("duration", 100L);
+ gcInfoMap.put("memoryUsageBeforeGc", memoryBefore);
+ gcInfoMap.put("memoryUsageAfterGc", memoryAfter);
+ return gcInfoMap;
+ }
+
+ private CompositeType getGCInfoCompositeType(TabularType memoryTabularType)
+ throws OpenDataException {
+ return new CompositeType(
+ "sun.management.BaseGcInfoCompositeType",
+ "gcInfo",
+ new String[] {
+ "id", "startTime", "endTime", "duration", "memoryUsageBeforeGc", "memoryUsageAfterGc"
+ },
+ new String[] {
+ "id", "startTime", "endTime", "duration", "memoryUsageBeforeGc", "memoryUsageAfterGc"
+ },
+ new OpenType>[] {
+ SimpleType.LONG,
+ SimpleType.LONG,
+ SimpleType.LONG,
+ SimpleType.LONG,
+ memoryTabularType,
+ memoryTabularType
+ });
+ }
+
+ private TabularType getMemoryTabularType() throws OpenDataException {
+ CompositeType memoryUsageType =
+ new CompositeType(
+ "java.lang.management.MemoryUsage",
+ "MemoryUsage",
+ new String[] {"init", "used", "committed", "max"},
+ new String[] {"init", "used", "committed", "max"},
+ new OpenType>[] {SimpleType.LONG, SimpleType.LONG, SimpleType.LONG, SimpleType.LONG});
+
+ CompositeType memoryUsageEntryType =
+ new CompositeType(
+ "memoryUsageEntry",
+ "memoryUsageEntry",
+ new String[] {"key", "value"},
+ new String[] {"key", "value"},
+ new OpenType>[] {SimpleType.STRING, memoryUsageType});
+
+ return new TabularType(
+ "memoryUsageTabular", "memoryUsageTabular", memoryUsageEntryType, new String[] {"key"});
+ }
+
+ private static CompositeData getGcNotificationData(
+ CompositeType gcInfoType, Map gcInfoMap) throws OpenDataException {
+ CompositeData gcInfoData = new CompositeDataSupport(gcInfoType, gcInfoMap);
+
+ CompositeType notificationType =
+ new CompositeType(
+ "sun.management.BaseGarbageCollectionNotifInfoCompositeType",
+ "GarbageCollectionNotificationInfo",
+ new String[] {"gcAction", "gcName", "gcCause", "gcInfo"},
+ new String[] {"gcAction", "gcName", "gcCause", "gcInfo"},
+ new OpenType>[] {
+ SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, gcInfoType
+ });
+
+ Map notifMap = new HashMap<>();
+ notifMap.put("gcAction", "end of minor GC");
+ notifMap.put("gcName", "MyGC");
+ notifMap.put("gcCause", "testCause");
+ notifMap.put("gcInfo", gcInfoData);
+
+ return new CompositeDataSupport(notificationType, notifMap);
+ }
}