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