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