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