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