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 io.prometheus.metrics.model.snapshots.Unit; 008import java.io.BufferedReader; 009import java.io.File; 010import java.io.IOException; 011import java.io.InputStreamReader; 012import java.lang.management.ManagementFactory; 013import java.lang.management.OperatingSystemMXBean; 014import java.lang.management.RuntimeMXBean; 015import java.lang.reflect.InvocationTargetException; 016import java.lang.reflect.Method; 017import java.nio.charset.StandardCharsets; 018import java.nio.file.Files; 019 020/** 021 * Process metrics. 022 * 023 * <p>These metrics are defined in the <a 024 * href="https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics">process 025 * metrics</a> section of the Prometheus client library documentation, and they are implemented 026 * across client libraries in multiple programming languages. 027 * 028 * <p>Technically, some of them are OS-level metrics and not JVM-level metrics. However, I'm still 029 * putting them in the {@code prometheus-metrics-instrumentation-jvm} module, because first it seems 030 * overkill to create a separate Maven module just for the {@link ProcessMetrics} class, and seconds 031 * some of these metrics are coming from the JVM via JMX anyway. 032 * 033 * <p>The {@link ProcessMetrics} are registered as part of the {@link JvmMetrics} like this: 034 * 035 * <pre>{@code 036 * JvmMetrics.builder().register(); 037 * }</pre> 038 * 039 * However, if you want only the {@link ProcessMetrics} you can also register them directly: 040 * 041 * <pre>{@code 042 * ProcessMetrics.builder().register(); 043 * }</pre> 044 * 045 * Example metrics being exported: 046 * 047 * <pre> 048 * # HELP process_cpu_seconds_total Total user and system CPU time spent in seconds. 049 * # TYPE process_cpu_seconds_total counter 050 * process_cpu_seconds_total 1.63 051 * # HELP process_max_fds Maximum number of open file descriptors. 052 * # TYPE process_max_fds gauge 053 * process_max_fds 524288.0 054 * # HELP process_open_fds Number of open file descriptors. 055 * # TYPE process_open_fds gauge 056 * process_open_fds 28.0 057 * # HELP process_resident_memory_bytes Resident memory size in bytes. 058 * # TYPE process_resident_memory_bytes gauge 059 * process_resident_memory_bytes 7.8577664E7 060 * # HELP process_start_time_seconds Start time of the process since unix epoch in seconds. 061 * # TYPE process_start_time_seconds gauge 062 * process_start_time_seconds 1.693829439767E9 063 * # HELP process_virtual_memory_bytes Virtual memory size in bytes. 064 * # TYPE process_virtual_memory_bytes gauge 065 * process_virtual_memory_bytes 1.2683624448E10 066 * </pre> 067 */ 068public class ProcessMetrics { 069 070 private static final String PROCESS_CPU_SECONDS_TOTAL = "process_cpu_seconds_total"; 071 private static final String PROCESS_START_TIME_SECONDS = "process_start_time_seconds"; 072 private static final String PROCESS_OPEN_FDS = "process_open_fds"; 073 private static final String PROCESS_MAX_FDS = "process_max_fds"; 074 private static final String PROCESS_VIRTUAL_MEMORY_BYTES = "process_virtual_memory_bytes"; 075 private static final String PROCESS_RESIDENT_MEMORY_BYTES = "process_resident_memory_bytes"; 076 077 static final File PROC_SELF_STATUS = new File("/proc/self/status"); 078 079 private final PrometheusProperties config; 080 private final OperatingSystemMXBean osBean; 081 private final RuntimeMXBean runtimeBean; 082 private final Grepper grepper; 083 private final boolean linux; 084 085 private ProcessMetrics( 086 OperatingSystemMXBean osBean, 087 RuntimeMXBean runtimeBean, 088 Grepper grepper, 089 PrometheusProperties config) { 090 this.osBean = osBean; 091 this.runtimeBean = runtimeBean; 092 this.grepper = grepper; 093 this.config = config; 094 this.linux = PROC_SELF_STATUS.canRead(); 095 } 096 097 private void register(PrometheusRegistry registry) { 098 099 CounterWithCallback.builder(config) 100 .name(PROCESS_CPU_SECONDS_TOTAL) 101 .help("Total user and system CPU time spent in seconds.") 102 .unit(Unit.SECONDS) 103 .callback( 104 callback -> { 105 try { 106 // There exist at least 2 similar but unrelated UnixOperatingSystemMXBean 107 // interfaces, in 108 // com.sun.management and com.ibm.lang.management. Hence use reflection and 109 // recursively go 110 // through implemented interfaces until the method can be made accessible and 111 // invoked. 112 Long processCpuTime = callLongGetter("getProcessCpuTime", osBean); 113 if (processCpuTime != null) { 114 callback.call(Unit.nanosToSeconds(processCpuTime)); 115 } 116 } catch (Exception ignored) { 117 // Ignored 118 } 119 }) 120 .register(registry); 121 122 GaugeWithCallback.builder(config) 123 .name(PROCESS_START_TIME_SECONDS) 124 .help("Start time of the process since unix epoch in seconds.") 125 .unit(Unit.SECONDS) 126 .callback(callback -> callback.call(Unit.millisToSeconds(runtimeBean.getStartTime()))) 127 .register(registry); 128 129 GaugeWithCallback.builder(config) 130 .name(PROCESS_OPEN_FDS) 131 .help("Number of open file descriptors.") 132 .callback( 133 callback -> { 134 try { 135 Long openFds = callLongGetter("getOpenFileDescriptorCount", osBean); 136 if (openFds != null) { 137 callback.call(openFds); 138 } 139 } catch (Exception ignored) { 140 // Ignored 141 } 142 }) 143 .register(registry); 144 145 GaugeWithCallback.builder(config) 146 .name(PROCESS_MAX_FDS) 147 .help("Maximum number of open file descriptors.") 148 .callback( 149 callback -> { 150 try { 151 Long maxFds = callLongGetter("getMaxFileDescriptorCount", osBean); 152 if (maxFds != null) { 153 callback.call(maxFds); 154 } 155 } catch (Exception ignored) { 156 // Ignored 157 } 158 }) 159 .register(registry); 160 161 if (linux) { 162 163 GaugeWithCallback.builder(config) 164 .name(PROCESS_VIRTUAL_MEMORY_BYTES) 165 .help("Virtual memory size in bytes.") 166 .unit(Unit.BYTES) 167 .callback( 168 callback -> { 169 try { 170 String line = grepper.lineStartingWith(PROC_SELF_STATUS, "VmSize:"); 171 callback.call(Unit.kiloBytesToBytes(Double.parseDouble(line.split("\\s+")[1]))); 172 } catch (Exception ignored) { 173 // Ignored 174 } 175 }) 176 .register(registry); 177 178 GaugeWithCallback.builder(config) 179 .name(PROCESS_RESIDENT_MEMORY_BYTES) 180 .help("Resident memory size in bytes.") 181 .unit(Unit.BYTES) 182 .callback( 183 callback -> { 184 try { 185 String line = grepper.lineStartingWith(PROC_SELF_STATUS, "VmRSS:"); 186 callback.call(Unit.kiloBytesToBytes(Double.parseDouble(line.split("\\s+")[1]))); 187 } catch (Exception ignored) { 188 // Ignored 189 } 190 }) 191 .register(registry); 192 } 193 } 194 195 private Long callLongGetter(String getterName, Object obj) 196 throws NoSuchMethodException, InvocationTargetException { 197 return callLongGetter(obj.getClass().getMethod(getterName), obj); 198 } 199 200 /** 201 * Attempts to call a method either directly or via one of the implemented interfaces. 202 * 203 * <p>A Method object refers to a specific method declared in a specific class. The first 204 * invocation might happen with method == SomeConcreteClass.publicLongGetter() and will fail if 205 * SomeConcreteClass is not public. We then recurse over all interfaces implemented by 206 * SomeConcreteClass (or extended by those interfaces and so on) until we eventually invoke 207 * callMethod() with method == SomePublicInterface.publicLongGetter(), which will then succeed. 208 * 209 * <p>There is a built-in assumption that the method will never return null (or, equivalently, 210 * that it returns the primitive data type, i.e. {@code long} rather than {@code Long}). If this 211 * assumption doesn't hold, the method might be called repeatedly and the returned value will be 212 * the one produced by the last call. 213 */ 214 private Long callLongGetter(Method method, Object obj) throws InvocationTargetException { 215 try { 216 return (Long) method.invoke(obj); 217 } catch (IllegalAccessException e) { 218 // Expected, the declaring class or interface might not be public. 219 } 220 221 // Iterate over all implemented/extended interfaces and attempt invoking the method with the 222 // same name and parameters on each. 223 for (Class<?> clazz : method.getDeclaringClass().getInterfaces()) { 224 try { 225 Method interfaceMethod = clazz.getMethod(method.getName(), method.getParameterTypes()); 226 Long result = callLongGetter(interfaceMethod, obj); 227 if (result != null) { 228 return result; 229 } 230 } catch (NoSuchMethodException e) { 231 // Expected, class might implement multiple, unrelated interfaces. 232 } 233 } 234 return null; 235 } 236 237 interface Grepper { 238 String lineStartingWith(File file, String prefix) throws IOException; 239 } 240 241 private static class FileGrepper implements Grepper { 242 243 @Override 244 public String lineStartingWith(File file, String prefix) throws IOException { 245 try (BufferedReader reader = 246 new BufferedReader( 247 new InputStreamReader(Files.newInputStream(file.toPath()), StandardCharsets.UTF_8))) { 248 String line = reader.readLine(); 249 while (line != null) { 250 if (line.startsWith(prefix)) { 251 return line; 252 } 253 line = reader.readLine(); 254 } 255 } 256 return null; 257 } 258 } 259 260 public static Builder builder() { 261 return new Builder(PrometheusProperties.get()); 262 } 263 264 public static Builder builder(PrometheusProperties config) { 265 return new Builder(config); 266 } 267 268 public static class Builder { 269 270 private final PrometheusProperties config; 271 private OperatingSystemMXBean osBean; 272 private RuntimeMXBean runtimeBean; 273 private Grepper grepper; 274 275 private Builder(PrometheusProperties config) { 276 this.config = config; 277 } 278 279 /** Package private. For testing only. */ 280 Builder osBean(OperatingSystemMXBean osBean) { 281 this.osBean = osBean; 282 return this; 283 } 284 285 /** Package private. For testing only. */ 286 Builder runtimeBean(RuntimeMXBean runtimeBean) { 287 this.runtimeBean = runtimeBean; 288 return this; 289 } 290 291 /** Package private. For testing only. */ 292 Builder grepper(Grepper grepper) { 293 this.grepper = grepper; 294 return this; 295 } 296 297 public void register() { 298 register(PrometheusRegistry.defaultRegistry); 299 } 300 301 public void register(PrometheusRegistry registry) { 302 OperatingSystemMXBean osBean = 303 this.osBean != null ? this.osBean : ManagementFactory.getOperatingSystemMXBean(); 304 RuntimeMXBean bean = 305 this.runtimeBean != null ? this.runtimeBean : ManagementFactory.getRuntimeMXBean(); 306 Grepper grepper = this.grepper != null ? this.grepper : new FileGrepper(); 307 new ProcessMetrics(osBean, bean, grepper, config).register(registry); 308 } 309 } 310}