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}