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