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