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}