001package io.prometheus.metrics.instrumentation.jvm;
002
003import io.prometheus.metrics.annotations.StableApi;
004import io.prometheus.metrics.config.PrometheusProperties;
005import io.prometheus.metrics.core.metrics.GaugeWithCallback;
006import io.prometheus.metrics.model.registry.PrometheusRegistry;
007import io.prometheus.metrics.model.snapshots.Labels;
008import io.prometheus.metrics.model.snapshots.Unit;
009import java.lang.management.ManagementFactory;
010import java.util.concurrent.atomic.AtomicBoolean;
011import java.util.function.Consumer;
012import java.util.regex.Matcher;
013import java.util.regex.Pattern;
014import javax.management.InstanceNotFoundException;
015import javax.management.MBeanException;
016import javax.management.MalformedObjectNameException;
017import javax.management.ObjectName;
018import javax.management.ReflectionException;
019
020/**
021 * JVM native memory. JVM native memory tracking is disabled by default. You need to enable it by
022 * starting your JVM with this flag:
023 *
024 * <pre>-XX:NativeMemoryTracking=summary</pre>
025 *
026 * <p>When native memory tracking is disabled the metrics are not registered either.
027 *
028 * <p>
029 *
030 * <p>The {@link JvmNativeMemoryMetrics} are registered as part of the {@link JvmMetrics} like this:
031 *
032 * <pre>{@code
033 * JvmMetrics.builder().register();
034 * }</pre>
035 *
036 * However, if you want only the {@link JvmNativeMemoryMetrics} you can also register them directly:
037 *
038 * <pre>{@code
039 * JvmNativeMemoryMetrics.builder().register();
040 * }</pre>
041 *
042 * Example metrics being exported:
043 *
044 * <pre>
045 * # HELP jvm_native_memory_committed_bytes Committed bytes of a given JVM. Committed memory represents the amount of memory the JVM is using right now.
046 * # TYPE jvm_native_memory_committed_bytes gauge
047 * jvm_native_memory_committed_bytes{pool="Arena Chunk"} 58480.0
048 * jvm_native_memory_committed_bytes{pool="Arguments"} 25119.0
049 * jvm_native_memory_committed_bytes{pool="Class"} 1.00609438E8
050 * jvm_native_memory_committed_bytes{pool="Code"} 2.7980888E7
051 * jvm_native_memory_committed_bytes{pool="Compiler"} 529922.0
052 * jvm_native_memory_committed_bytes{pool="GC"} 515466.0
053 * jvm_native_memory_committed_bytes{pool="Internal"} 673194.0
054 * jvm_native_memory_committed_bytes{pool="Java Heap"} 4.0923136E7
055 * jvm_native_memory_committed_bytes{pool="Logging"} 4596.0
056 * jvm_native_memory_committed_bytes{pool="Module"} 96408.0
057 * jvm_native_memory_committed_bytes{pool="Native Memory Tracking"} 3929432.0
058 * jvm_native_memory_committed_bytes{pool="Other"} 667656.0
059 * jvm_native_memory_committed_bytes{pool="Safepoint"} 8192.0
060 * jvm_native_memory_committed_bytes{pool="Symbol"} 2.4609808E7
061 * jvm_native_memory_committed_bytes{pool="Synchronizer"} 272520.0
062 * jvm_native_memory_committed_bytes{pool="Thread"} 3546896.0
063 * jvm_native_memory_committed_bytes{pool="Total"} 2.0448392E8
064 * jvm_native_memory_committed_bytes{pool="Tracing"} 1.0
065 * jvm_native_memory_committed_bytes{pool="Unknown"} 32768.0
066 * # HELP jvm_native_memory_reserved_bytes Reserved bytes of a given JVM. Reserved memory represents the total amount of memory the JVM can potentially use.
067 * # TYPE jvm_native_memory_reserved_bytes gauge
068 * jvm_native_memory_reserved_bytes{pool="Arena Chunk"} 25736.0
069 * jvm_native_memory_reserved_bytes{pool="Arguments"} 25119.0
070 * jvm_native_memory_reserved_bytes{pool="Class"} 1.162665374E9
071 * jvm_native_memory_reserved_bytes{pool="Code"} 2.55386712E8
072 * jvm_native_memory_reserved_bytes{pool="Compiler"} 529922.0
073 * jvm_native_memory_reserved_bytes{pool="GC"} 1695114.0
074 * jvm_native_memory_reserved_bytes{pool="Internal"} 673191.0
075 * jvm_native_memory_reserved_bytes{pool="Java Heap"} 4.02653184E8
076 * jvm_native_memory_reserved_bytes{pool="Logging"} 4596.0
077 * jvm_native_memory_reserved_bytes{pool="Module"} 96408.0
078 * jvm_native_memory_reserved_bytes{pool="Native Memory Tracking"} 3929400.0
079 * jvm_native_memory_reserved_bytes{pool="Other"} 667656.0
080 * jvm_native_memory_reserved_bytes{pool="Safepoint"} 8192.0
081 * jvm_native_memory_reserved_bytes{pool="Symbol"} 2.4609808E7
082 * jvm_native_memory_reserved_bytes{pool="Synchronizer"} 272520.0
083 * jvm_native_memory_reserved_bytes{pool="Thread"} 3.383272E7
084 * jvm_native_memory_reserved_bytes{pool="Total"} 1.887108421E9
085 * jvm_native_memory_reserved_bytes{pool="Tracing"} 1.0
086 * jvm_native_memory_reserved_bytes{pool="Unknown"} 32768.0
087 * </pre>
088 */
089@StableApi
090public class JvmNativeMemoryMetrics {
091  private static final String JVM_NATIVE_MEMORY_RESERVED_BYTES = "jvm_native_memory_reserved_bytes";
092  private static final String JVM_NATIVE_MEMORY_COMMITTED_BYTES =
093      "jvm_native_memory_committed_bytes";
094
095  private static final Pattern pattern =
096      Pattern.compile("\\s*([A-Z][A-Za-z\\s]*[A-Za-z]+).*reserved=(\\d+), committed=(\\d+)");
097
098  /** Package private. For testing only. */
099  static final AtomicBoolean isEnabled = new AtomicBoolean(true);
100
101  private final PrometheusProperties config;
102  private final PlatformMBeanServerAdapter adapter;
103  private final Labels constLabels;
104
105  private JvmNativeMemoryMetrics(
106      PrometheusProperties config, PlatformMBeanServerAdapter adapter, Labels constLabels) {
107    this.config = config;
108    this.adapter = adapter;
109    this.constLabels = constLabels;
110  }
111
112  private void register(PrometheusRegistry registry) {
113    // first call will check if enabled and set the flag
114    vmNativeMemorySummaryInBytesOrEmpty();
115    if (isEnabled.get()) {
116      GaugeWithCallback.builder(config)
117          .name(JVM_NATIVE_MEMORY_RESERVED_BYTES)
118          .help(
119              "Reserved bytes of a given JVM. Reserved memory represents the total amount of "
120                  + "memory the JVM can potentially use.")
121          .unit(Unit.BYTES)
122          .labelNames("pool")
123          .callback(makeCallback(true))
124          .constLabels(constLabels)
125          .register(registry);
126
127      GaugeWithCallback.builder(config)
128          .name(JVM_NATIVE_MEMORY_COMMITTED_BYTES)
129          .help(
130              "Committed bytes of a given JVM. Committed memory represents the amount of "
131                  + "memory the JVM is using right now.")
132          .unit(Unit.BYTES)
133          .labelNames("pool")
134          .callback(makeCallback(false))
135          .constLabels(constLabels)
136          .register(registry);
137    }
138  }
139
140  private Consumer<GaugeWithCallback.Callback> makeCallback(Boolean reserved) {
141    return callback -> {
142      String summary = vmNativeMemorySummaryInBytesOrEmpty();
143      if (!summary.isEmpty()) {
144        Matcher matcher = pattern.matcher(summary);
145        while (matcher.find()) {
146          String category = matcher.group(1);
147          long value =
148              reserved ? Long.parseLong(matcher.group(2)) : Long.parseLong(matcher.group(3));
149          callback.call(value, category);
150        }
151      }
152    };
153  }
154
155  private String vmNativeMemorySummaryInBytesOrEmpty() {
156    if (!isEnabled.get()) {
157      return "";
158    }
159    try {
160      // requires -XX:NativeMemoryTracking=summary
161      String summary = adapter.vmNativeMemorySummaryInBytes();
162      if (summary.isEmpty() || summary.trim().contains("Native memory tracking is not enabled")) {
163        isEnabled.set(false);
164        return "";
165      } else {
166        return summary;
167      }
168    } catch (RuntimeException ex) {
169      // ignore errors (native memory tracking not enabled or other runtime failures)
170      isEnabled.set(false);
171      return "";
172    }
173  }
174
175  interface PlatformMBeanServerAdapter {
176    String vmNativeMemorySummaryInBytes();
177  }
178
179  static class DefaultPlatformMBeanServerAdapter implements PlatformMBeanServerAdapter {
180    @Override
181    public String vmNativeMemorySummaryInBytes() {
182      try {
183        return (String)
184            ManagementFactory.getPlatformMBeanServer()
185                .invoke(
186                    new ObjectName("com.sun.management:type=DiagnosticCommand"),
187                    "vmNativeMemory",
188                    new Object[] {new String[] {"summary", "scale=B"}},
189                    new String[] {"[Ljava.lang.String;"});
190      } catch (ReflectionException
191          | MalformedObjectNameException
192          | InstanceNotFoundException
193          | MBeanException e) {
194        throw new IllegalStateException("Native memory tracking is not enabled", e);
195      }
196    }
197  }
198
199  public static Builder builder() {
200    return new Builder(PrometheusProperties.get());
201  }
202
203  public static Builder builder(PrometheusProperties config) {
204    return new Builder(config);
205  }
206
207  public static class Builder {
208
209    private final PrometheusProperties config;
210    private final PlatformMBeanServerAdapter adapter;
211    private Labels constLabels = Labels.EMPTY;
212
213    private Builder(PrometheusProperties config) {
214      this(config, new DefaultPlatformMBeanServerAdapter());
215    }
216
217    /** Package private. For testing only. */
218    Builder(PrometheusProperties config, PlatformMBeanServerAdapter adapter) {
219      this.config = config;
220      this.adapter = adapter;
221    }
222
223    public Builder constLabels(Labels constLabels) {
224      this.constLabels = constLabels;
225      return this;
226    }
227
228    public void register() {
229      register(PrometheusRegistry.defaultRegistry);
230    }
231
232    public void register(PrometheusRegistry registry) {
233      new JvmNativeMemoryMetrics(config, adapter, constLabels).register(registry);
234    }
235  }
236}