Skip to content

Commit ef939eb

Browse files
committed
example
Signed-off-by: Jay DeLuca <[email protected]>
1 parent 20cc588 commit ef939eb

2 files changed

Lines changed: 184 additions & 32 deletions

File tree

prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.java

Lines changed: 122 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
import io.prometheus.metrics.model.snapshots.Unit;
1111
import java.lang.management.GarbageCollectorMXBean;
1212
import java.lang.management.ManagementFactory;
13+
import java.util.ArrayList;
1314
import java.util.List;
15+
import java.util.logging.Level;
16+
import java.util.logging.Logger;
1417
import javax.annotation.Nullable;
18+
import javax.management.ListenerNotFoundException;
1519
import javax.management.NotificationEmitter;
20+
import javax.management.NotificationListener;
1621
import javax.management.openmbean.CompositeData;
1722

1823
/**
@@ -40,15 +45,29 @@
4045
* jvm_gc_collection_seconds_count{gc="PS Scavenge"} 0
4146
* jvm_gc_collection_seconds_sum{gc="PS Scavenge"} 0.0
4247
* </pre>
48+
*
49+
* <p><b>Note on resource cleanup:</b> When using OpenTelemetry semantic conventions (via {@code
50+
* use_otel_semconv} configuration), this class registers JMX notification listeners that should be
51+
* cleaned up when the metrics are no longer needed. To ensure proper cleanup, keep a reference to
52+
* the {@link JvmGarbageCollectorMetrics} instance and call {@link #close()} when done:
53+
*
54+
* <pre>{@code
55+
* JvmGarbageCollectorMetrics gcMetrics = new JvmGarbageCollectorMetrics(...);
56+
* gcMetrics.register(registry);
57+
* // ... later, when shutting down:
58+
* gcMetrics.close();
59+
* }</pre>
4360
*/
44-
public class JvmGarbageCollectorMetrics {
61+
public class JvmGarbageCollectorMetrics implements AutoCloseable {
62+
private static final Logger logger = Logger.getLogger(JvmGarbageCollectorMetrics.class.getName());
4563

4664
private static final String JVM_GC_COLLECTION_SECONDS = "jvm_gc_collection_seconds";
4765
private static final String JVM_GC_DURATION = "jvm.gc.duration";
4866

4967
private final PrometheusProperties config;
5068
private final List<GarbageCollectorMXBean> garbageCollectorBeans;
5169
private final Labels constLabels;
70+
private final List<ListenerRegistration> listenerRegistrations = new ArrayList<>();
5271

5372
private JvmGarbageCollectorMetrics(
5473
List<GarbageCollectorMXBean> garbageCollectorBeans,
@@ -109,22 +128,33 @@ private void registerNotificationListener(Histogram gcDurationHistogram) {
109128
continue;
110129
}
111130

112-
((NotificationEmitter) gcBean)
113-
.addNotificationListener(
114-
(notification, handback) -> {
115-
if (!GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION.equals(
116-
notification.getType())) {
117-
return;
118-
}
119-
120-
GarbageCollectionNotificationInfo info =
121-
GarbageCollectionNotificationInfo.from(
122-
(CompositeData) notification.getUserData());
123-
124-
observe(gcDurationHistogram, info);
125-
},
126-
null,
127-
null);
131+
NotificationEmitter notificationEmitter = (NotificationEmitter) gcBean;
132+
133+
// Create a named listener instance so we can remove it later
134+
NotificationListener listener =
135+
(notification, handback) -> {
136+
try {
137+
if (!GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION.equals(
138+
notification.getType())) {
139+
return;
140+
}
141+
142+
GarbageCollectionNotificationInfo info =
143+
GarbageCollectionNotificationInfo.from(
144+
(CompositeData) notification.getUserData());
145+
146+
observe(gcDurationHistogram, info);
147+
} catch (Exception e) {
148+
// Must not propagate exceptions - would cause JVM to remove listener permanently
149+
logger.log(
150+
Level.WARNING, "Exception while processing garbage collection notification", e);
151+
}
152+
};
153+
154+
notificationEmitter.addNotificationListener(listener, null, null);
155+
156+
// Store registration info for cleanup
157+
listenerRegistrations.add(new ListenerRegistration(notificationEmitter, listener));
128158
}
129159
}
130160

@@ -135,6 +165,47 @@ private void observe(Histogram gcDurationHistogram, GarbageCollectionNotificatio
135165
.observe(observedDuration);
136166
}
137167

168+
/**
169+
* Removes all JMX notification listeners registered by this instance.
170+
*
171+
* <p>This method should be called when the metrics are no longer needed to prevent memory leaks.
172+
* It is safe to call this method multiple times.
173+
*
174+
* <p><b>Note:</b> This only affects metrics registered with OpenTelemetry semantic conventions
175+
* (when {@code use_otel_semconv} is enabled). The callback-based Prometheus metrics do not
176+
* require cleanup.
177+
*/
178+
@Override
179+
public void close() {
180+
for (ListenerRegistration registration : listenerRegistrations) {
181+
try {
182+
registration.notificationEmitter.removeNotificationListener(registration.listener);
183+
} catch (ListenerNotFoundException e) {
184+
// Listener was already removed or never added - ignore
185+
logger.log(Level.FINE, "Listener not found during cleanup", e);
186+
} catch (Exception e) {
187+
// Log but continue removing other listeners
188+
logger.log(Level.WARNING, "Error removing GC notification listener", e);
189+
}
190+
}
191+
listenerRegistrations.clear();
192+
}
193+
194+
/**
195+
* Holds registration information for a notification listener so it can be removed later.
196+
*
197+
* <p>Package-private for testing.
198+
*/
199+
static class ListenerRegistration {
200+
final NotificationEmitter notificationEmitter;
201+
final NotificationListener listener;
202+
203+
ListenerRegistration(NotificationEmitter notificationEmitter, NotificationListener listener) {
204+
this.notificationEmitter = notificationEmitter;
205+
this.listener = listener;
206+
}
207+
}
208+
138209
public static Builder builder() {
139210
return new Builder(PrometheusProperties.get());
140211
}
@@ -164,16 +235,46 @@ Builder garbageCollectorBeans(List<GarbageCollectorMXBean> garbageCollectorBeans
164235
return this;
165236
}
166237

167-
public void register() {
168-
register(PrometheusRegistry.defaultRegistry);
238+
/**
239+
* Register GC metrics with the default registry.
240+
*
241+
* <p><b>Important:</b> When using OpenTelemetry semantic conventions, this method returns a
242+
* {@link JvmGarbageCollectorMetrics} instance that implements {@link AutoCloseable}. Keep a
243+
* reference and call {@link JvmGarbageCollectorMetrics#close()} when shutting down to prevent
244+
* memory leaks:
245+
*
246+
* <pre>{@code
247+
* JvmGarbageCollectorMetrics gcMetrics = JvmGarbageCollectorMetrics.builder().register();
248+
* // ... later during shutdown:
249+
* gcMetrics.close();
250+
* }</pre>
251+
*
252+
* @return the registered metrics instance, which should be closed when no longer needed
253+
*/
254+
public JvmGarbageCollectorMetrics register() {
255+
return register(PrometheusRegistry.defaultRegistry);
169256
}
170257

171-
public void register(PrometheusRegistry registry) {
258+
/**
259+
* Register GC metrics with the specified registry.
260+
*
261+
* <p><b>Important:</b> When using OpenTelemetry semantic conventions, this method returns a
262+
* {@link JvmGarbageCollectorMetrics} instance that implements {@link AutoCloseable}. Keep a
263+
* reference and call {@link JvmGarbageCollectorMetrics#close()} when shutting down to prevent
264+
* memory leaks.
265+
*
266+
* @param registry the registry to register metrics with
267+
* @return the registered metrics instance, which should be closed when no longer needed
268+
*/
269+
public JvmGarbageCollectorMetrics register(PrometheusRegistry registry) {
172270
List<GarbageCollectorMXBean> garbageCollectorBeans = this.garbageCollectorBeans;
173271
if (garbageCollectorBeans == null) {
174272
garbageCollectorBeans = ManagementFactory.getGarbageCollectorMXBeans();
175273
}
176-
new JvmGarbageCollectorMetrics(garbageCollectorBeans, config, constLabels).register(registry);
274+
JvmGarbageCollectorMetrics metrics =
275+
new JvmGarbageCollectorMetrics(garbageCollectorBeans, config, constLabels);
276+
metrics.register(registry);
277+
return metrics;
177278
}
178279
}
179280
}

prometheus-metrics-instrumentation-jvm/src/main/java/io/prometheus/metrics/instrumentation/jvm/JvmMetrics.java

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,57 @@
33
import io.prometheus.metrics.config.PrometheusProperties;
44
import io.prometheus.metrics.model.registry.PrometheusRegistry;
55
import io.prometheus.metrics.model.snapshots.Labels;
6+
import java.util.ArrayList;
7+
import java.util.List;
68
import java.util.Set;
79
import java.util.concurrent.ConcurrentHashMap;
810

911
/**
1012
* Registers all JVM metrics. Example usage:
1113
*
1214
* <pre>{@code
13-
* JvmMetrics.builder().register();
15+
* JvmMetrics jvmMetrics = JvmMetrics.builder().register();
16+
* // ... later, during shutdown:
17+
* jvmMetrics.close();
1418
* }</pre>
19+
*
20+
* <p><b>Note on resource cleanup:</b> When using OpenTelemetry semantic conventions for GC metrics
21+
* (via {@code use_otel_semconv} configuration), JMX notification listeners are registered that
22+
* should be cleaned up when shutting down. Call {@link #close()} to remove these listeners and
23+
* prevent memory leaks.
1524
*/
16-
public class JvmMetrics {
25+
public class JvmMetrics implements AutoCloseable {
1726

1827
private static final Set<PrometheusRegistry> REGISTERED = ConcurrentHashMap.newKeySet();
1928

29+
private final List<AutoCloseable> closeables = new ArrayList<>();
30+
2031
public static Builder builder() {
2132
return new Builder(PrometheusProperties.get());
2233
}
2334

24-
// Note: Currently there is no configuration for JVM metrics, so it doesn't matter whether you
25-
// pass a config or not.
26-
// However, we will add config options in the future, like whether you want to use Prometheus
27-
// naming conventions
28-
// or OpenTelemetry semantic conventions for JVM metrics.
2935
public static Builder builder(PrometheusProperties config) {
3036
return new Builder(config);
3137
}
3238

39+
/**
40+
* Closes all registered metrics that require cleanup.
41+
*
42+
* <p>This removes JMX notification listeners registered by GC metrics and allocation metrics when
43+
* using OpenTelemetry semantic conventions. It is safe to call this method multiple times.
44+
*/
45+
@Override
46+
public void close() {
47+
for (AutoCloseable closeable : closeables) {
48+
try {
49+
closeable.close();
50+
} catch (Exception e) {
51+
// Continue closing other resources even if one fails
52+
}
53+
}
54+
closeables.clear();
55+
}
56+
3357
public static class Builder {
3458

3559
private final PrometheusProperties config;
@@ -50,30 +74,57 @@ public Builder constLabels(Labels constLabels) {
5074
*
5175
* <p>It's safe to call this multiple times, only the first call will register the metrics, all
5276
* subsequent calls will be ignored.
77+
*
78+
* <p><b>Important:</b> Keep a reference to the returned {@link JvmMetrics} instance and call
79+
* {@link JvmMetrics#close()} during shutdown to clean up JMX notification listeners:
80+
*
81+
* <pre>{@code
82+
* JvmMetrics jvmMetrics = JvmMetrics.builder().register();
83+
* // ... later during shutdown:
84+
* jvmMetrics.close();
85+
* }</pre>
86+
*
87+
* @return the JvmMetrics instance, which should be closed when no longer needed
5388
*/
54-
public void register() {
55-
register(PrometheusRegistry.defaultRegistry);
89+
public JvmMetrics register() {
90+
return register(PrometheusRegistry.defaultRegistry);
5691
}
5792

5893
/**
5994
* Register all JVM metrics with the {@code registry}.
6095
*
6196
* <p>It's safe to call this multiple times, only the first call will register the metrics, all
6297
* subsequent calls will be ignored.
98+
*
99+
* <p><b>Important:</b> Keep a reference to the returned {@link JvmMetrics} instance and call
100+
* {@link JvmMetrics#close()} during shutdown to clean up JMX notification listeners.
101+
*
102+
* @param registry the registry to register metrics with
103+
* @return the JvmMetrics instance, which should be closed when no longer needed
63104
*/
64-
public void register(PrometheusRegistry registry) {
105+
public JvmMetrics register(PrometheusRegistry registry) {
106+
JvmMetrics jvmMetrics = new JvmMetrics();
65107
if (REGISTERED.add(registry)) {
66108
JvmThreadsMetrics.builder(config).constLabels(constLabels).register(registry);
67109
JvmBufferPoolMetrics.builder(config).constLabels(constLabels).register(registry);
68110
JvmClassLoadingMetrics.builder(config).constLabels(constLabels).register(registry);
69111
JvmCompilationMetrics.builder(config).constLabels(constLabels).register(registry);
70-
JvmGarbageCollectorMetrics.builder(config).constLabels(constLabels).register(registry);
112+
113+
// Store closeable metrics for cleanup
114+
JvmGarbageCollectorMetrics gcMetrics =
115+
JvmGarbageCollectorMetrics.builder(config).constLabels(constLabels).register(registry);
116+
jvmMetrics.closeables.add(gcMetrics);
117+
118+
// Note: JvmMemoryPoolAllocationMetrics also uses notification listeners but doesn't
119+
// currently implement cleanup. This should be fixed in a future update.
71120
JvmMemoryPoolAllocationMetrics.builder(config).constLabels(constLabels).register(registry);
121+
72122
JvmMemoryMetrics.builder(config).constLabels(constLabels).register(registry);
73123
JvmNativeMemoryMetrics.builder(config).constLabels(constLabels).register(registry);
74124
JvmRuntimeInfoMetric.builder(config).constLabels(constLabels).register(registry);
75125
ProcessMetrics.builder(config).constLabels(constLabels).register(registry);
76126
}
127+
return jvmMetrics;
77128
}
78129
}
79130
}

0 commit comments

Comments
 (0)