001package io.prometheus.metrics.instrumentation.jvm;
002
003import static java.util.Objects.requireNonNull;
004
005import io.prometheus.metrics.config.PrometheusProperties;
006import io.prometheus.metrics.core.metrics.CounterWithCallback;
007import io.prometheus.metrics.core.metrics.GaugeWithCallback;
008import io.prometheus.metrics.model.registry.PrometheusRegistry;
009import java.lang.management.ManagementFactory;
010import java.lang.management.ThreadInfo;
011import java.lang.management.ThreadMXBean;
012import java.util.Arrays;
013import java.util.HashMap;
014import java.util.Map;
015import javax.annotation.Nullable;
016
017/**
018 * JVM Thread metrics. The {@link JvmThreadsMetrics} are registered as part of the {@link
019 * JvmMetrics} like this:
020 *
021 * <pre>{@code
022 * JvmMetrics.builder().register();
023 * }</pre>
024 *
025 * However, if you want only the {@link JvmThreadsMetrics} you can also register them directly:
026 *
027 * <pre>{@code
028 * JvmThreadMetrics.builder().register();
029 * }</pre>
030 *
031 * Example metrics being exported:
032 *
033 * <pre>
034 * # HELP jvm_threads_current Current thread count of a JVM
035 * # TYPE jvm_threads_current gauge
036 * jvm_threads_current 10.0
037 * # HELP jvm_threads_daemon Daemon thread count of a JVM
038 * # TYPE jvm_threads_daemon gauge
039 * jvm_threads_daemon 8.0
040 * # HELP jvm_threads_deadlocked Cycles of JVM-threads that are in deadlock waiting to acquire object monitors or ownable synchronizers
041 * # TYPE jvm_threads_deadlocked gauge
042 * jvm_threads_deadlocked 0.0
043 * # HELP jvm_threads_deadlocked_monitor Cycles of JVM-threads that are in deadlock waiting to acquire object monitors
044 * # TYPE jvm_threads_deadlocked_monitor gauge
045 * jvm_threads_deadlocked_monitor 0.0
046 * # HELP jvm_threads_peak Peak thread count of a JVM
047 * # TYPE jvm_threads_peak gauge
048 * jvm_threads_peak 10.0
049 * # HELP jvm_threads_started_total Started thread count of a JVM
050 * # TYPE jvm_threads_started_total counter
051 * jvm_threads_started_total 10.0
052 * # HELP jvm_threads_state Current count of threads by state
053 * # TYPE jvm_threads_state gauge
054 * jvm_threads_state{state="BLOCKED"} 0.0
055 * jvm_threads_state{state="NEW"} 0.0
056 * jvm_threads_state{state="RUNNABLE"} 5.0
057 * jvm_threads_state{state="TERMINATED"} 0.0
058 * jvm_threads_state{state="TIMED_WAITING"} 2.0
059 * jvm_threads_state{state="UNKNOWN"} 0.0
060 * jvm_threads_state{state="WAITING"} 3.0
061 * </pre>
062 */
063public class JvmThreadsMetrics {
064
065  private static final String UNKNOWN = "UNKNOWN";
066  private static final String JVM_THREADS_STATE = "jvm_threads_state";
067  private static final String JVM_THREADS_CURRENT = "jvm_threads_current";
068  private static final String JVM_THREADS_DAEMON = "jvm_threads_daemon";
069  private static final String JVM_THREADS_PEAK = "jvm_threads_peak";
070  private static final String JVM_THREADS_STARTED_TOTAL = "jvm_threads_started_total";
071  private static final String JVM_THREADS_DEADLOCKED = "jvm_threads_deadlocked";
072  private static final String JVM_THREADS_DEADLOCKED_MONITOR = "jvm_threads_deadlocked_monitor";
073
074  private final PrometheusProperties config;
075  private final ThreadMXBean threadBean;
076  private final boolean isNativeImage;
077
078  private JvmThreadsMetrics(
079      boolean isNativeImage, ThreadMXBean threadBean, PrometheusProperties config) {
080    this.config = config;
081    this.threadBean = threadBean;
082    this.isNativeImage = isNativeImage;
083  }
084
085  private void register(PrometheusRegistry registry) {
086
087    GaugeWithCallback.builder(config)
088        .name(JVM_THREADS_CURRENT)
089        .help("Current thread count of a JVM")
090        .callback(callback -> callback.call(threadBean.getThreadCount()))
091        .register(registry);
092
093    GaugeWithCallback.builder(config)
094        .name(JVM_THREADS_DAEMON)
095        .help("Daemon thread count of a JVM")
096        .callback(callback -> callback.call(threadBean.getDaemonThreadCount()))
097        .register(registry);
098
099    GaugeWithCallback.builder(config)
100        .name(JVM_THREADS_PEAK)
101        .help("Peak thread count of a JVM")
102        .callback(callback -> callback.call(threadBean.getPeakThreadCount()))
103        .register(registry);
104
105    CounterWithCallback.builder(config)
106        .name(JVM_THREADS_STARTED_TOTAL)
107        .help("Started thread count of a JVM")
108        .callback(callback -> callback.call(threadBean.getTotalStartedThreadCount()))
109        .register(registry);
110
111    if (!isNativeImage) {
112      GaugeWithCallback.builder(config)
113          .name(JVM_THREADS_DEADLOCKED)
114          .help(
115              "Cycles of JVM-threads that are in deadlock waiting to acquire object monitors or "
116                  + "ownable synchronizers")
117          .callback(
118              callback -> callback.call(nullSafeArrayLength(threadBean.findDeadlockedThreads())))
119          .register(registry);
120
121      GaugeWithCallback.builder(config)
122          .name(JVM_THREADS_DEADLOCKED_MONITOR)
123          .help("Cycles of JVM-threads that are in deadlock waiting to acquire object monitors")
124          .callback(
125              callback ->
126                  callback.call(nullSafeArrayLength(threadBean.findMonitorDeadlockedThreads())))
127          .register(registry);
128
129      GaugeWithCallback.builder(config)
130          .name(JVM_THREADS_STATE)
131          .help("Current count of threads by state")
132          .labelNames("state")
133          .callback(
134              callback -> {
135                Map<String, Integer> threadStateCounts = getThreadStateCountMap(threadBean);
136                for (Map.Entry<String, Integer> entry : threadStateCounts.entrySet()) {
137                  callback.call(entry.getValue(), entry.getKey());
138                }
139              })
140          .register(registry);
141    }
142  }
143
144  private Map<String, Integer> getThreadStateCountMap(ThreadMXBean threadBean) {
145    long[] threadIds = threadBean.getAllThreadIds();
146
147    // Code to remove any thread id values <= 0
148    int writePos = 0;
149    for (int i = 0; i < threadIds.length; i++) {
150      if (threadIds[i] > 0) {
151        threadIds[writePos++] = threadIds[i];
152      }
153    }
154
155    final int numberOfInvalidThreadIds = threadIds.length - writePos;
156    threadIds = Arrays.copyOf(threadIds, writePos);
157
158    // Get thread information without computing any stack traces
159    ThreadInfo[] allThreads = threadBean.getThreadInfo(threadIds, 0);
160
161    // Initialize the map with all thread states
162    Map<String, Integer> threadCounts = new HashMap<>();
163    for (Thread.State state : Thread.State.values()) {
164      threadCounts.put(state.name(), 0);
165    }
166
167    // Collect the actual thread counts
168    for (ThreadInfo curThread : allThreads) {
169      if (curThread != null) {
170        Thread.State threadState = curThread.getThreadState();
171        threadCounts.put(
172            threadState.name(), requireNonNull(threadCounts.get(threadState.name())) + 1);
173      }
174    }
175
176    // Add the thread count for invalid thread ids
177    threadCounts.put(UNKNOWN, numberOfInvalidThreadIds);
178
179    return threadCounts;
180  }
181
182  private double nullSafeArrayLength(long[] array) {
183    return null == array ? 0 : array.length;
184  }
185
186  public static Builder builder() {
187    return new Builder(PrometheusProperties.get());
188  }
189
190  public static Builder builder(PrometheusProperties config) {
191    return new Builder(config);
192  }
193
194  public static class Builder {
195
196    private final PrometheusProperties config;
197    @Nullable private Boolean isNativeImage;
198    @Nullable private ThreadMXBean threadBean;
199
200    private Builder(PrometheusProperties config) {
201      this.config = config;
202    }
203
204    /** Package private. For testing only. */
205    Builder threadBean(ThreadMXBean threadBean) {
206      this.threadBean = threadBean;
207      return this;
208    }
209
210    /** Package private. For testing only. */
211    Builder isNativeImage(boolean isNativeImage) {
212      this.isNativeImage = isNativeImage;
213      return this;
214    }
215
216    public void register() {
217      register(PrometheusRegistry.defaultRegistry);
218    }
219
220    public void register(PrometheusRegistry registry) {
221      ThreadMXBean threadBean =
222          this.threadBean != null ? this.threadBean : ManagementFactory.getThreadMXBean();
223      boolean isNativeImage =
224          this.isNativeImage != null ? this.isNativeImage : NativeImageChecker.isGraalVmNativeImage;
225      new JvmThreadsMetrics(isNativeImage, threadBean, config).register(registry);
226    }
227  }
228}