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}