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