001package io.prometheus.client.hotspot;
002
003import io.prometheus.client.Collector;
004import io.prometheus.client.CounterMetricFamily;
005import io.prometheus.client.GaugeMetricFamily;
006
007import java.io.BufferedReader;
008import java.io.FileNotFoundException;
009import java.io.FileReader;
010import java.io.IOException;
011import java.lang.management.ManagementFactory;
012import java.lang.management.OperatingSystemMXBean;
013import java.lang.management.RuntimeMXBean;
014import java.lang.reflect.InvocationTargetException;
015import java.lang.reflect.Method;
016import java.util.ArrayList;
017import java.util.List;
018import java.util.logging.Level;
019import java.util.logging.Logger;
020
021/**
022 * Exports the standard exports common across all prometheus clients.
023 * <p>
024 * This includes stats like CPU time spent and memory usage.
025 * <p>
026 * Example usage:
027 * <pre>
028 * {@code
029 *   new StandardExports().register();
030 * }
031 * </pre>
032 */
033public class StandardExports extends Collector {
034  private static final Logger LOGGER = Logger.getLogger(StandardExports.class.getName());
035
036  private final StatusReader statusReader;
037  private final OperatingSystemMXBean osBean;
038  private final RuntimeMXBean runtimeBean;
039  private final boolean linux;
040
041  public StandardExports() {
042    this(new StatusReader(),
043         ManagementFactory.getOperatingSystemMXBean(),
044         ManagementFactory.getRuntimeMXBean());
045  }
046
047  StandardExports(StatusReader statusReader, OperatingSystemMXBean osBean, RuntimeMXBean runtimeBean) {
048      this.statusReader = statusReader;
049      this.osBean = osBean;
050      this.runtimeBean = runtimeBean;
051      this.linux = (osBean.getName().indexOf("Linux") == 0);
052  }
053
054  private final static double KB = 1024;
055
056  @Override
057  public List<MetricFamilySamples> collect() {
058    List<MetricFamilySamples> mfs = new ArrayList<MetricFamilySamples>();
059
060    try {
061      // There exist at least 2 similar but unrelated UnixOperatingSystemMXBean interfaces, in
062      // com.sun.management and com.ibm.lang.management. Hence use reflection and recursively go
063      // through implemented interfaces until the method can be made accessible and invoked.
064      Long processCpuTime = callLongGetter("getProcessCpuTime", osBean);
065      mfs.add(new CounterMetricFamily("process_cpu_seconds_total", "Total user and system CPU time spent in seconds.",
066          processCpuTime / NANOSECONDS_PER_SECOND));
067    }
068    catch (Exception e) {
069      LOGGER.log(Level.FINE,"Could not access process cpu time", e);
070    }
071
072    mfs.add(new GaugeMetricFamily("process_start_time_seconds", "Start time of the process since unix epoch in seconds.",
073        runtimeBean.getStartTime() / MILLISECONDS_PER_SECOND));
074
075    // There exist at least 2 similar but unrelated UnixOperatingSystemMXBean interfaces, in
076    // com.sun.management and com.ibm.lang.management. Hence use reflection and recursively go
077    // through implemented interfaces until the method can be made accessible and invoked.
078    try {
079      Long openFdCount = callLongGetter("getOpenFileDescriptorCount", osBean);
080      mfs.add(new GaugeMetricFamily(
081          "process_open_fds", "Number of open file descriptors.", openFdCount));
082      Long maxFdCount = callLongGetter("getMaxFileDescriptorCount", osBean);
083      mfs.add(new GaugeMetricFamily(
084          "process_max_fds", "Maximum number of open file descriptors.", maxFdCount));
085    } catch (Exception e) {
086      // Ignore, expected on non-Unix OSs.
087    }
088
089    // There's no standard Java or POSIX way to get memory stats,
090    // so add support for just Linux for now.
091    if (linux) {
092      try {
093        collectMemoryMetricsLinux(mfs);
094      } catch (Exception e) {
095        // If the format changes, log a warning and return what we can.
096        LOGGER.warning(e.toString());
097      }
098    }
099    return mfs;
100  }
101
102  static Long callLongGetter(String getterName, Object obj)
103      throws NoSuchMethodException, InvocationTargetException {
104    return callLongGetter(obj.getClass().getMethod(getterName), obj);
105  }
106
107  /**
108   * Attempts to call a method either directly or via one of the implemented interfaces.
109   * <p>
110   * A Method object refers to a specific method declared in a specific class. The first invocation
111   * might happen with method == SomeConcreteClass.publicLongGetter() and will fail if
112   * SomeConcreteClass is not public. We then recurse over all interfaces implemented by
113   * SomeConcreteClass (or extended by those interfaces and so on) until we eventually invoke
114   * callMethod() with method == SomePublicInterface.publicLongGetter(), which will then succeed.
115   * <p>
116   * There is a built-in assumption that the method will never return null (or, equivalently, that
117   * it returns the primitive data type, i.e. {@code long} rather than {@code Long}). If this
118   * assumption doesn't hold, the method might be called repeatedly and the returned value will be
119   * the one produced by the last call.
120   */
121  static Long callLongGetter(Method method, Object obj) throws InvocationTargetException  {
122    try {
123      return (Long) method.invoke(obj);
124    } catch (IllegalAccessException e) {
125      // Expected, the declaring class or interface might not be public.
126    }
127
128    // Iterate over all implemented/extended interfaces and attempt invoking the method with the
129    // same name and parameters on each.
130    for (Class<?> clazz : method.getDeclaringClass().getInterfaces()) {
131      try {
132        Method interfaceMethod = clazz.getMethod(method.getName(), method.getParameterTypes());
133        Long result = callLongGetter(interfaceMethod, obj);
134        if (result != null) {
135          return result;
136        }
137      } catch (NoSuchMethodException e) {
138        // Expected, class might implement multiple, unrelated interfaces.
139      }
140    }
141
142    return null;
143  }
144
145  void collectMemoryMetricsLinux(List<MetricFamilySamples> mfs) {
146    // statm/stat report in pages, and it's non-trivial to get pagesize from Java
147    // so we parse status instead.
148    BufferedReader br = null;
149    try {
150      br = statusReader.procSelfStatusReader();
151      String line;
152      while ((line = br.readLine()) != null) {
153        if (line.startsWith("VmSize:")) {
154          mfs.add(new GaugeMetricFamily("process_virtual_memory_bytes",
155              "Virtual memory size in bytes.",
156              Float.parseFloat(line.split("\\s+")[1]) * KB));
157        } else if (line.startsWith("VmRSS:")) {
158          mfs.add(new GaugeMetricFamily("process_resident_memory_bytes",
159              "Resident memory size in bytes.",
160              Float.parseFloat(line.split("\\s+")[1]) * KB));
161        }
162      }
163    } catch (IOException e) {
164      LOGGER.fine(e.toString());
165    } finally {
166      if (br != null) {
167        try {
168          br.close();
169        } catch (IOException e) {
170          LOGGER.fine(e.toString());
171        }
172      }
173    }
174  }
175
176  static class StatusReader {
177    BufferedReader procSelfStatusReader() throws FileNotFoundException {
178      return new BufferedReader(new FileReader("/proc/self/status"));
179    }
180  }
181}