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