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}