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