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