001package io.prometheus.metrics.instrumentation.jvm;
002
003import com.sun.management.GarbageCollectionNotificationInfo;
004import com.sun.management.GcInfo;
005import io.prometheus.metrics.config.PrometheusProperties;
006import io.prometheus.metrics.core.metrics.Counter;
007import io.prometheus.metrics.model.registry.PrometheusRegistry;
008import io.prometheus.metrics.model.snapshots.Labels;
009import java.lang.management.GarbageCollectorMXBean;
010import java.lang.management.ManagementFactory;
011import java.lang.management.MemoryUsage;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Map;
015import java.util.Objects;
016import javax.annotation.Nullable;
017import javax.management.Notification;
018import javax.management.NotificationEmitter;
019import javax.management.NotificationListener;
020import javax.management.openmbean.CompositeData;
021
022/**
023 * JVM memory allocation metrics. The {@link JvmMemoryPoolAllocationMetrics} are registered as part
024 * of the {@link JvmMetrics} like this:
025 *
026 * <pre>{@code
027 * JvmMetrics.builder().register();
028 * }</pre>
029 *
030 * However, if you want only the {@link JvmMemoryPoolAllocationMetrics} you can also register them
031 * directly:
032 *
033 * <pre>{@code
034 * JvmMemoryAllocationMetrics.builder().register();
035 * }</pre>
036 *
037 * Example metrics being exported:
038 *
039 * <pre>
040 * # HELP jvm_memory_pool_allocated_bytes_total Total bytes allocated in a given JVM memory pool. Only updated after GC, not continuously.
041 * # TYPE jvm_memory_pool_allocated_bytes_total counter
042 * jvm_memory_pool_allocated_bytes_total{pool="Code Cache"} 4336448.0
043 * jvm_memory_pool_allocated_bytes_total{pool="Compressed Class Space"} 875016.0
044 * jvm_memory_pool_allocated_bytes_total{pool="Metaspace"} 7480456.0
045 * jvm_memory_pool_allocated_bytes_total{pool="PS Eden Space"} 1.79232824E8
046 * jvm_memory_pool_allocated_bytes_total{pool="PS Old Gen"} 1428888.0
047 * jvm_memory_pool_allocated_bytes_total{pool="PS Survivor Space"} 4115280.0
048 * </pre>
049 */
050public class JvmMemoryPoolAllocationMetrics {
051
052  private static final String JVM_MEMORY_POOL_ALLOCATED_BYTES_TOTAL =
053      "jvm_memory_pool_allocated_bytes_total";
054
055  private final List<GarbageCollectorMXBean> garbageCollectorBeans;
056  private final Labels constLabels;
057
058  private JvmMemoryPoolAllocationMetrics(
059      List<GarbageCollectorMXBean> garbageCollectorBeans, Labels constLabels) {
060    this.garbageCollectorBeans = garbageCollectorBeans;
061    this.constLabels = constLabels;
062  }
063
064  private void register(PrometheusRegistry registry) {
065    Counter allocatedCounter =
066        Counter.builder()
067            .name(JVM_MEMORY_POOL_ALLOCATED_BYTES_TOTAL)
068            .help(
069                "Total bytes allocated in a given JVM memory pool. Only updated after GC, "
070                    + "not continuously.")
071            .labelNames("pool")
072            .constLabels(constLabels)
073            .register(registry);
074
075    AllocationCountingNotificationListener listener =
076        new AllocationCountingNotificationListener(allocatedCounter);
077    for (GarbageCollectorMXBean bean : garbageCollectorBeans) {
078      if (bean instanceof NotificationEmitter) {
079        ((NotificationEmitter) bean).addNotificationListener(listener, null, null);
080      }
081    }
082  }
083
084  static class AllocationCountingNotificationListener implements NotificationListener {
085
086    private final Map<String, Long> lastMemoryUsage = new HashMap<>();
087    private final Counter counter;
088
089    AllocationCountingNotificationListener(Counter counter) {
090      this.counter = counter;
091    }
092
093    @Override
094    public synchronized void handleNotification(Notification notification, Object handback) {
095      GarbageCollectionNotificationInfo info =
096          GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData());
097      GcInfo gcInfo = info.getGcInfo();
098      Map<String, MemoryUsage> memoryUsageBeforeGc = gcInfo.getMemoryUsageBeforeGc();
099      Map<String, MemoryUsage> memoryUsageAfterGc = gcInfo.getMemoryUsageAfterGc();
100      for (Map.Entry<String, MemoryUsage> entry : memoryUsageBeforeGc.entrySet()) {
101        String memoryPool = entry.getKey();
102        long before = entry.getValue().getUsed();
103        long after = Objects.requireNonNull(memoryUsageAfterGc.get(memoryPool)).getUsed();
104        handleMemoryPool(memoryPool, before, after);
105      }
106    }
107
108    // Visible for testing
109    void handleMemoryPool(String memoryPool, long before, long after) {
110      /*
111       * Calculate increase in the memory pool by comparing memory used
112       * after last GC, before this GC, and after this GC.
113       * See ascii illustration below.
114       * Make sure to count only increases and ignore decreases.
115       * (Typically a pool will only increase between GCs or during GCs, not both.
116       * E.g. eden pools between GCs. Survivor and old generation pools during GCs.)
117       *
118       *                         |<-- diff1 -->|<-- diff2 -->|
119       * Timeline: |-- last GC --|             |---- GC -----|
120       *                      ___^__        ___^____      ___^___
121       * Mem. usage vars:    / last \      / before \    / after \
122       */
123
124      // Get last memory usage after GC and remember memory used after for next time
125      long last = getAndSet(lastMemoryUsage, memoryPool, after);
126      // Difference since last GC
127      long diff1 = before - last;
128      // Difference during this GC
129      long diff2 = after - before;
130      // Make sure to only count increases
131      if (diff1 < 0) {
132        diff1 = 0;
133      }
134      if (diff2 < 0) {
135        diff2 = 0;
136      }
137      long increase = diff1 + diff2;
138      if (increase > 0) {
139        counter.labelValues(memoryPool).inc(increase);
140      }
141    }
142
143    private static long getAndSet(Map<String, Long> map, String key, long value) {
144      Long last = map.put(key, value);
145      return last == null ? 0 : last;
146    }
147  }
148
149  public static Builder builder() {
150    return new Builder();
151  }
152
153  @SuppressWarnings("unused")
154  public static Builder builder(PrometheusProperties config) {
155    return new Builder();
156  }
157
158  public static class Builder {
159    @Nullable private List<GarbageCollectorMXBean> garbageCollectorBeans;
160    private Labels constLabels = Labels.EMPTY;
161
162    private Builder() {}
163
164    public Builder constLabels(Labels constLabels) {
165      this.constLabels = constLabels;
166      return this;
167    }
168
169    /** Package private. For testing only. */
170    Builder withGarbageCollectorBeans(List<GarbageCollectorMXBean> garbageCollectorBeans) {
171      this.garbageCollectorBeans = garbageCollectorBeans;
172      return this;
173    }
174
175    public void register() {
176      register(PrometheusRegistry.defaultRegistry);
177    }
178
179    public void register(PrometheusRegistry registry) {
180      List<GarbageCollectorMXBean> garbageCollectorBeans = this.garbageCollectorBeans;
181      if (garbageCollectorBeans == null) {
182        garbageCollectorBeans = ManagementFactory.getGarbageCollectorMXBeans();
183      }
184      new JvmMemoryPoolAllocationMetrics(garbageCollectorBeans, constLabels).register(registry);
185    }
186  }
187}