From ff3ece37e8d718f31922d91eaedcf4654ac7ba6f Mon Sep 17 00:00:00 2001 From: gniadeck <77535280+gniadeck@users.noreply.github.com> Date: Sun, 14 Dec 2025 14:13:45 +0100 Subject: [PATCH 01/12] Add GC pause duration histogram Signed-off-by: gniadeck <77535280+gniadeck@users.noreply.github.com> --- .../jvm/JvmGarbageCollectorMetrics.java | 46 +++++++++++++++++++ .../jvm/JvmGarbageCollectorMetricsTest.java | 4 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java index a87b52a4f..bab35959b 100644 --- a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java +++ b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java @@ -1,6 +1,8 @@ package io.prometheus.metrics.instrumentation.jvm; +import com.sun.management.GarbageCollectionNotificationInfo; import io.prometheus.metrics.config.PrometheusProperties; +import io.prometheus.metrics.core.metrics.Histogram; import io.prometheus.metrics.core.metrics.SummaryWithCallback; import io.prometheus.metrics.model.registry.PrometheusRegistry; import io.prometheus.metrics.model.snapshots.Labels; @@ -10,6 +12,8 @@ import java.lang.management.ManagementFactory; import java.util.List; import javax.annotation.Nullable; +import javax.management.NotificationEmitter; +import javax.management.openmbean.CompositeData; /** * JVM Garbage Collector metrics. The {@link JvmGarbageCollectorMetrics} are registered as part of @@ -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_SECONDS = "jvm_gc_duration_seconds"; private final PrometheusProperties config; private final List garbageCollectorBeans; @@ -73,6 +78,47 @@ private void register(PrometheusRegistry registry) { }) .constLabels(constLabels) .register(registry); + + registerGCDurationHistogram(registry); + } + + private void registerGCDurationHistogram(PrometheusRegistry registry) { + double[] buckets = {0.01, 0.1, 1, 10}; + + Histogram gcDurationHistogram = + Histogram.builder(config) + .name(JVM_GC_DURATION_SECONDS) + .help("JVM GC pause duration histogram.") + .unit(Unit.SECONDS) + .labelNames("gc", "action", "cause") + .classicUpperBounds(buckets) + .register(registry); + + for (GarbageCollectorMXBean gcBean : garbageCollectorBeans) { + + if (!(gcBean instanceof NotificationEmitter)) { + continue; + } + + NotificationEmitter notificationEmitter = (NotificationEmitter) gcBean; + + notificationEmitter.addNotificationListener( + (notification, handback) -> { + if (!GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION.equals( + notification.getType())) { + return; + } + + GarbageCollectionNotificationInfo info = + GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData()); + + gcDurationHistogram + .labelValues(info.getGcName(), info.getGcAction(), info.getGcCause()) + .observe(Unit.millisToSeconds(info.getGcInfo().getDuration())); + }, + null, + null); + } } public static Builder builder() { 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..b4f6df3d9 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 @@ -58,7 +58,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_seconds") + .build(); PrometheusRegistry registry = new PrometheusRegistry(); JvmGarbageCollectorMetrics.builder() From b2e2af368a999822afcd73bc86aceb4027bf18c4 Mon Sep 17 00:00:00 2001 From: gniadeck <77535280+gniadeck@users.noreply.github.com> Date: Sun, 14 Dec 2025 14:19:40 +0100 Subject: [PATCH 02/12] align gc label name with opentelemetry spec Signed-off-by: gniadeck <77535280+gniadeck@users.noreply.github.com> --- .../metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java index bab35959b..44f615250 100644 --- a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java +++ b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java @@ -90,7 +90,7 @@ private void registerGCDurationHistogram(PrometheusRegistry registry) { .name(JVM_GC_DURATION_SECONDS) .help("JVM GC pause duration histogram.") .unit(Unit.SECONDS) - .labelNames("gc", "action", "cause") + .labelNames("name", "action", "cause") .classicUpperBounds(buckets) .register(registry); From 642392263c18258fa33df0f0b1fdd0e079809945 Mon Sep 17 00:00:00 2001 From: gniadeck <77535280+gniadeck@users.noreply.github.com> Date: Sun, 14 Dec 2025 14:23:53 +0100 Subject: [PATCH 03/12] simplification, spotless fixes Signed-off-by: gniadeck <77535280+gniadeck@users.noreply.github.com> --- .../jvm/JvmGarbageCollectorMetrics.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java index 44f615250..3cb7ceb44 100644 --- a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java +++ b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java @@ -100,24 +100,24 @@ private void registerGCDurationHistogram(PrometheusRegistry registry) { continue; } - NotificationEmitter notificationEmitter = (NotificationEmitter) gcBean; - - notificationEmitter.addNotificationListener( - (notification, handback) -> { - if (!GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION.equals( - notification.getType())) { - return; - } - - GarbageCollectionNotificationInfo info = - GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData()); - - gcDurationHistogram - .labelValues(info.getGcName(), info.getGcAction(), info.getGcCause()) - .observe(Unit.millisToSeconds(info.getGcInfo().getDuration())); - }, - null, - null); + ((NotificationEmitter) gcBean) + .addNotificationListener( + (notification, handback) -> { + if (!GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION.equals( + notification.getType())) { + return; + } + + GarbageCollectionNotificationInfo info = + GarbageCollectionNotificationInfo.from( + (CompositeData) notification.getUserData()); + + gcDurationHistogram + .labelValues(info.getGcName(), info.getGcAction(), info.getGcCause()) + .observe(Unit.millisToSeconds(info.getGcInfo().getDuration())); + }, + null, + null); } } From 683150b5339e40dbbf4d0aa63dbd72246a0074d1 Mon Sep 17 00:00:00 2001 From: gniadeck <77535280+gniadeck@users.noreply.github.com> Date: Tue, 16 Dec 2025 20:56:42 +0100 Subject: [PATCH 04/12] align naming with opentelemetry semantic conventions Signed-off-by: gniadeck <77535280+gniadeck@users.noreply.github.com> --- .../jvm/JvmGarbageCollectorMetrics.java | 11 +++++------ .../jvm/JvmGarbageCollectorMetricsTest.java | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java index 3cb7ceb44..ed6390a1a 100644 --- a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java +++ b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java @@ -44,7 +44,7 @@ public class JvmGarbageCollectorMetrics { private static final String JVM_GC_COLLECTION_SECONDS = "jvm_gc_collection_seconds"; - private static final String JVM_GC_DURATION_SECONDS = "jvm_gc_duration_seconds"; + private static final String JVM_GC_DURATION = "jvm_gc_duration"; private final PrometheusProperties config; private final List garbageCollectorBeans; @@ -87,10 +87,9 @@ private void registerGCDurationHistogram(PrometheusRegistry registry) { Histogram gcDurationHistogram = Histogram.builder(config) - .name(JVM_GC_DURATION_SECONDS) - .help("JVM GC pause duration histogram.") - .unit(Unit.SECONDS) - .labelNames("name", "action", "cause") + .name(JVM_GC_DURATION) + .help("Duration of JVM garbage collection actions.") + .labelNames("jvm_gc_action", "jvm_gc_name", "jvm_gc_cause") .classicUpperBounds(buckets) .register(registry); @@ -113,7 +112,7 @@ private void registerGCDurationHistogram(PrometheusRegistry registry) { (CompositeData) notification.getUserData()); gcDurationHistogram - .labelValues(info.getGcName(), info.getGcAction(), info.getGcCause()) + .labelValues(info.getGcAction(), info.getGcName(), info.getGcCause()) .observe(Unit.millisToSeconds(info.getGcInfo().getDuration())); }, null, 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 b4f6df3d9..dbd50867e 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 @@ -59,7 +59,7 @@ void testGoodCase() throws IOException { void testIgnoredMetricNotScraped() { MetricNameFilter filter = MetricNameFilter.builder() - .nameMustNotBeEqualTo("jvm_gc_collection_seconds", "jvm_gc_duration_seconds") + .nameMustNotBeEqualTo("jvm_gc_collection_seconds", "jvm_gc_duration") .build(); PrometheusRegistry registry = new PrometheusRegistry(); From 298717f51f101ae5f2539b8209e416d267cc815a Mon Sep 17 00:00:00 2001 From: gniadeck <77535280+gniadeck@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:16:15 +0100 Subject: [PATCH 05/12] Label test and migration to opentelemetry naming Signed-off-by: gniadeck <77535280+gniadeck@users.noreply.github.com> --- .../jvm/JvmGarbageCollectorMetrics.java | 4 +- .../jvm/JvmGarbageCollectorMetricsTest.java | 136 +++++++++++++++++- 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java index ed6390a1a..8d37b723e 100644 --- a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java +++ b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java @@ -44,7 +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 static final String JVM_GC_DURATION = "jvm.gc.duration"; private final PrometheusProperties config; private final List garbageCollectorBeans; @@ -89,7 +89,7 @@ private void registerGCDurationHistogram(PrometheusRegistry registry) { Histogram.builder(config) .name(JVM_GC_DURATION) .help("Duration of JVM garbage collection actions.") - .labelNames("jvm_gc_action", "jvm_gc_name", "jvm_gc_cause") + .labelNames("jvm.gc.action", "jvm.gc.name", "jvm.gc.cause") .classicUpperBounds(buckets) .register(registry); 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 dbd50867e..0e634e52e 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,7 +1,10 @@ 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; @@ -12,15 +15,22 @@ 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() { @@ -72,4 +82,124 @@ void testIgnoredMetricNotScraped() { verify(mockGcBean1, times(0)).getCollectionCount(); assertThat(snapshots.size()).isZero(); } + + @Test + @SuppressWarnings("rawtypes") + public void testGCDurationHistogramLabels() throws Exception { + GarbageCollectorMXBean mockGcBean = + mock( + GarbageCollectorMXBean.class, + withSettings().extraInterfaces(NotificationEmitter.class)); + when(mockGcBean.getName()).thenReturn("MyGC"); + + PrometheusRegistry registry = new PrometheusRegistry(); + JvmGarbageCollectorMetrics.builder() + .garbageCollectorBeans(Collections.singletonList(mockGcBean)) + .register(registry); + + NotificationListener listener; + ArgumentCaptor captor = forClass(NotificationListener.class); + verify((NotificationEmitter) mockGcBean) + .addNotificationListener(captor.capture(), isNull(), isNull()); + listener = captor.getValue(); + + TabularType memoryTabularType = getMemoryTabularType(); + TabularData memoryBefore = new TabularDataSupport(memoryTabularType); + TabularData memoryAfter = new TabularDataSupport(memoryTabularType); + + CompositeType gcInfoType = + 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 + }); + + java.util.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); + + CompositeData notificationData = getGcNotificationData(gcInfoType, gcInfoMap); + + 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_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="0.01"} 0 + {"jvm.gc.duration_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="0.1"} 1 + {"jvm.gc.duration_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="1.0"} 1 + {"jvm.gc.duration_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="10.0"} 1 + {"jvm.gc.duration_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="+Inf"} 1 + {"jvm.gc.duration_count","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC"} 1 + {"jvm.gc.duration_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 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); + } } From c5d1b90e472de53cead062383f946d1de8b6c87c Mon Sep 17 00:00:00 2001 From: gniadeck <77535280+gniadeck@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:26:44 +0100 Subject: [PATCH 06/12] Add useOtelMetrics to MetricsProperties Signed-off-by: gniadeck <77535280+gniadeck@users.noreply.github.com> --- .../metrics/config/MetricsProperties.java | 25 +++++++++++++-- .../jvm/JvmGarbageCollectorMetrics.java | 9 ++++-- .../jvm/JvmGarbageCollectorMetricsTest.java | 32 ++++++++++++++++--- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java index a530f35e1..11947bca1 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java @@ -48,6 +48,7 @@ public class MetricsProperties { SUMMARY_MAX_AGE_SECONDS, SUMMARY_NUMBER_OF_AGE_BUCKETS }; + private static final String USE_OTEL_METRICS = "useOtelMetrics"; @Nullable private final Boolean exemplarsEnabled; @Nullable private final Boolean histogramNativeOnly; @@ -62,6 +63,7 @@ public class MetricsProperties { @Nullable private final List summaryQuantileErrors; @Nullable private final Long summaryMaxAgeSeconds; @Nullable private final Integer summaryNumberOfAgeBuckets; + @Nullable private final Boolean useOtelMetrics; public MetricsProperties( @Nullable Boolean exemplarsEnabled, @@ -76,7 +78,8 @@ public MetricsProperties( @Nullable List summaryQuantiles, @Nullable List summaryQuantileErrors, @Nullable Long summaryMaxAgeSeconds, - @Nullable Integer summaryNumberOfAgeBuckets) { + @Nullable Integer summaryNumberOfAgeBuckets, + @Nullable Boolean useOtelMetrics) { this( exemplarsEnabled, histogramNativeOnly, @@ -91,6 +94,7 @@ public MetricsProperties( summaryQuantileErrors, summaryMaxAgeSeconds, summaryNumberOfAgeBuckets, + useOtelMetrics, ""); } @@ -108,6 +112,7 @@ private MetricsProperties( @Nullable List summaryQuantileErrors, @Nullable Long summaryMaxAgeSeconds, @Nullable Integer summaryNumberOfAgeBuckets, + @Nullable Boolean useOtelMetrics, String configPropertyPrefix) { this.exemplarsEnabled = exemplarsEnabled; this.histogramNativeOnly = isHistogramNativeOnly(histogramClassicOnly, histogramNativeOnly); @@ -129,6 +134,7 @@ private MetricsProperties( : unmodifiableList(new ArrayList<>(summaryQuantileErrors)); this.summaryMaxAgeSeconds = summaryMaxAgeSeconds; this.summaryNumberOfAgeBuckets = summaryNumberOfAgeBuckets; + this.useOtelMetrics = useOtelMetrics; validate(configPropertyPrefix); } @@ -353,6 +359,12 @@ public Integer getSummaryNumberOfAgeBuckets() { return summaryNumberOfAgeBuckets; } + /** See {@code Summary.Builder.useOtelMetrics()} */ + @Nullable + public Boolean useOtelMetrics() { + return useOtelMetrics; + } + /** * Note that this will remove entries from {@code propertySource}. This is because we want to know * if there are unused properties remaining after all properties have been loaded. @@ -373,6 +385,7 @@ static MetricsProperties load(String prefix, PropertySource propertySource) Util.loadDoubleList(prefix, SUMMARY_QUANTILE_ERRORS, propertySource), Util.loadLong(prefix, SUMMARY_MAX_AGE_SECONDS, propertySource), Util.loadInteger(prefix, SUMMARY_NUMBER_OF_AGE_BUCKETS, propertySource), + Util.loadBoolean(prefix + "." + USE_OTEL_METRICS, properties), prefix); } @@ -394,6 +407,7 @@ public static class Builder { @Nullable private List summaryQuantileErrors; @Nullable private Long summaryMaxAgeSeconds; @Nullable private Integer summaryNumberOfAgeBuckets; + @Nullable private Boolean useOtelMetrics; private Builder() {} @@ -411,7 +425,8 @@ public MetricsProperties build() { summaryQuantiles, summaryQuantileErrors, summaryMaxAgeSeconds, - summaryNumberOfAgeBuckets); + summaryNumberOfAgeBuckets, + useOtelMetrics); } /** See {@link MetricsProperties#getExemplarsEnabled()} */ @@ -495,5 +510,11 @@ public Builder summaryNumberOfAgeBuckets(@Nullable Integer summaryNumberOfAgeBuc this.summaryNumberOfAgeBuckets = summaryNumberOfAgeBuckets; return this; } + + /** See {@link MetricsProperties#useOtelMetrics()} */ + public Builder useOtelMetrics(@Nullable Boolean useOtelMetrics) { + this.useOtelMetrics = useOtelMetrics; + return this; + } } } diff --git a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java index 8d37b723e..62cdf87ba 100644 --- a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java +++ b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java @@ -60,7 +60,14 @@ private JvmGarbageCollectorMetrics( } private void register(PrometheusRegistry registry) { + if (Boolean.TRUE.equals(config.getDefaultMetricProperties().useOtelMetrics())) { + registerGCDurationHistogram(registry); + } else { + registerGCDurationSummary(registry); + } + } + private void registerGCDurationSummary(PrometheusRegistry registry) { SummaryWithCallback.builder(config) .name(JVM_GC_COLLECTION_SECONDS) .help("Time spent in a given JVM garbage collector in seconds.") @@ -78,8 +85,6 @@ private void register(PrometheusRegistry registry) { }) .constLabels(constLabels) .register(registry); - - registerGCDurationHistogram(registry); } private void registerGCDurationHistogram(PrometheusRegistry registry) { 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 0e634e52e..842d4881e 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 @@ -9,6 +9,8 @@ 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; @@ -83,6 +85,23 @@ void testIgnoredMetricNotScraped() { assertThat(snapshots.size()).isZero(); } + @Test + public void testNonOtelMetricsAbsentWhenUseOtelEnabled() { + + PrometheusRegistry registry = new PrometheusRegistry(); + PrometheusProperties properties = + PrometheusProperties.builder() + .defaultMetricsProperties(MetricsProperties.builder().useOtelMetrics(true).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 { @@ -92,8 +111,13 @@ public void testGCDurationHistogramLabels() throws Exception { withSettings().extraInterfaces(NotificationEmitter.class)); when(mockGcBean.getName()).thenReturn("MyGC"); + PrometheusProperties properties = + PrometheusProperties.builder() + .defaultMetricsProperties(MetricsProperties.builder().useOtelMetrics(true).build()) + .build(); + PrometheusRegistry registry = new PrometheusRegistry(); - JvmGarbageCollectorMetrics.builder() + JvmGarbageCollectorMetrics.builder(properties) .garbageCollectorBeans(Collections.singletonList(mockGcBean)) .register(registry); @@ -117,7 +141,7 @@ public void testGCDurationHistogramLabels() throws Exception { new String[] { "id", "startTime", "endTime", "duration", "memoryUsageBeforeGc", "memoryUsageAfterGc" }, - new OpenType[] { + new OpenType[] { SimpleType.LONG, SimpleType.LONG, SimpleType.LONG, @@ -168,7 +192,7 @@ private TabularType getMemoryTabularType() throws OpenDataException { "MemoryUsage", new String[] {"init", "used", "committed", "max"}, new String[] {"init", "used", "committed", "max"}, - new OpenType[] {SimpleType.LONG, SimpleType.LONG, SimpleType.LONG, SimpleType.LONG}); + new OpenType[] {SimpleType.LONG, SimpleType.LONG, SimpleType.LONG, SimpleType.LONG}); CompositeType memoryUsageEntryType = new CompositeType( @@ -176,7 +200,7 @@ private TabularType getMemoryTabularType() throws OpenDataException { "memoryUsageEntry", new String[] {"key", "value"}, new String[] {"key", "value"}, - new OpenType[] {SimpleType.STRING, memoryUsageType}); + new OpenType[] {SimpleType.STRING, memoryUsageType}); return new TabularType( "memoryUsageTabular", "memoryUsageTabular", memoryUsageEntryType, new String[] {"key"}); From 94ac2d6b5b111b3674dcf3294a1754550c020481 Mon Sep 17 00:00:00 2001 From: gniadeck <77535280+gniadeck@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:59:13 +0100 Subject: [PATCH 07/12] Add type info to remove compilation warning Signed-off-by: gniadeck <77535280+gniadeck@users.noreply.github.com> --- .../instrumentation/jvm/JvmGarbageCollectorMetricsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 842d4881e..cc994a964 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 @@ -216,7 +216,7 @@ private static CompositeData getGcNotificationData( "GarbageCollectionNotificationInfo", new String[] {"gcAction", "gcName", "gcCause", "gcInfo"}, new String[] {"gcAction", "gcName", "gcCause", "gcInfo"}, - new OpenType[] {SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, gcInfoType}); + new OpenType[] {SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, gcInfoType}); Map notifMap = new HashMap<>(); notifMap.put("gcAction", "end of minor GC"); From 2f73e112cf33b3acb653d9b5819467ac915ffc8b Mon Sep 17 00:00:00 2001 From: gniadeck <77535280+gniadeck@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:15:43 +0100 Subject: [PATCH 08/12] apply spotless Signed-off-by: gniadeck <77535280+gniadeck@users.noreply.github.com> --- .../instrumentation/jvm/JvmGarbageCollectorMetricsTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 cc994a964..84b9fcfc9 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 @@ -216,7 +216,9 @@ private static CompositeData getGcNotificationData( "GarbageCollectionNotificationInfo", new String[] {"gcAction", "gcName", "gcCause", "gcInfo"}, new String[] {"gcAction", "gcName", "gcCause", "gcInfo"}, - new OpenType[] {SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, gcInfoType}); + new OpenType[] { + SimpleType.STRING, SimpleType.STRING, SimpleType.STRING, gcInfoType + }); Map notifMap = new HashMap<>(); notifMap.put("gcAction", "end of minor GC"); From 5d925cc9494dbd4bcba58ec7f65e47dd1a6f2894 Mon Sep 17 00:00:00 2001 From: gniadeck <77535280+gniadeck@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:24:45 +0100 Subject: [PATCH 09/12] add unit to jvm gc histogram, improve isOtelMetric handling, add otelOptIn metric property Signed-off-by: gniadeck <77535280+gniadeck@users.noreply.github.com> --- docs/content/config/config.md | 3 +- docs/content/instrumentation/jvm.md | 15 ++ .../metrics/config/MetricsProperties.java | 31 +++- .../metrics/config/PrometheusProperties.java | 15 ++ .../config/PrometheusPropertiesTest.java | 40 +++++ .../jvm/JvmGarbageCollectorMetrics.java | 39 ++++- .../jvm/JvmGarbageCollectorMetricsTest.java | 158 ++++++++++++++---- 7 files changed, 255 insertions(+), 46 deletions(-) diff --git a/docs/content/config/config.md b/docs/content/config/config.md index cd7f7af7b..810aeca57 100644 --- a/docs/content/config/config.md +++ b/docs/content/config/config.md @@ -75,7 +75,7 @@ When the same property is defined in multiple sources, the following precedence | Name | Javadoc | Note | -| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +|---------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| | io.prometheus.metrics.exemplars_enabled | [Counter.Builder.withExemplars()]() | (1) (2) | | io.prometheus.metrics.histogram_native_only | [Histogram.Builder.nativeOnly()]() | (2) | | io.prometheus.metrics.histogram_classic_only | [Histogram.Builder.classicOnly()]() | (2) | @@ -89,6 +89,7 @@ When the same property is defined in multiple sources, the following precedence | io.prometheus.metrics.summary_quantile_errors | [Summary.Builder.quantile(double, double)]() | (5) | | io.prometheus.metrics.summary_max_age_seconds | [Summary.Builder.maxAgeSeconds()]() | | | io.prometheus.metrics.summary_number_of_age_buckets | [Summary.Builder.numberOfAgeBuckets()]() | | +| io.prometheus.metrics.otel_opt_in | [MetricsProperties.isOtelOptIn()]() | (2) | diff --git a/docs/content/instrumentation/jvm.md b/docs/content/instrumentation/jvm.md index 804c1b09b..110df42f0 100644 --- a/docs/content/instrumentation/jvm.md +++ b/docs/content/instrumentation/jvm.md @@ -123,6 +123,21 @@ jvm_gc_collection_seconds_count{gc="PS Scavenge"} 0 jvm_gc_collection_seconds_sum{gc="PS Scavenge"} 0.0 ``` +For more detailed GC metrics, enable the [useOtelMetrics](https://prometheus.github.io/client_java/config/config/#metrics-properties) configuration option. This replaces the standard metric with a +histogram implemented according to the [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmgcduration). + +```text +# HELP jvm_gc_duration_seconds Duration of JVM garbage collection actions. +# TYPE jvm_gc_duration_seconds histogram +jvm_gc_duration_seconds_bucket{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation",le="0.01"} 4 +jvm_gc_duration_seconds_bucket{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation",le="0.1"} 4 +jvm_gc_duration_seconds_bucket{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation",le="1.0"} 4 +jvm_gc_duration_seconds_bucket{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation",le="10.0"} 4 +jvm_gc_duration_seconds_bucket{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation",le="+Inf"} 4 +jvm_gc_duration_seconds_count{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation"} 4 +jvm_gc_duration_seconds_sum{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation"} 0.029 +``` + ## JVM Memory Metrics JVM memory metrics are provided by diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java index 11947bca1..fd8954cb5 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java @@ -49,6 +49,7 @@ public class MetricsProperties { SUMMARY_NUMBER_OF_AGE_BUCKETS }; private static final String USE_OTEL_METRICS = "useOtelMetrics"; + private static final String OTEL_OPT_IN = "otelOptIn"; @Nullable private final Boolean exemplarsEnabled; @Nullable private final Boolean histogramNativeOnly; @@ -64,6 +65,7 @@ public class MetricsProperties { @Nullable private final Long summaryMaxAgeSeconds; @Nullable private final Integer summaryNumberOfAgeBuckets; @Nullable private final Boolean useOtelMetrics; + @Nullable private final Boolean otelOptIn; public MetricsProperties( @Nullable Boolean exemplarsEnabled, @@ -79,7 +81,8 @@ public MetricsProperties( @Nullable List summaryQuantileErrors, @Nullable Long summaryMaxAgeSeconds, @Nullable Integer summaryNumberOfAgeBuckets, - @Nullable Boolean useOtelMetrics) { + @Nullable Boolean useOtelMetrics, + Boolean otelOptIn) { this( exemplarsEnabled, histogramNativeOnly, @@ -95,6 +98,7 @@ public MetricsProperties( summaryMaxAgeSeconds, summaryNumberOfAgeBuckets, useOtelMetrics, + otelOptIn, ""); } @@ -113,8 +117,10 @@ private MetricsProperties( @Nullable Long summaryMaxAgeSeconds, @Nullable Integer summaryNumberOfAgeBuckets, @Nullable Boolean useOtelMetrics, + @Nullable Boolean otelOptIn, String configPropertyPrefix) { this.exemplarsEnabled = exemplarsEnabled; + this.otelOptIn = otelOptIn; this.histogramNativeOnly = isHistogramNativeOnly(histogramClassicOnly, histogramNativeOnly); this.histogramClassicOnly = isHistogramClassicOnly(histogramClassicOnly, histogramNativeOnly); this.histogramClassicUpperBounds = @@ -359,12 +365,22 @@ public Integer getSummaryNumberOfAgeBuckets() { return summaryNumberOfAgeBuckets; } - /** See {@code Summary.Builder.useOtelMetrics()} */ + /** + * Where applicable, metrics are registered in accordance with OpenTelemetry Semantic Conventions. + * Implementation should respect opt-in requirements and ensure no data duplication occurs with + * existing Prometheus metrics. + */ @Nullable public Boolean useOtelMetrics() { return useOtelMetrics; } + /** Where applicable, if using otel metrics, allow usage of opt-in labels */ + @Nullable + public Boolean isOtelOptIn() { + return otelOptIn; + } + /** * Note that this will remove entries from {@code propertySource}. This is because we want to know * if there are unused properties remaining after all properties have been loaded. @@ -386,6 +402,7 @@ static MetricsProperties load(String prefix, PropertySource propertySource) Util.loadLong(prefix, SUMMARY_MAX_AGE_SECONDS, propertySource), Util.loadInteger(prefix, SUMMARY_NUMBER_OF_AGE_BUCKETS, propertySource), Util.loadBoolean(prefix + "." + USE_OTEL_METRICS, properties), + Util.loadBoolean(prefix + "." + OTEL_OPT_IN, properties), prefix); } @@ -408,6 +425,7 @@ public static class Builder { @Nullable private Long summaryMaxAgeSeconds; @Nullable private Integer summaryNumberOfAgeBuckets; @Nullable private Boolean useOtelMetrics; + @Nullable private Boolean otelOptIn; private Builder() {} @@ -426,7 +444,8 @@ public MetricsProperties build() { summaryQuantileErrors, summaryMaxAgeSeconds, summaryNumberOfAgeBuckets, - useOtelMetrics); + useOtelMetrics, + otelOptIn); } /** See {@link MetricsProperties#getExemplarsEnabled()} */ @@ -516,5 +535,11 @@ public Builder useOtelMetrics(@Nullable Boolean useOtelMetrics) { this.useOtelMetrics = useOtelMetrics; return this; } + + /** See {@link MetricsProperties#isOtelOptIn()} */ + public Builder otelOptIn(@Nullable Boolean otelOptIn) { + this.otelOptIn = otelOptIn; + return this; + } } } diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java index 055fe4aa3..da61df59d 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Optional; import javax.annotation.Nullable; /** @@ -167,6 +168,20 @@ public ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties() { return exporterOpenTelemetryProperties; } + public boolean useOtelMetrics(String prometheusMetric, String otelMetric) { + Boolean useByPrometheusMetric = usesOtelMetric(prometheusMetric); + if (Boolean.FALSE.equals(useByPrometheusMetric)) return false; + Boolean useByOtelMetric = usesOtelMetric(otelMetric); + if (Boolean.FALSE.equals(useByOtelMetric)) return false; + return Boolean.TRUE.equals(getDefaultMetricProperties().useOtelMetrics()); + } + + private Boolean usesOtelMetric(String metric) { + return Optional.ofNullable(getMetricProperties(metric)) + .map(MetricsProperties::useOtelMetrics) + .orElse(null); + } + public static class Builder { private MetricsProperties defaultMetricsProperties = MetricsProperties.builder().build(); private final MetricPropertiesMap metricProperties = new MetricPropertiesMap(); diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java index 3e891202a..607b78839 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java @@ -128,4 +128,44 @@ void testMetricNameStartingWithNumber() { assertThat(result.getMetricProperties("123metric")).isSameAs(customProps); assertThat(result.getMetricProperties("_23metric")).isSameAs(customProps); } + + @Test + void useOtelMetricsSupportsNegativeOverride() { + Map metricMap = new HashMap<>(); + metricMap.put("prom_metric", otelProperties(false)); + PrometheusProperties props = buildProperties(true, metricMap); + assertThat(props.useOtelMetrics("prom_metric", "any_otel_metric")).isFalse(); + } + + @Test + void useOtelMetricsDisablesByMetricName() { + Map metricMap = new HashMap<>(); + metricMap.put("otel_metric", otelProperties(false)); + PrometheusProperties props = buildProperties(true, metricMap); + assertThat(props.useOtelMetrics("some_prom_metric", "otel_metric")).isFalse(); + } + + @Test + void useOtelMetricsRespectsDefaultIfNoOverride() { + PrometheusProperties props = buildProperties(true, Collections.emptyMap()); + assertThat(props.useOtelMetrics("prom_x", "otel_y")).isTrue(); + } + + @Test + void noOverridesReturnsFalse() { + PrometheusProperties props = PrometheusProperties.get(); + assertThat(props.useOtelMetrics("prom_x", "otel_y")).isFalse(); + } + + private static PrometheusProperties buildProperties( + Boolean defaultUse, Map metricProps) { + return PrometheusProperties.builder() + .defaultMetricsProperties(otelProperties(defaultUse)) + .metricProperties(new HashMap<>(metricProps)) + .build(); + } + + private static MetricsProperties otelProperties(Boolean useOtel) { + return MetricsProperties.builder().useOtelMetrics(useOtel).build(); + } } diff --git a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java index 62cdf87ba..f8f41f611 100644 --- a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java +++ b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java @@ -1,6 +1,7 @@ package io.prometheus.metrics.instrumentation.jvm; import com.sun.management.GarbageCollectionNotificationInfo; +import io.prometheus.metrics.config.MetricsProperties; import io.prometheus.metrics.config.PrometheusProperties; import io.prometheus.metrics.core.metrics.Histogram; import io.prometheus.metrics.core.metrics.SummaryWithCallback; @@ -10,7 +11,9 @@ import io.prometheus.metrics.model.snapshots.Unit; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; +import java.util.ArrayList; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; import javax.management.NotificationEmitter; import javax.management.openmbean.CompositeData; @@ -60,7 +63,7 @@ private JvmGarbageCollectorMetrics( } private void register(PrometheusRegistry registry) { - if (Boolean.TRUE.equals(config.getDefaultMetricProperties().useOtelMetrics())) { + if (config.useOtelMetrics(JVM_GC_COLLECTION_SECONDS, JVM_GC_DURATION)) { registerGCDurationHistogram(registry); } else { registerGCDurationSummary(registry); @@ -90,14 +93,28 @@ private void registerGCDurationSummary(PrometheusRegistry registry) { private void registerGCDurationHistogram(PrometheusRegistry registry) { double[] buckets = {0.01, 0.1, 1, 10}; + List labels = new ArrayList<>(List.of("jvm.gc.action", "jvm.gc.name")); + boolean otelOptIn = + Optional.ofNullable(config.getMetricProperties(JVM_GC_DURATION)) + .map(MetricsProperties::isOtelOptIn) + .orElse(false); + if (otelOptIn) { + labels.add("jvm.gc.cause"); + } + 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") + .labelNames(labels.toArray(String[]::new)) .classicUpperBounds(buckets) .register(registry); + registerNotificationListener(gcDurationHistogram, otelOptIn); + } + + private void registerNotificationListener(Histogram gcDurationHistogram, boolean otelOptIn) { for (GarbageCollectorMXBean gcBean : garbageCollectorBeans) { if (!(gcBean instanceof NotificationEmitter)) { @@ -116,15 +133,27 @@ private void registerGCDurationHistogram(PrometheusRegistry registry) { GarbageCollectionNotificationInfo.from( (CompositeData) notification.getUserData()); - gcDurationHistogram - .labelValues(info.getGcAction(), info.getGcName(), info.getGcCause()) - .observe(Unit.millisToSeconds(info.getGcInfo().getDuration())); + observe(gcDurationHistogram, otelOptIn, info); }, null, null); } } + private void observe( + Histogram gcDurationHistogram, boolean otelOptIn, GarbageCollectionNotificationInfo info) { + double observedDuration = Unit.millisToSeconds(info.getGcInfo().getDuration()); + if (otelOptIn) { + gcDurationHistogram + .labelValues(info.getGcAction(), info.getGcName(), info.getGcCause()) + .observe(observedDuration); + } else { + gcDurationHistogram + .labelValues(info.getGcAction(), info.getGcName()) + .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 84b9fcfc9..68b11c127 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 @@ -27,6 +27,8 @@ import javax.management.openmbean.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; class JvmGarbageCollectorMetricsTest { @@ -114,6 +116,8 @@ public void testGCDurationHistogramLabels() throws Exception { PrometheusProperties properties = PrometheusProperties.builder() .defaultMetricsProperties(MetricsProperties.builder().useOtelMetrics(true).build()) + .metricProperties( + Map.of("jvm_gc_duration", MetricsProperties.builder().otelOptIn(true).build())) .build(); PrometheusRegistry registry = new PrometheusRegistry(); @@ -127,38 +131,65 @@ public void testGCDurationHistogramLabels() throws Exception { .addNotificationListener(captor.capture(), isNull(), isNull()); listener = captor.getValue(); - TabularType memoryTabularType = getMemoryTabularType(); - TabularData memoryBefore = new TabularDataSupport(memoryTabularType); - TabularData memoryAfter = new TabularDataSupport(memoryTabularType); + CompositeData notificationData = getNotificationData(); - CompositeType gcInfoType = - 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 - }); + Notification notification = + new Notification( + GARBAGE_COLLECTION_NOTIFICATION, mockGcBean, 1, System.currentTimeMillis(), "gc"); + notification.setUserData(notificationData); - java.util.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); + listener.handleNotification(notification, null); - CompositeData notificationData = getGcNotificationData(gcInfoType, gcInfoMap); + 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); + } + + @SuppressWarnings("rawtypes") + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testGCDurationHistogramLabelsWithNoOptIn(boolean nullOptIn) throws Exception { + GarbageCollectorMXBean mockGcBean = + mock( + GarbageCollectorMXBean.class, + withSettings().extraInterfaces(NotificationEmitter.class)); + when(mockGcBean.getName()).thenReturn("MyGC"); + + PrometheusProperties.Builder builder = + PrometheusProperties.builder() + .defaultMetricsProperties(MetricsProperties.builder().useOtelMetrics(true).build()); + if (!nullOptIn) { + builder = + builder.metricProperties( + Map.of("jvm.gc.duration", MetricsProperties.builder().otelOptIn(true).build())); + } + PrometheusProperties properties = builder.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( @@ -171,20 +202,73 @@ public void testGCDurationHistogramLabels() throws Exception { String expected = """ - {"jvm.gc.duration_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="0.01"} 0 - {"jvm.gc.duration_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="0.1"} 1 - {"jvm.gc.duration_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="1.0"} 1 - {"jvm.gc.duration_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="10.0"} 1 - {"jvm.gc.duration_bucket","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC",le="+Inf"} 1 - {"jvm.gc.duration_count","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC"} 1 - {"jvm.gc.duration_sum","jvm.gc.action"="end of minor GC","jvm.gc.cause"="testCause","jvm.gc.name"="MyGC"} 0.1 - """; + {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC",le="0.01"} 0 + {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC",le="0.1"} 1 + {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC",le="1.0"} 1 + {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC",le="10.0"} 1 + {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC",le="+Inf"} 1 + {"jvm.gc.duration_seconds_count","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC"} 1 + {"jvm.gc.duration_seconds_sum","jvm.gc.action"="end of minor GC","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( From d0e8172df19fd2b921e85965b90a97a6a50d0861 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 6 Feb 2026 17:37:26 +0100 Subject: [PATCH 10/12] remove attribute opt-in Signed-off-by: Gregor Zeitlinger --- docs/content/config/config.md | 3 +- docs/content/instrumentation/jvm.md | 2 +- .../metrics/config/MetricsProperties.java | 34 +++------- .../metrics/config/PrometheusProperties.java | 9 +-- .../config/PrometheusPropertiesTest.java | 14 +---- .../jvm/JvmGarbageCollectorMetrics.java | 49 +++++---------- .../jvm/JvmGarbageCollectorMetricsTest.java | 63 +------------------ 7 files changed, 34 insertions(+), 140 deletions(-) diff --git a/docs/content/config/config.md b/docs/content/config/config.md index 810aeca57..f163cf3a1 100644 --- a/docs/content/config/config.md +++ b/docs/content/config/config.md @@ -89,7 +89,8 @@ When the same property is defined in multiple sources, the following precedence | io.prometheus.metrics.summary_quantile_errors | [Summary.Builder.quantile(double, double)]() | (5) | | io.prometheus.metrics.summary_max_age_seconds | [Summary.Builder.maxAgeSeconds()]() | | | io.prometheus.metrics.summary_number_of_age_buckets | [Summary.Builder.numberOfAgeBuckets()]() | | -| io.prometheus.metrics.otel_opt_in | [MetricsProperties.isOtelOptIn()]() | (2) | +| io.prometheus.metrics.use_otel_metrics | [MetricsProperties.useOtelMetrics()]() | (2) | + diff --git a/docs/content/instrumentation/jvm.md b/docs/content/instrumentation/jvm.md index 110df42f0..64c7d5b2c 100644 --- a/docs/content/instrumentation/jvm.md +++ b/docs/content/instrumentation/jvm.md @@ -123,7 +123,7 @@ jvm_gc_collection_seconds_count{gc="PS Scavenge"} 0 jvm_gc_collection_seconds_sum{gc="PS Scavenge"} 0.0 ``` -For more detailed GC metrics, enable the [useOtelMetrics](https://prometheus.github.io/client_java/config/config/#metrics-properties) configuration option. This replaces the standard metric with a +For more detailed GC metrics, enable the [use_otel_metrics](https://prometheus.github.io/client_java/config/config/#metrics-properties) configuration option. This replaces the standard metric with a histogram implemented according to the [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmgcduration). ```text diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java index fd8954cb5..4253d177a 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java @@ -27,6 +27,7 @@ public class MetricsProperties { private static final String SUMMARY_QUANTILE_ERRORS = "summary_quantile_errors"; private static final String SUMMARY_MAX_AGE_SECONDS = "summary_max_age_seconds"; private static final String SUMMARY_NUMBER_OF_AGE_BUCKETS = "summary_number_of_age_buckets"; + private static final String USE_OTEL_METRICS = "use_otel_metrics"; /** * All known property suffixes that can be configured for metrics. @@ -46,10 +47,9 @@ public class MetricsProperties { SUMMARY_QUANTILES, SUMMARY_QUANTILE_ERRORS, SUMMARY_MAX_AGE_SECONDS, - SUMMARY_NUMBER_OF_AGE_BUCKETS + SUMMARY_NUMBER_OF_AGE_BUCKETS, + USE_OTEL_METRICS }; - private static final String USE_OTEL_METRICS = "useOtelMetrics"; - private static final String OTEL_OPT_IN = "otelOptIn"; @Nullable private final Boolean exemplarsEnabled; @Nullable private final Boolean histogramNativeOnly; @@ -65,7 +65,6 @@ public class MetricsProperties { @Nullable private final Long summaryMaxAgeSeconds; @Nullable private final Integer summaryNumberOfAgeBuckets; @Nullable private final Boolean useOtelMetrics; - @Nullable private final Boolean otelOptIn; public MetricsProperties( @Nullable Boolean exemplarsEnabled, @@ -80,9 +79,7 @@ public MetricsProperties( @Nullable List summaryQuantiles, @Nullable List summaryQuantileErrors, @Nullable Long summaryMaxAgeSeconds, - @Nullable Integer summaryNumberOfAgeBuckets, - @Nullable Boolean useOtelMetrics, - Boolean otelOptIn) { + @Nullable Integer summaryNumberOfAgeBuckets) { this( exemplarsEnabled, histogramNativeOnly, @@ -97,8 +94,7 @@ public MetricsProperties( summaryQuantileErrors, summaryMaxAgeSeconds, summaryNumberOfAgeBuckets, - useOtelMetrics, - otelOptIn, + false, ""); } @@ -117,10 +113,8 @@ private MetricsProperties( @Nullable Long summaryMaxAgeSeconds, @Nullable Integer summaryNumberOfAgeBuckets, @Nullable Boolean useOtelMetrics, - @Nullable Boolean otelOptIn, String configPropertyPrefix) { this.exemplarsEnabled = exemplarsEnabled; - this.otelOptIn = otelOptIn; this.histogramNativeOnly = isHistogramNativeOnly(histogramClassicOnly, histogramNativeOnly); this.histogramClassicOnly = isHistogramClassicOnly(histogramClassicOnly, histogramNativeOnly); this.histogramClassicUpperBounds = @@ -375,12 +369,6 @@ public Boolean useOtelMetrics() { return useOtelMetrics; } - /** Where applicable, if using otel metrics, allow usage of opt-in labels */ - @Nullable - public Boolean isOtelOptIn() { - return otelOptIn; - } - /** * Note that this will remove entries from {@code propertySource}. This is because we want to know * if there are unused properties remaining after all properties have been loaded. @@ -401,8 +389,7 @@ static MetricsProperties load(String prefix, PropertySource propertySource) Util.loadDoubleList(prefix, SUMMARY_QUANTILE_ERRORS, propertySource), Util.loadLong(prefix, SUMMARY_MAX_AGE_SECONDS, propertySource), Util.loadInteger(prefix, SUMMARY_NUMBER_OF_AGE_BUCKETS, propertySource), - Util.loadBoolean(prefix + "." + USE_OTEL_METRICS, properties), - Util.loadBoolean(prefix + "." + OTEL_OPT_IN, properties), + Util.loadBoolean(prefix, USE_OTEL_METRICS, propertySource), prefix); } @@ -425,7 +412,6 @@ public static class Builder { @Nullable private Long summaryMaxAgeSeconds; @Nullable private Integer summaryNumberOfAgeBuckets; @Nullable private Boolean useOtelMetrics; - @Nullable private Boolean otelOptIn; private Builder() {} @@ -445,7 +431,7 @@ public MetricsProperties build() { summaryMaxAgeSeconds, summaryNumberOfAgeBuckets, useOtelMetrics, - otelOptIn); + ""); } /** See {@link MetricsProperties#getExemplarsEnabled()} */ @@ -535,11 +521,5 @@ public Builder useOtelMetrics(@Nullable Boolean useOtelMetrics) { this.useOtelMetrics = useOtelMetrics; return this; } - - /** See {@link MetricsProperties#isOtelOptIn()} */ - public Builder otelOptIn(@Nullable Boolean otelOptIn) { - this.otelOptIn = otelOptIn; - return this; - } } } diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java index da61df59d..2932d0225 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java @@ -168,14 +168,15 @@ public ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties() { return exporterOpenTelemetryProperties; } - public boolean useOtelMetrics(String prometheusMetric, String otelMetric) { - Boolean useByPrometheusMetric = usesOtelMetric(prometheusMetric); - if (Boolean.FALSE.equals(useByPrometheusMetric)) return false; + public boolean useOtelMetrics(String otelMetric) { Boolean useByOtelMetric = usesOtelMetric(otelMetric); - if (Boolean.FALSE.equals(useByOtelMetric)) return false; + if (useByOtelMetric != null) { + return useByOtelMetric; + } return Boolean.TRUE.equals(getDefaultMetricProperties().useOtelMetrics()); } + @Nullable private Boolean usesOtelMetric(String metric) { return Optional.ofNullable(getMetricProperties(metric)) .map(MetricsProperties::useOtelMetrics) diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java index 607b78839..a46456475 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java @@ -129,32 +129,24 @@ void testMetricNameStartingWithNumber() { assertThat(result.getMetricProperties("_23metric")).isSameAs(customProps); } - @Test - void useOtelMetricsSupportsNegativeOverride() { - Map metricMap = new HashMap<>(); - metricMap.put("prom_metric", otelProperties(false)); - PrometheusProperties props = buildProperties(true, metricMap); - assertThat(props.useOtelMetrics("prom_metric", "any_otel_metric")).isFalse(); - } - @Test void useOtelMetricsDisablesByMetricName() { Map metricMap = new HashMap<>(); metricMap.put("otel_metric", otelProperties(false)); PrometheusProperties props = buildProperties(true, metricMap); - assertThat(props.useOtelMetrics("some_prom_metric", "otel_metric")).isFalse(); + assertThat(props.useOtelMetrics("otel_metric")).isFalse(); } @Test void useOtelMetricsRespectsDefaultIfNoOverride() { PrometheusProperties props = buildProperties(true, Collections.emptyMap()); - assertThat(props.useOtelMetrics("prom_x", "otel_y")).isTrue(); + assertThat(props.useOtelMetrics("otel_y")).isTrue(); } @Test void noOverridesReturnsFalse() { PrometheusProperties props = PrometheusProperties.get(); - assertThat(props.useOtelMetrics("prom_x", "otel_y")).isFalse(); + assertThat(props.useOtelMetrics("otel_y")).isFalse(); } private static PrometheusProperties buildProperties( diff --git a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java index f8f41f611..b0a90a1b3 100644 --- a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java +++ b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java @@ -1,7 +1,6 @@ package io.prometheus.metrics.instrumentation.jvm; import com.sun.management.GarbageCollectionNotificationInfo; -import io.prometheus.metrics.config.MetricsProperties; import io.prometheus.metrics.config.PrometheusProperties; import io.prometheus.metrics.core.metrics.Histogram; import io.prometheus.metrics.core.metrics.SummaryWithCallback; @@ -11,9 +10,7 @@ import io.prometheus.metrics.model.snapshots.Unit; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; -import java.util.ArrayList; import java.util.List; -import java.util.Optional; import javax.annotation.Nullable; import javax.management.NotificationEmitter; import javax.management.openmbean.CompositeData; @@ -26,14 +23,14 @@ * JvmMetrics.builder().register(); * } * - * However, if you want only the {@link JvmGarbageCollectorMetrics} you can also register them + *

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.
@@ -63,14 +60,14 @@ private JvmGarbageCollectorMetrics(
   }
 
   private void register(PrometheusRegistry registry) {
-    if (config.useOtelMetrics(JVM_GC_COLLECTION_SECONDS, JVM_GC_DURATION)) {
-      registerGCDurationHistogram(registry);
+    if (config.useOtelMetrics(JVM_GC_DURATION)) {
+      registerOtel(registry);
     } else {
-      registerGCDurationSummary(registry);
+      registerPrometheus(registry);
     }
   }
 
-  private void registerGCDurationSummary(PrometheusRegistry 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.")
@@ -90,31 +87,22 @@ private void registerGCDurationSummary(PrometheusRegistry registry) {
         .register(registry);
   }
 
-  private void registerGCDurationHistogram(PrometheusRegistry registry) {
+  private void registerOtel(PrometheusRegistry registry) {
     double[] buckets = {0.01, 0.1, 1, 10};
 
-    List labels = new ArrayList<>(List.of("jvm.gc.action", "jvm.gc.name"));
-    boolean otelOptIn =
-        Optional.ofNullable(config.getMetricProperties(JVM_GC_DURATION))
-            .map(MetricsProperties::isOtelOptIn)
-            .orElse(false);
-    if (otelOptIn) {
-      labels.add("jvm.gc.cause");
-    }
-
     Histogram gcDurationHistogram =
         Histogram.builder(config)
             .name(JVM_GC_DURATION)
             .unit(Unit.SECONDS)
             .help("Duration of JVM garbage collection actions.")
-            .labelNames(labels.toArray(String[]::new))
+            .labelNames("jvm.gc.action", "jvm.gc.name", "jvm.gc.cause")
             .classicUpperBounds(buckets)
             .register(registry);
 
-    registerNotificationListener(gcDurationHistogram, otelOptIn);
+    registerNotificationListener(gcDurationHistogram);
   }
 
-  private void registerNotificationListener(Histogram gcDurationHistogram, boolean otelOptIn) {
+  private void registerNotificationListener(Histogram gcDurationHistogram) {
     for (GarbageCollectorMXBean gcBean : garbageCollectorBeans) {
 
       if (!(gcBean instanceof NotificationEmitter)) {
@@ -133,25 +121,18 @@ private void registerNotificationListener(Histogram gcDurationHistogram, boolean
                     GarbageCollectionNotificationInfo.from(
                         (CompositeData) notification.getUserData());
 
-                observe(gcDurationHistogram, otelOptIn, info);
+                observe(gcDurationHistogram, info);
               },
               null,
               null);
     }
   }
 
-  private void observe(
-      Histogram gcDurationHistogram, boolean otelOptIn, GarbageCollectionNotificationInfo info) {
+  private void observe(Histogram gcDurationHistogram, GarbageCollectionNotificationInfo info) {
     double observedDuration = Unit.millisToSeconds(info.getGcInfo().getDuration());
-    if (otelOptIn) {
-      gcDurationHistogram
-          .labelValues(info.getGcAction(), info.getGcName(), info.getGcCause())
-          .observe(observedDuration);
-    } else {
-      gcDurationHistogram
-          .labelValues(info.getGcAction(), info.getGcName())
-          .observe(observedDuration);
-    }
+    gcDurationHistogram
+        .labelValues(info.getGcAction(), info.getGcName(), info.getGcCause())
+        .observe(observedDuration);
   }
 
   public static Builder builder() {
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 68b11c127..ad6e95ff8 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
@@ -27,8 +27,6 @@
 import javax.management.openmbean.*;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.ArgumentCaptor;
 
 class JvmGarbageCollectorMetricsTest {
@@ -116,8 +114,7 @@ public void testGCDurationHistogramLabels() throws Exception {
     PrometheusProperties properties =
         PrometheusProperties.builder()
             .defaultMetricsProperties(MetricsProperties.builder().useOtelMetrics(true).build())
-            .metricProperties(
-                Map.of("jvm_gc_duration", MetricsProperties.builder().otelOptIn(true).build()))
+            .metricProperties(Map.of("jvm_gc_duration", MetricsProperties.builder().build()))
             .build();
 
     PrometheusRegistry registry = new PrometheusRegistry();
@@ -158,64 +155,6 @@ public void testGCDurationHistogramLabels() throws Exception {
     assertThat(metrics).contains(expected);
   }
 
-  @SuppressWarnings("rawtypes")
-  @ParameterizedTest
-  @ValueSource(booleans = {true, false})
-  public void testGCDurationHistogramLabelsWithNoOptIn(boolean nullOptIn) throws Exception {
-    GarbageCollectorMXBean mockGcBean =
-        mock(
-            GarbageCollectorMXBean.class,
-            withSettings().extraInterfaces(NotificationEmitter.class));
-    when(mockGcBean.getName()).thenReturn("MyGC");
-
-    PrometheusProperties.Builder builder =
-        PrometheusProperties.builder()
-            .defaultMetricsProperties(MetricsProperties.builder().useOtelMetrics(true).build());
-    if (!nullOptIn) {
-      builder =
-          builder.metricProperties(
-              Map.of("jvm.gc.duration", MetricsProperties.builder().otelOptIn(true).build()));
-    }
-    PrometheusProperties properties = builder.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.name"="MyGC",le="0.01"} 0
-  {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC",le="0.1"} 1
-  {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC",le="1.0"} 1
-  {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC",le="10.0"} 1
-  {"jvm.gc.duration_seconds_bucket","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC",le="+Inf"} 1
-  {"jvm.gc.duration_seconds_count","jvm.gc.action"="end of minor GC","jvm.gc.name"="MyGC"} 1
-  {"jvm.gc.duration_seconds_sum","jvm.gc.action"="end of minor GC","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);

From 447d0a33539841807580349ed2b3c32d5cf28fd7 Mon Sep 17 00:00:00 2001
From: Gregor Zeitlinger 
Date: Fri, 6 Feb 2026 18:16:52 +0100
Subject: [PATCH 11/12] rename use_otel_metrics to use_otel_semconv and change
 from boolean to list

Replace the boolean property with a comma-separated list of OTel metric
names, giving users fine-grained control over which metrics use
OpenTelemetry Semantic Conventions. Use "*" to enable all metrics.

Signed-off-by: Gregor Zeitlinger 
---
 docs/content/config/config.md                 |  9 ++---
 docs/content/instrumentation/jvm.md           |  8 +++--
 .../metrics/config/MetricsProperties.java     | 34 +++++++++---------
 .../metrics/config/PrometheusProperties.java  | 22 +++++-------
 .../io/prometheus/metrics/config/Util.java    |  8 +++++
 .../config/PrometheusPropertiesTest.java      | 36 +++++++++----------
 .../jvm/JvmGarbageCollectorMetrics.java       |  2 +-
 .../jvm/JvmGarbageCollectorMetricsTest.java   |  5 ++-
 8 files changed, 66 insertions(+), 58 deletions(-)

diff --git a/docs/content/config/config.md b/docs/content/config/config.md
index f163cf3a1..e4903d889 100644
--- a/docs/content/config/config.md
+++ b/docs/content/config/config.md
@@ -75,7 +75,7 @@ When the same property is defined in multiple sources, the following precedence
 
 
 | Name                                                          | Javadoc                                                                                                                                                                         | Note    |
-|---------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
+| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
 | io.prometheus.metrics.exemplars_enabled                       | [Counter.Builder.withExemplars()]()                                                   | (1) (2) |
 | io.prometheus.metrics.histogram_native_only                   | [Histogram.Builder.nativeOnly()]()                                                     | (2)     |
 | io.prometheus.metrics.histogram_classic_only                  | [Histogram.Builder.classicOnly()]()                                                   | (2)     |
@@ -89,8 +89,7 @@ When the same property is defined in multiple sources, the following precedence
 | io.prometheus.metrics.summary_quantile_errors                 | [Summary.Builder.quantile(double, double)]()                                  | (5)     |
 | io.prometheus.metrics.summary_max_age_seconds                 | [Summary.Builder.maxAgeSeconds()]()                                               |         |
 | io.prometheus.metrics.summary_number_of_age_buckets           | [Summary.Builder.numberOfAgeBuckets()]()                                      |         |
-| io.prometheus.metrics.use_otel_metrics                        | [MetricsProperties.useOtelMetrics()]()                                                   | (2)     |
-
+| io.prometheus.metrics.use_otel_semconv                        | [MetricsProperties.useOtelSemconv()]()                                                   | (6)     |
 
 
 
@@ -102,7 +101,9 @@ not just for counters
(3) Comma-separated list. Example: `.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10`.
(4) Comma-separated list. Example: `0.5, 0.95, 0.99`.
(5) Comma-separated list. If specified, the list must have the same length as -`io.prometheus.metrics.summary_quantiles`. Example: `0.01, 0.005, 0.005`. +`io.prometheus.metrics.summary_quantiles`. Example: `0.01, 0.005, 0.005`.
+(6) Comma-separated list of OTel metric names. Use `*` to enable all. +Example: `jvm.gc.duration` or `*`. There's one special feature about metric properties: You can set a property for one specific metric only by specifying the metric name. Example: diff --git a/docs/content/instrumentation/jvm.md b/docs/content/instrumentation/jvm.md index 64c7d5b2c..5de35eaf8 100644 --- a/docs/content/instrumentation/jvm.md +++ b/docs/content/instrumentation/jvm.md @@ -123,8 +123,12 @@ jvm_gc_collection_seconds_count{gc="PS Scavenge"} 0 jvm_gc_collection_seconds_sum{gc="PS Scavenge"} 0.0 ``` -For more detailed GC metrics, enable the [use_otel_metrics](https://prometheus.github.io/client_java/config/config/#metrics-properties) configuration option. This replaces the standard metric with a -histogram implemented according to the [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmgcduration). +For more detailed GC metrics, enable the +[use_otel_semconv](https://prometheus.github.io/client_java/config/config/#metrics-properties) +configuration option by specifying `jvm.gc.duration` or `*` (for all +OTel metrics). This replaces the standard metric with a histogram +implemented according to the +[OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmgcduration). ```text # HELP jvm_gc_duration_seconds Duration of JVM garbage collection actions. diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java index 4253d177a..55a425b14 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java @@ -27,7 +27,7 @@ public class MetricsProperties { private static final String SUMMARY_QUANTILE_ERRORS = "summary_quantile_errors"; private static final String SUMMARY_MAX_AGE_SECONDS = "summary_max_age_seconds"; private static final String SUMMARY_NUMBER_OF_AGE_BUCKETS = "summary_number_of_age_buckets"; - private static final String USE_OTEL_METRICS = "use_otel_metrics"; + private static final String USE_OTEL_SEMCONV = "use_otel_semconv"; /** * All known property suffixes that can be configured for metrics. @@ -48,7 +48,7 @@ public class MetricsProperties { SUMMARY_QUANTILE_ERRORS, SUMMARY_MAX_AGE_SECONDS, SUMMARY_NUMBER_OF_AGE_BUCKETS, - USE_OTEL_METRICS + USE_OTEL_SEMCONV }; @Nullable private final Boolean exemplarsEnabled; @@ -64,7 +64,7 @@ public class MetricsProperties { @Nullable private final List summaryQuantileErrors; @Nullable private final Long summaryMaxAgeSeconds; @Nullable private final Integer summaryNumberOfAgeBuckets; - @Nullable private final Boolean useOtelMetrics; + @Nullable private final List useOtelSemconv; public MetricsProperties( @Nullable Boolean exemplarsEnabled, @@ -94,7 +94,7 @@ public MetricsProperties( summaryQuantileErrors, summaryMaxAgeSeconds, summaryNumberOfAgeBuckets, - false, + null, ""); } @@ -112,7 +112,7 @@ private MetricsProperties( @Nullable List summaryQuantileErrors, @Nullable Long summaryMaxAgeSeconds, @Nullable Integer summaryNumberOfAgeBuckets, - @Nullable Boolean useOtelMetrics, + @Nullable List useOtelSemconv, String configPropertyPrefix) { this.exemplarsEnabled = exemplarsEnabled; this.histogramNativeOnly = isHistogramNativeOnly(histogramClassicOnly, histogramNativeOnly); @@ -134,7 +134,8 @@ private MetricsProperties( : unmodifiableList(new ArrayList<>(summaryQuantileErrors)); this.summaryMaxAgeSeconds = summaryMaxAgeSeconds; this.summaryNumberOfAgeBuckets = summaryNumberOfAgeBuckets; - this.useOtelMetrics = useOtelMetrics; + this.useOtelSemconv = + useOtelSemconv == null ? null : unmodifiableList(new ArrayList<>(useOtelSemconv)); validate(configPropertyPrefix); } @@ -360,13 +361,12 @@ public Integer getSummaryNumberOfAgeBuckets() { } /** - * Where applicable, metrics are registered in accordance with OpenTelemetry Semantic Conventions. - * Implementation should respect opt-in requirements and ensure no data duplication occurs with - * existing Prometheus metrics. + * List of OTel metric names for which OpenTelemetry Semantic Conventions should be used. Use + * {@code "*"} to enable for all metrics. Returns {@code null} if not configured. */ @Nullable - public Boolean useOtelMetrics() { - return useOtelMetrics; + public List useOtelSemconv() { + return useOtelSemconv; } /** @@ -389,7 +389,7 @@ static MetricsProperties load(String prefix, PropertySource propertySource) Util.loadDoubleList(prefix, SUMMARY_QUANTILE_ERRORS, propertySource), Util.loadLong(prefix, SUMMARY_MAX_AGE_SECONDS, propertySource), Util.loadInteger(prefix, SUMMARY_NUMBER_OF_AGE_BUCKETS, propertySource), - Util.loadBoolean(prefix, USE_OTEL_METRICS, propertySource), + Util.loadStringList(prefix, USE_OTEL_SEMCONV, propertySource), prefix); } @@ -411,7 +411,7 @@ public static class Builder { @Nullable private List summaryQuantileErrors; @Nullable private Long summaryMaxAgeSeconds; @Nullable private Integer summaryNumberOfAgeBuckets; - @Nullable private Boolean useOtelMetrics; + @Nullable private List useOtelSemconv; private Builder() {} @@ -430,7 +430,7 @@ public MetricsProperties build() { summaryQuantileErrors, summaryMaxAgeSeconds, summaryNumberOfAgeBuckets, - useOtelMetrics, + useOtelSemconv, ""); } @@ -516,9 +516,9 @@ public Builder summaryNumberOfAgeBuckets(@Nullable Integer summaryNumberOfAgeBuc return this; } - /** See {@link MetricsProperties#useOtelMetrics()} */ - public Builder useOtelMetrics(@Nullable Boolean useOtelMetrics) { - this.useOtelMetrics = useOtelMetrics; + /** See {@link MetricsProperties#useOtelSemconv()} */ + public Builder useOtelSemconv(String... useOtelSemconv) { + this.useOtelSemconv = Util.toStringList(useOtelSemconv); return this; } } diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java index 2932d0225..5f4f2e3b7 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java @@ -1,8 +1,8 @@ package io.prometheus.metrics.config; import java.util.HashMap; +import java.util.List; import java.util.Map; -import java.util.Optional; import javax.annotation.Nullable; /** @@ -168,19 +168,15 @@ public ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties() { return exporterOpenTelemetryProperties; } - public boolean useOtelMetrics(String otelMetric) { - Boolean useByOtelMetric = usesOtelMetric(otelMetric); - if (useByOtelMetric != null) { - return useByOtelMetric; + public boolean useOtelSemconv(String otelMetric) { + List list = getDefaultMetricProperties().useOtelSemconv(); + if (list == null || list.isEmpty()) { + return false; } - return Boolean.TRUE.equals(getDefaultMetricProperties().useOtelMetrics()); - } - - @Nullable - private Boolean usesOtelMetric(String metric) { - return Optional.ofNullable(getMetricProperties(metric)) - .map(MetricsProperties::useOtelMetrics) - .orElse(null); + if (list.contains("*")) { + return true; + } + return list.contains(otelMetric); } public static class Builder { diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java index 20bd75699..52a4aa017 100644 --- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java +++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java @@ -31,6 +31,14 @@ static Boolean loadBoolean(String prefix, String propertyName, PropertySource pr return null; } + @Nullable + static List toStringList(@Nullable String... values) { + if (values == null) { + return null; + } + return Arrays.asList(values); + } + @Nullable static List toList(@Nullable double... values) { if (values == null) { diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java index a46456475..ee4d7cb99 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java @@ -130,34 +130,34 @@ void testMetricNameStartingWithNumber() { } @Test - void useOtelMetricsDisablesByMetricName() { - Map metricMap = new HashMap<>(); - metricMap.put("otel_metric", otelProperties(false)); - PrometheusProperties props = buildProperties(true, metricMap); - assertThat(props.useOtelMetrics("otel_metric")).isFalse(); + void useOtelSemconvReturnsFalseForMetricNotInList() { + PrometheusProperties props = buildProperties("jvm.gc.duration"); + assertThat(props.useOtelSemconv("other.metric")).isFalse(); } @Test - void useOtelMetricsRespectsDefaultIfNoOverride() { - PrometheusProperties props = buildProperties(true, Collections.emptyMap()); - assertThat(props.useOtelMetrics("otel_y")).isTrue(); + void useOtelSemconvWildcardEnablesAll() { + PrometheusProperties props = buildProperties("*"); + assertThat(props.useOtelSemconv("any.metric")).isTrue(); } @Test - void noOverridesReturnsFalse() { + void useOtelSemconvNullListReturnsFalse() { PrometheusProperties props = PrometheusProperties.get(); - assertThat(props.useOtelMetrics("otel_y")).isFalse(); + assertThat(props.useOtelSemconv("otel_y")).isFalse(); } - private static PrometheusProperties buildProperties( - Boolean defaultUse, Map metricProps) { - return PrometheusProperties.builder() - .defaultMetricsProperties(otelProperties(defaultUse)) - .metricProperties(new HashMap<>(metricProps)) - .build(); + @Test + void useOtelSemconvSpecificMetricReturnsTrueForMatch() { + PrometheusProperties props = buildProperties("jvm.gc.duration", "jvm.memory.used"); + assertThat(props.useOtelSemconv("jvm.gc.duration")).isTrue(); + assertThat(props.useOtelSemconv("jvm.memory.used")).isTrue(); + assertThat(props.useOtelSemconv("other.metric")).isFalse(); } - private static MetricsProperties otelProperties(Boolean useOtel) { - return MetricsProperties.builder().useOtelMetrics(useOtel).build(); + private static PrometheusProperties buildProperties(String... otelSemconv) { + return PrometheusProperties.builder() + .defaultMetricsProperties(MetricsProperties.builder().useOtelSemconv(otelSemconv).build()) + .build(); } } diff --git a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java index b0a90a1b3..8b824bca3 100644 --- a/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java +++ b/prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java @@ -60,7 +60,7 @@ private JvmGarbageCollectorMetrics( } private void register(PrometheusRegistry registry) { - if (config.useOtelMetrics(JVM_GC_DURATION)) { + if (config.useOtelSemconv(JVM_GC_DURATION)) { registerOtel(registry); } else { registerPrometheus(registry); 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 ad6e95ff8..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 @@ -91,7 +91,7 @@ public void testNonOtelMetricsAbsentWhenUseOtelEnabled() { PrometheusRegistry registry = new PrometheusRegistry(); PrometheusProperties properties = PrometheusProperties.builder() - .defaultMetricsProperties(MetricsProperties.builder().useOtelMetrics(true).build()) + .defaultMetricsProperties(MetricsProperties.builder().useOtelSemconv("*").build()) .build(); JvmGarbageCollectorMetrics.builder(properties) .garbageCollectorBeans(Arrays.asList(mockGcBean1, mockGcBean2)) @@ -113,8 +113,7 @@ public void testGCDurationHistogramLabels() throws Exception { PrometheusProperties properties = PrometheusProperties.builder() - .defaultMetricsProperties(MetricsProperties.builder().useOtelMetrics(true).build()) - .metricProperties(Map.of("jvm_gc_duration", MetricsProperties.builder().build())) + .defaultMetricsProperties(MetricsProperties.builder().useOtelSemconv("*").build()) .build(); PrometheusRegistry registry = new PrometheusRegistry(); From 20cc588a38b10320b695b447023ed36ce78412f8 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 6 Feb 2026 18:29:30 +0100 Subject: [PATCH 12/12] lint Signed-off-by: Gregor Zeitlinger --- docs/content/instrumentation/jvm.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/content/instrumentation/jvm.md b/docs/content/instrumentation/jvm.md index 5de35eaf8..0855b3fe7 100644 --- a/docs/content/instrumentation/jvm.md +++ b/docs/content/instrumentation/jvm.md @@ -130,6 +130,8 @@ OTel metrics). This replaces the standard metric with a histogram implemented according to the [OpenTelemetry Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/#metric-jvmgcduration). + + ```text # HELP jvm_gc_duration_seconds Duration of JVM garbage collection actions. # TYPE jvm_gc_duration_seconds histogram @@ -142,6 +144,8 @@ jvm_gc_duration_seconds_count{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Yo jvm_gc_duration_seconds_sum{jvm_gc_action="end of minor GC",jvm_gc_name="G1 Young Generation"} 0.029 ``` + + ## JVM Memory Metrics JVM memory metrics are provided by