001package io.prometheus.jmx;
002
003import java.io.IOException;
004import java.lang.management.ManagementFactory;
005import java.util.HashMap;
006import java.util.HashSet;
007import java.util.LinkedHashMap;
008import java.util.LinkedList;
009import java.util.List;
010import java.util.Map;
011import java.util.Set;
012import java.util.TreeSet;
013import java.util.logging.Level;
014import java.util.logging.Logger;
015import java.util.regex.Matcher;
016import java.util.regex.Pattern;
017import javax.management.JMException;
018import javax.management.MBeanAttributeInfo;
019import javax.management.MBeanInfo;
020import javax.management.MBeanServerConnection;
021import javax.management.ObjectInstance;
022import javax.management.ObjectName;
023import javax.management.openmbean.CompositeData;
024import javax.management.openmbean.CompositeType;
025import javax.management.openmbean.TabularData;
026import javax.management.openmbean.TabularType;
027import javax.management.remote.JMXConnector;
028import javax.management.remote.JMXConnectorFactory;
029import javax.management.remote.JMXServiceURL;
030import javax.management.remote.rmi.RMIConnectorServer;
031import javax.naming.Context;
032import javax.rmi.ssl.SslRMIClientSocketFactory;
033
034
035public class JmxScraper {
036    private static final Logger logger = Logger.getLogger(JmxScraper.class.getName());;
037    private static final Pattern PROPERTY_PATTERN = Pattern.compile(
038            "([^,=:\\*\\?]+)" + // Name - non-empty, anything but comma, equals, colon, star, or question mark
039            "=" +  // Equals
040            "(" + // Either
041                "\"" + // Quoted
042                    "(?:" + // A possibly empty sequence of
043                    "[^\\\\\"]" + // Anything but backslash or quote
044                    "|\\\\\\\\" + // or an escaped backslash
045                    "|\\\\n" + // or an escaped newline
046                    "|\\\\\"" + // or an escaped quote
047                    "|\\\\\\?" + // or an escaped question mark
048                    "|\\\\\\*" + // or an escaped star
049                    ")*" +
050                "\"" +
051            "|" + // Or
052                "[^,=:\"]*" + // Unquoted - can be empty, anything but comma, equals, colon, or quote
053            ")");
054
055    public static interface MBeanReceiver {
056        void recordBean(
057            String domain,
058            LinkedHashMap<String, String> beanProperties,
059            LinkedList<String> attrKeys,
060            String attrName,
061            String attrType,
062            String attrDescription,
063            Object value);
064    }
065
066    private MBeanReceiver receiver;
067    private String jmxUrl;
068    private String username;
069    private String password;
070    private boolean ssl;
071    private List<ObjectName> whitelistObjectNames, blacklistObjectNames;
072
073    public JmxScraper(String jmxUrl, String username, String password, boolean ssl, List<ObjectName> whitelistObjectNames, List<ObjectName> blacklistObjectNames, MBeanReceiver receiver) {
074        this.jmxUrl = jmxUrl;
075        this.receiver = receiver;
076        this.username = username;
077        this.password = password;
078        this.ssl = ssl;
079        this.whitelistObjectNames = whitelistObjectNames;
080        this.blacklistObjectNames = blacklistObjectNames;
081    }
082
083    /**
084      * Get a list of mbeans on host_port and scrape their values.
085      *
086      * Values are passed to the receiver in a single thread.
087      */
088    public void doScrape() throws Exception {
089        MBeanServerConnection beanConn;
090        JMXConnector jmxc = null;
091        if (jmxUrl.isEmpty()) {
092          beanConn = ManagementFactory.getPlatformMBeanServer();
093        } else {
094          Map<String, Object> environment = new HashMap<String, Object>();
095          if (username != null && username.length() != 0 && password != null && password.length() != 0) {
096            String[] credent = new String[] {username, password};
097            environment.put(javax.management.remote.JMXConnector.CREDENTIALS, credent);
098          }
099          if (ssl) {
100              environment.put(Context.SECURITY_PROTOCOL, "ssl");
101              SslRMIClientSocketFactory clientSocketFactory = new SslRMIClientSocketFactory();
102              environment.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientSocketFactory);
103              environment.put("com.sun.jndi.rmi.factory.socket", clientSocketFactory);
104          }
105
106          jmxc = JMXConnectorFactory.connect(new JMXServiceURL(jmxUrl), environment);
107          beanConn = jmxc.getMBeanServerConnection();
108        }
109        try {
110            // Query MBean names, see #89 for reasons queryMBeans() is used instead of queryNames()
111            Set<ObjectInstance> mBeanNames = new HashSet();
112            for (ObjectName name : whitelistObjectNames) {
113                mBeanNames.addAll(beanConn.queryMBeans(name, null));
114            }
115            for (ObjectName name : blacklistObjectNames) {
116                mBeanNames.removeAll(beanConn.queryMBeans(name, null));
117            }
118            for (ObjectInstance name : mBeanNames) {
119                long start = System.nanoTime();
120                scrapeBean(beanConn, name.getObjectName());
121                logger.fine("TIME: " + (System.nanoTime() - start) + " ns for " + name.getObjectName().toString());
122            }
123        } finally {
124          if (jmxc != null) {
125            jmxc.close();
126          }
127        }
128    }
129
130    private void scrapeBean(MBeanServerConnection beanConn, ObjectName mbeanName) {
131        MBeanInfo info;
132        try {
133          info = beanConn.getMBeanInfo(mbeanName);
134        } catch (IOException e) {
135          logScrape(mbeanName.toString(), "getMBeanInfo Fail: " + e);
136          return;
137        } catch (JMException e) {
138          logScrape(mbeanName.toString(), "getMBeanInfo Fail: " + e);
139          return;
140        }
141        MBeanAttributeInfo[] attrInfos = info.getAttributes();
142
143        for (int idx = 0; idx < attrInfos.length; ++idx) {
144            MBeanAttributeInfo attr = attrInfos[idx];
145            if (!attr.isReadable()) {
146                logScrape(mbeanName, attr, "not readable");
147                continue;
148            }
149
150            Object value;
151            try {
152                value = beanConn.getAttribute(mbeanName, attr.getName());
153            } catch(Exception e) {
154                logScrape(mbeanName, attr, "Fail: " + e);
155                continue;
156            }
157
158            logScrape(mbeanName, attr, "process");
159            processBeanValue(
160                    mbeanName.getDomain(),
161                    getKeyPropertyList(mbeanName),
162                    new LinkedList<String>(),
163                    attr.getName(),
164                    attr.getType(),
165                    attr.getDescription(),
166                    value
167                    );
168        }
169    }
170
171    static LinkedHashMap<String, String> getKeyPropertyList(ObjectName mbeanName) {
172        // Implement a version of ObjectName.getKeyPropertyList that returns the
173        // properties in the ordered they were added (the ObjectName stores them
174        // in the order they were added).
175        LinkedHashMap<String, String> output = new LinkedHashMap<String, String>();
176        String properties = mbeanName.getKeyPropertyListString();
177        Matcher match = PROPERTY_PATTERN.matcher(properties);
178        while (match.lookingAt()) {
179            output.put(match.group(1), match.group(2));
180            properties = properties.substring(match.end());
181            if (properties.startsWith(",")) {
182                properties = properties.substring(1);
183            }
184            match.reset(properties);
185        }
186        return output;
187    }
188
189    /**
190     * Recursive function for exporting the values of an mBean.
191     * JMX is a very open technology, without any prescribed way of declaring mBeans
192     * so this function tries to do a best-effort pass of getting the values/names
193     * out in a way it can be processed elsewhere easily.
194     */
195    private void processBeanValue(
196            String domain,
197            LinkedHashMap<String, String> beanProperties,
198            LinkedList<String> attrKeys,
199            String attrName,
200            String attrType,
201            String attrDescription,
202            Object value) {
203        if (value == null) {
204            logScrape(domain + beanProperties + attrName, "null");
205        } else if (value instanceof Number || value instanceof String || value instanceof Boolean) {
206            logScrape(domain + beanProperties + attrName, value.toString());
207            this.receiver.recordBean(
208                    domain,
209                    beanProperties,
210                    attrKeys,
211                    attrName,
212                    attrType,
213                    attrDescription,
214                    value);
215        } else if (value instanceof CompositeData) {
216            logScrape(domain + beanProperties + attrName, "compositedata");
217            CompositeData composite = (CompositeData) value;
218            CompositeType type = composite.getCompositeType();
219            attrKeys = new LinkedList<String>(attrKeys);
220            attrKeys.add(attrName);
221            for(String key : type.keySet()) {
222                String typ = type.getType(key).getTypeName();
223                Object valu = composite.get(key);
224                processBeanValue(
225                        domain,
226                        beanProperties,
227                        attrKeys,
228                        key,
229                        typ,
230                        type.getDescription(),
231                        valu);
232            }
233        } else if (value instanceof TabularData) {
234            // I don't pretend to have a good understanding of TabularData.
235            // The real world usage doesn't appear to match how they were
236            // meant to be used according to the docs. I've only seen them
237            // used as 'key' 'value' pairs even when 'value' is itself a
238            // CompositeData of multiple values.
239            logScrape(domain + beanProperties + attrName, "tabulardata");
240            TabularData tds = (TabularData) value;
241            TabularType tt = tds.getTabularType();
242
243            List<String> rowKeys = tt.getIndexNames();
244            LinkedHashMap<String, String> l2s = new LinkedHashMap<String, String>(beanProperties);
245
246            CompositeType type = tt.getRowType();
247            Set<String> valueKeys = new TreeSet<String>(type.keySet());
248            valueKeys.removeAll(rowKeys);
249
250            LinkedList<String> extendedAttrKeys = new LinkedList<String>(attrKeys);
251            extendedAttrKeys.add(attrName);
252            for (Object valu : tds.values()) {
253                if (valu instanceof CompositeData) {
254                    CompositeData composite = (CompositeData) valu;
255                    for (String idx : rowKeys) {
256                        l2s.put(idx, composite.get(idx).toString());
257                    }
258                    for(String valueIdx : valueKeys) {
259                        LinkedList<String> attrNames = extendedAttrKeys;
260                        String typ = type.getType(valueIdx).getTypeName();
261                        String name = valueIdx;
262                        if (valueIdx.toLowerCase().equals("value")) {
263                            // Skip appending 'value' to the name
264                            attrNames = attrKeys;
265                            name = attrName;
266                        } 
267                        processBeanValue(
268                            domain,
269                            l2s,
270                            attrNames,
271                            name,
272                            typ,
273                            type.getDescription(),
274                            composite.get(valueIdx));
275                    }
276                } else {
277                    logScrape(domain, "not a correct tabulardata format");
278                }
279            }
280        } else if (value.getClass().isArray()) {
281            logScrape(domain, "arrays are unsupported");
282        } else {
283            logScrape(domain + beanProperties, attrType + " is not exported");
284        }
285    }
286
287    /**
288     * For debugging.
289     */
290    private static void logScrape(ObjectName mbeanName, MBeanAttributeInfo attr, String msg) {
291        logScrape(mbeanName + "'_'" + attr.getName(), msg);
292    }
293    private static void logScrape(String name, String msg) {
294        logger.log(Level.FINE, "scrape: '" + name + "': " + msg);
295    }
296
297    private static class StdoutWriter implements MBeanReceiver {
298        public void recordBean(
299            String domain,
300            LinkedHashMap<String, String> beanProperties,
301            LinkedList<String> attrKeys,
302            String attrName,
303            String attrType,
304            String attrDescription,
305            Object value) {
306            System.out.println(domain +
307                               beanProperties + 
308                               attrKeys +
309                               attrName +
310                               ": " + value);
311        }
312    }
313
314    /**
315     * Convenience function to run standalone.
316     */
317    public static void main(String[] args) throws Exception {
318      List<ObjectName> objectNames = new LinkedList<ObjectName>();
319      objectNames.add(null);
320      if (args.length >= 3){
321            new JmxScraper(args[0], args[1], args[2], false, objectNames, new LinkedList<ObjectName>(), new StdoutWriter()).doScrape();
322        }
323      else if (args.length > 0){
324          new JmxScraper(args[0], "", "", false, objectNames, new LinkedList<ObjectName>(), new StdoutWriter()).doScrape();
325      }
326      else {
327          new JmxScraper("", "", "", false, objectNames, new LinkedList<ObjectName>(), new StdoutWriter()).doScrape();
328      }
329    }
330}
331