Skip to content

Commit ce76a49

Browse files
alin-amanabrian-brazil
authored andcommitted
Make StandardExports work with modules and/or IBM JVMs (#281)
Make StandardExports work in restricted environments or IBM JVMs where com.sun.management.UnixOperatingSystemMXBean is not available. Use reflection instead to collect file descriptor metrics. Use reflection to recursively try invoking a method on the concrete class then on all the interfaces it implements until either success or all attempts have failed
1 parent bd94a66 commit ce76a49

File tree

2 files changed

+101
-25
lines changed

2 files changed

+101
-25
lines changed

simpleclient_hotspot/src/main/java/io/prometheus/client/hotspot/StandardExports.java

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.prometheus.client.hotspot;
22

3-
import com.sun.management.UnixOperatingSystemMXBean;
43
import io.prometheus.client.Collector;
54
import io.prometheus.client.CounterMetricFamily;
65
import io.prometheus.client.GaugeMetricFamily;
@@ -12,6 +11,7 @@
1211
import java.lang.management.ManagementFactory;
1312
import java.lang.management.OperatingSystemMXBean;
1413
import java.lang.management.RuntimeMXBean;
14+
import java.lang.reflect.InvocationTargetException;
1515
import java.lang.reflect.Method;
1616
import java.util.ArrayList;
1717
import java.util.List;
@@ -36,7 +36,6 @@ public class StandardExports extends Collector {
3636
private final StatusReader statusReader;
3737
private final OperatingSystemMXBean osBean;
3838
private final RuntimeMXBean runtimeBean;
39-
private final boolean unix;
4039
private final boolean linux;
4140

4241
public StandardExports() {
@@ -49,43 +48,42 @@ public StandardExports() {
4948
this.statusReader = statusReader;
5049
this.osBean = osBean;
5150
this.runtimeBean = runtimeBean;
52-
this.unix = (osBean instanceof UnixOperatingSystemMXBean);
5351
this.linux = (osBean.getName().indexOf("Linux") == 0);
5452
}
5553

5654
private final static double KB = 1024;
5755

56+
@Override
5857
public List<MetricFamilySamples> collect() {
5958
List<MetricFamilySamples> mfs = new ArrayList<MetricFamilySamples>();
6059

6160
try {
62-
/*
63-
There are two interfaces com.ibm.lang.management.OperatingSystemMXBean and com.sun.management.OperatingSystemMXBean,
64-
but no common interface which exposes getProcessCpuTime. Hence call on the implementing class, which is default access,
65-
so use reflection hack to set it accessible.
66-
*/
67-
Method method = osBean.getClass().getMethod("getProcessCpuTime", null);
68-
if (!method.isAccessible()) {
69-
method.setAccessible(true);
70-
}
71-
Long l = (Long) method.invoke(osBean);
61+
// There exist at least 2 similar but unrelated UnixOperatingSystemMXBean interfaces, in
62+
// com.sun.management and com.ibm.lang.management. Hence use reflection and recursively go
63+
// through implemented interfaces until the method can be made accessible and invoked.
64+
Long processCpuTime = callLongGetter("getProcessCpuTime", osBean);
7265
mfs.add(new CounterMetricFamily("process_cpu_seconds_total", "Total user and system CPU time spent in seconds.",
73-
l / NANOSECONDS_PER_SECOND));
66+
processCpuTime / NANOSECONDS_PER_SECOND));
7467
}
7568
catch (Exception e) {
7669
LOGGER.log(Level.FINE,"Could not access process cpu time", e);
7770
}
7871

79-
8072
mfs.add(new GaugeMetricFamily("process_start_time_seconds", "Start time of the process since unix epoch in seconds.",
8173
runtimeBean.getStartTime() / MILLISECONDS_PER_SECOND));
8274

83-
if (unix) {
84-
UnixOperatingSystemMXBean unixBean = (UnixOperatingSystemMXBean) osBean;
85-
mfs.add(new GaugeMetricFamily("process_open_fds", "Number of open file descriptors.",
86-
unixBean.getOpenFileDescriptorCount()));
87-
mfs.add(new GaugeMetricFamily("process_max_fds", "Maximum number of open file descriptors.",
88-
unixBean.getMaxFileDescriptorCount()));
75+
// There exist at least 2 similar but unrelated UnixOperatingSystemMXBean interfaces, in
76+
// com.sun.management and com.ibm.lang.management. Hence use reflection and recursively go
77+
// through implemented interfaces until the method can be made accessible and invoked.
78+
try {
79+
Long openFdCount = callLongGetter("getOpenFileDescriptorCount", osBean);
80+
mfs.add(new GaugeMetricFamily(
81+
"process_open_fds", "Number of open file descriptors.", openFdCount));
82+
Long maxFdCount = callLongGetter("getMaxFileDescriptorCount", osBean);
83+
mfs.add(new GaugeMetricFamily(
84+
"process_max_fds", "Maximum number of open file descriptors.", maxFdCount));
85+
} catch (Exception e) {
86+
// Ignore, expected on non-Unix OSs.
8987
}
9088

9189
// There's no standard Java or POSIX way to get memory stats,
@@ -101,6 +99,49 @@ but no common interface which exposes getProcessCpuTime. Hence call on the imple
10199
return mfs;
102100
}
103101

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+
104145
void collectMemoryMetricsLinux(List<MetricFamilySamples> mfs) {
105146
// statm/stat report in pages, and it's non-trivial to get pagesize from Java
106147
// so we parse status instead.

simpleclient_hotspot/src/test/java/io/prometheus/client/hotspot/StandardExportsTest.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
import static org.junit.Assert.assertEquals;
44
import static org.junit.Assert.assertNull;
5+
import static org.mockito.Mockito.mock;
56
import static org.mockito.Mockito.when;
67

7-
import com.sun.management.UnixOperatingSystemMXBean;
88
import java.io.BufferedReader;
99
import java.io.StringReader;
1010
import java.io.FileNotFoundException;
11+
import java.lang.management.OperatingSystemMXBean;
1112
import java.lang.management.RuntimeMXBean;
1213

1314
import io.prometheus.client.CollectorRegistry;
1415
import org.junit.Before;
1516
import org.junit.Test;
16-
import org.mockito.Mockito;
1717

1818
public class StandardExportsTest {
1919

@@ -23,17 +23,37 @@ BufferedReader procSelfStatusReader() throws FileNotFoundException {
2323
}
2424
}
2525

26+
// Interface with a signature equivalent to com.sun.management.OperatingSystemMXBean and
27+
// com.ibm.lang.management.OperatingSystemMXBean, without explicitly depending on either of
28+
// these classes, used to test reflection.
29+
interface GenericOperatingSystemMXBean extends OperatingSystemMXBean {
30+
public long getCommittedVirtualMemorySize();
31+
public long getTotalSwapSpaceSize();
32+
public long getFreeSwapSpaceSize();
33+
public long getProcessCpuTime();
34+
public long getFreePhysicalMemorySize();
35+
public long getTotalPhysicalMemorySize();
36+
}
37+
38+
// Interface with a signature equivalent to com.sun.management.UnixOperatingSystemMXBean and
39+
// com.ibm.lang.management.UnixOperatingSystemMXBean, without explicitly depending on either of
40+
// these classes, used to test reflection.
41+
interface UnixOperatingSystemMXBean extends GenericOperatingSystemMXBean {
42+
public long getOpenFileDescriptorCount();
43+
public long getMaxFileDescriptorCount();
44+
}
45+
2646
UnixOperatingSystemMXBean osBean;
2747
RuntimeMXBean runtimeBean;
2848

2949
@Before
3050
public void setUp() {
31-
osBean = Mockito.mock(UnixOperatingSystemMXBean.class);
51+
osBean = mock(UnixOperatingSystemMXBean.class);
3252
when(osBean.getName()).thenReturn("Linux");
3353
when(osBean.getProcessCpuTime()).thenReturn(123L);
3454
when(osBean.getOpenFileDescriptorCount()).thenReturn(10L);
3555
when(osBean.getMaxFileDescriptorCount()).thenReturn(20L);
36-
runtimeBean = Mockito.mock(RuntimeMXBean.class);
56+
runtimeBean = mock(RuntimeMXBean.class);
3757
when(runtimeBean.getStartTime()).thenReturn(456L);
3858
}
3959

@@ -56,6 +76,21 @@ public void testStandardExports() {
5676
registry.getSampleValue("process_resident_memory_bytes", new String[]{}, new String[]{}), .001);
5777
}
5878

79+
@Test
80+
public void testNonUnixStandardExports() {
81+
GenericOperatingSystemMXBean genericOsBean = mock(GenericOperatingSystemMXBean.class);
82+
when(genericOsBean.getName()).thenReturn("Windows");
83+
when(genericOsBean.getProcessCpuTime()).thenReturn(123L);
84+
85+
CollectorRegistry registry = new CollectorRegistry();
86+
new StandardExports(new StatusReaderTest(), genericOsBean, runtimeBean).register(registry);
87+
88+
assertEquals(123 / 1.0E9,
89+
registry.getSampleValue("process_cpu_seconds_total", new String[]{}, new String[]{}), .0000001);
90+
assertEquals(456 / 1.0E3,
91+
registry.getSampleValue("process_start_time_seconds", new String[]{}, new String[]{}), .0000001);
92+
}
93+
5994
@Test
6095
public void testBrokenProcStatusReturnsOtherStats() {
6196
class StatusReaderBroken extends StandardExports.StatusReader {

0 commit comments

Comments
 (0)