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