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}