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