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