001package io.prometheus.jmx; 002 003import io.prometheus.client.Collector; 004import io.prometheus.client.Counter; 005import java.io.IOException; 006import java.io.PrintWriter; 007import java.io.File; 008import java.io.FileReader; 009import java.io.StringWriter; 010import java.util.ArrayList; 011import java.util.Iterator; 012import java.util.LinkedHashMap; 013import java.util.LinkedList; 014import java.util.List; 015import java.util.HashMap; 016import java.util.Map; 017import java.util.Set; 018import java.util.TreeMap; 019import java.util.logging.Logger; 020import java.util.regex.Matcher; 021import java.util.regex.Pattern; 022import javax.management.MalformedObjectNameException; 023import javax.management.ObjectName; 024 025import org.yaml.snakeyaml.Yaml; 026 027import static java.lang.String.format; 028 029public class JmxCollector extends Collector implements Collector.Describable { 030 static final Counter configReloadSuccess = Counter.build() 031 .name("jmx_config_reload_success_total") 032 .help("Number of times configuration have successfully been reloaded.").register(); 033 034 static final Counter configReloadFailure = Counter.build() 035 .name("jmx_config_reload_failure_total") 036 .help("Number of times configuration have failed to be reloaded.").register(); 037 038 private static final Logger LOGGER = Logger.getLogger(JmxCollector.class.getName()); 039 040 private static class Rule { 041 Pattern pattern; 042 String name; 043 String value; 044 Double valueFactor = 1.0; 045 String help; 046 boolean attrNameSnakeCase; 047 Type type = Type.UNTYPED; 048 ArrayList<String> labelNames; 049 ArrayList<String> labelValues; 050 } 051 052 private static class Config { 053 Integer startDelaySeconds = 0; 054 String jmxUrl = ""; 055 String username = ""; 056 String password = ""; 057 boolean ssl = false; 058 boolean lowercaseOutputName; 059 boolean lowercaseOutputLabelNames; 060 List<ObjectName> whitelistObjectNames = new ArrayList<ObjectName>(); 061 List<ObjectName> blacklistObjectNames = new ArrayList<ObjectName>(); 062 ArrayList<Rule> rules = new ArrayList<Rule>(); 063 long lastUpdate = 0L; 064 } 065 066 private Config config; 067 private File configFile; 068 private long createTimeNanoSecs = System.nanoTime(); 069 070 private static final Pattern snakeCasePattern = Pattern.compile("([a-z0-9])([A-Z])"); 071 072 public JmxCollector(File in) throws IOException, MalformedObjectNameException { 073 configFile = in; 074 config = loadConfig((Map<String, Object>)new Yaml().load(new FileReader(in))); 075 config.lastUpdate = configFile.lastModified(); 076 } 077 078 public JmxCollector(String yamlConfig) throws MalformedObjectNameException { 079 config = loadConfig((Map<String, Object>)new Yaml().load(yamlConfig)); 080 } 081 082 private void reloadConfig() { 083 try { 084 FileReader fr = new FileReader(configFile); 085 086 try { 087 Map<String, Object> newYamlConfig = (Map<String, Object>)new Yaml().load(fr); 088 config = loadConfig(newYamlConfig); 089 config.lastUpdate = configFile.lastModified(); 090 configReloadSuccess.inc(); 091 } catch (Exception e) { 092 LOGGER.severe("Configuration reload failed: " + e.toString()); 093 configReloadFailure.inc(); 094 } finally { 095 fr.close(); 096 } 097 098 } catch (IOException e) { 099 LOGGER.severe("Configuration reload failed: " + e.toString()); 100 configReloadFailure.inc(); 101 } 102 } 103 104 private Config loadConfig(Map<String, Object> yamlConfig) throws MalformedObjectNameException { 105 Config cfg = new Config(); 106 107 if (yamlConfig == null) { // Yaml config empty, set config to empty map. 108 yamlConfig = new HashMap<String, Object>(); 109 } 110 111 if (yamlConfig.containsKey("startDelaySeconds")) { 112 try { 113 cfg.startDelaySeconds = (Integer) yamlConfig.get("startDelaySeconds"); 114 } catch (NumberFormatException e) { 115 throw new IllegalArgumentException("Invalid number provided for startDelaySeconds", e); 116 } 117 } 118 if (yamlConfig.containsKey("hostPort")) { 119 if (yamlConfig.containsKey("jmxUrl")) { 120 throw new IllegalArgumentException("At most one of hostPort and jmxUrl must be provided"); 121 } 122 cfg.jmxUrl ="service:jmx:rmi:///jndi/rmi://" + (String)yamlConfig.get("hostPort") + "/jmxrmi"; 123 } else if (yamlConfig.containsKey("jmxUrl")) { 124 cfg.jmxUrl = (String)yamlConfig.get("jmxUrl"); 125 } 126 127 if (yamlConfig.containsKey("username")) { 128 cfg.username = (String)yamlConfig.get("username"); 129 } 130 131 if (yamlConfig.containsKey("password")) { 132 cfg.password = (String)yamlConfig.get("password"); 133 } 134 135 if (yamlConfig.containsKey("ssl")) { 136 cfg.ssl = (Boolean)yamlConfig.get("ssl"); 137 } 138 139 if (yamlConfig.containsKey("lowercaseOutputName")) { 140 cfg.lowercaseOutputName = (Boolean)yamlConfig.get("lowercaseOutputName"); 141 } 142 143 if (yamlConfig.containsKey("lowercaseOutputLabelNames")) { 144 cfg.lowercaseOutputLabelNames = (Boolean)yamlConfig.get("lowercaseOutputLabelNames"); 145 } 146 147 if (yamlConfig.containsKey("whitelistObjectNames")) { 148 List<Object> names = (List<Object>) yamlConfig.get("whitelistObjectNames"); 149 for(Object name : names) { 150 cfg.whitelistObjectNames.add(new ObjectName((String)name)); 151 } 152 } else { 153 cfg.whitelistObjectNames.add(null); 154 } 155 156 if (yamlConfig.containsKey("blacklistObjectNames")) { 157 List<Object> names = (List<Object>) yamlConfig.get("blacklistObjectNames"); 158 for (Object name : names) { 159 cfg.blacklistObjectNames.add(new ObjectName((String)name)); 160 } 161 } 162 163 if (yamlConfig.containsKey("rules")) { 164 List<Map<String,Object>> configRules = (List<Map<String,Object>>) yamlConfig.get("rules"); 165 for (Map<String, Object> ruleObject : configRules) { 166 Map<String, Object> yamlRule = ruleObject; 167 Rule rule = new Rule(); 168 cfg.rules.add(rule); 169 if (yamlRule.containsKey("pattern")) { 170 rule.pattern = Pattern.compile("^.*" + (String)yamlRule.get("pattern") + ".*$"); 171 } 172 if (yamlRule.containsKey("name")) { 173 rule.name = (String)yamlRule.get("name"); 174 } 175 if (yamlRule.containsKey("value")) { 176 rule.value = String.valueOf(yamlRule.get("value")); 177 } 178 if (yamlRule.containsKey("valueFactor")) { 179 String valueFactor = String.valueOf(yamlRule.get("valueFactor")); 180 try { 181 rule.valueFactor = Double.valueOf(valueFactor); 182 } catch (NumberFormatException e) { 183 // use default value 184 } 185 } 186 if (yamlRule.containsKey("attrNameSnakeCase")) { 187 rule.attrNameSnakeCase = (Boolean)yamlRule.get("attrNameSnakeCase"); 188 } 189 if (yamlRule.containsKey("type")) { 190 rule.type = Type.valueOf((String)yamlRule.get("type")); 191 } 192 if (yamlRule.containsKey("help")) { 193 rule.help = (String)yamlRule.get("help"); 194 } 195 if (yamlRule.containsKey("labels")) { 196 TreeMap labels = new TreeMap((Map<String, Object>)yamlRule.get("labels")); 197 rule.labelNames = new ArrayList<String>(); 198 rule.labelValues = new ArrayList<String>(); 199 for (Map.Entry<String, Object> entry : (Set<Map.Entry<String, Object>>)labels.entrySet()) { 200 rule.labelNames.add(entry.getKey()); 201 rule.labelValues.add((String)entry.getValue()); 202 } 203 } 204 205 // Validation. 206 if ((rule.labelNames != null || rule.help != null) && rule.name == null) { 207 throw new IllegalArgumentException("Must provide name, if help or labels are given: " + yamlRule); 208 } 209 if (rule.name != null && rule.pattern == null) { 210 throw new IllegalArgumentException("Must provide pattern, if name is given: " + yamlRule); 211 } 212 } 213 } else { 214 // Default to a single default rule. 215 cfg.rules.add(new Rule()); 216 } 217 218 return cfg; 219 220 } 221 222 class Receiver implements JmxScraper.MBeanReceiver { 223 Map<String, MetricFamilySamples> metricFamilySamplesMap = 224 new HashMap<String, MetricFamilySamples>(); 225 226 private static final char SEP = '_'; 227 228 private final Pattern unsafeChars = Pattern.compile("[^a-zA-Z0-9:_]"); 229 private final Pattern multipleUnderscores = Pattern.compile("__+"); 230 231 // [] and () are special in regexes, so swtich to <>. 232 private String angleBrackets(String s) { 233 return "<" + s.substring(1, s.length() - 1) + ">"; 234 } 235 236 private String safeName(String s) { 237 // Change invalid chars to underscore, and merge underscores. 238 return multipleUnderscores.matcher(unsafeChars.matcher(s).replaceAll("_")).replaceAll("_"); 239 } 240 241 void addSample(MetricFamilySamples.Sample sample, Type type, String help) { 242 MetricFamilySamples mfs = metricFamilySamplesMap.get(sample.name); 243 if (mfs == null) { 244 // JmxScraper.MBeanReceiver is only called from one thread, 245 // so there's no race here. 246 mfs = new MetricFamilySamples(sample.name, type, help, new ArrayList<MetricFamilySamples.Sample>()); 247 metricFamilySamplesMap.put(sample.name, mfs); 248 } 249 mfs.samples.add(sample); 250 } 251 252 private void defaultExport( 253 String domain, 254 LinkedHashMap<String, String> beanProperties, 255 LinkedList<String> attrKeys, 256 String attrName, 257 String attrType, 258 String help, 259 Object value, 260 Type type) { 261 StringBuilder name = new StringBuilder(); 262 name.append(domain); 263 if (beanProperties.size() > 0) { 264 name.append(SEP); 265 name.append(beanProperties.values().iterator().next()); 266 } 267 for (String k : attrKeys) { 268 name.append(SEP); 269 name.append(k); 270 } 271 name.append(SEP); 272 name.append(attrName); 273 String fullname = safeName(name.toString()); 274 275 if (config.lowercaseOutputName) { 276 fullname = fullname.toLowerCase(); 277 } 278 279 List<String> labelNames = new ArrayList<String>(); 280 List<String> labelValues = new ArrayList<String>(); 281 if (beanProperties.size() > 1) { 282 Iterator<Map.Entry<String, String>> iter = beanProperties.entrySet().iterator(); 283 // Skip the first one, it's been used in the name. 284 iter.next(); 285 while (iter.hasNext()) { 286 Map.Entry<String, String> entry = iter.next(); 287 String labelName = safeName(entry.getKey()); 288 if (config.lowercaseOutputLabelNames) { 289 labelName = labelName.toLowerCase(); 290 } 291 labelNames.add(labelName); 292 labelValues.add(entry.getValue()); 293 } 294 } 295 296 addSample(new MetricFamilySamples.Sample(fullname, labelNames, labelValues, ((Number)value).doubleValue()), 297 type, help); 298 } 299 300 public void recordBean( 301 String domain, 302 LinkedHashMap<String, String> beanProperties, 303 LinkedList<String> attrKeys, 304 String attrName, 305 String attrType, 306 String attrDescription, 307 Object beanValue) { 308 309 String beanName = domain + angleBrackets(beanProperties.toString()) + angleBrackets(attrKeys.toString()); 310 // attrDescription tends not to be useful, so give the fully qualified name too. 311 String help = attrDescription + " (" + beanName + attrName + ")"; 312 String attrNameSnakeCase = snakeCasePattern.matcher(attrName).replaceAll("$1_$2").toLowerCase(); 313 314 for (Rule rule : config.rules) { 315 Matcher matcher = null; 316 String matchName = beanName + (rule.attrNameSnakeCase ? attrNameSnakeCase : attrName); 317 if (rule.pattern != null) { 318 matcher = rule.pattern.matcher(matchName + ": " + beanValue); 319 if (!matcher.matches()) { 320 continue; 321 } 322 } 323 324 Number value; 325 if (rule.value != null && !rule.value.isEmpty()) { 326 String val = matcher.replaceAll(rule.value); 327 328 try { 329 beanValue = Double.valueOf(val); 330 } catch (NumberFormatException e) { 331 LOGGER.fine("Unable to parse configured value '" + val + "' to number for bean: " + beanName + attrName + ": " + beanValue); 332 return; 333 } 334 } 335 if (beanValue instanceof Number) { 336 value = ((Number)beanValue).doubleValue() * rule.valueFactor; 337 } else if (beanValue instanceof Boolean) { 338 value = (Boolean)beanValue ? 1 : 0; 339 } else { 340 LOGGER.fine("Ignoring unsupported bean: " + beanName + attrName + ": " + beanValue); 341 return; 342 } 343 344 // If there's no name provided, use default export format. 345 if (rule.name == null) { 346 defaultExport(domain, beanProperties, attrKeys, rule.attrNameSnakeCase ? attrNameSnakeCase : attrName, attrType, help, value, rule.type); 347 return; 348 } 349 350 // Matcher is set below here due to validation in the constructor. 351 String name = safeName(matcher.replaceAll(rule.name)); 352 if (name.isEmpty()) { 353 return; 354 } 355 if (config.lowercaseOutputName) { 356 name = name.toLowerCase(); 357 } 358 359 // Set the help. 360 if (rule.help != null) { 361 help = matcher.replaceAll(rule.help); 362 } 363 364 // Set the labels. 365 ArrayList<String> labelNames = new ArrayList<String>(); 366 ArrayList<String> labelValues = new ArrayList<String>(); 367 if (rule.labelNames != null) { 368 for (int i = 0; i < rule.labelNames.size(); i++) { 369 final String unsafeLabelName = rule.labelNames.get(i); 370 final String labelValReplacement = rule.labelValues.get(i); 371 try { 372 String labelName = safeName(matcher.replaceAll(unsafeLabelName)); 373 String labelValue = matcher.replaceAll(labelValReplacement); 374 if (config.lowercaseOutputLabelNames) { 375 labelName = labelName.toLowerCase(); 376 } 377 if (!labelName.isEmpty() && !labelValue.isEmpty()) { 378 labelNames.add(labelName); 379 labelValues.add(labelValue); 380 } 381 } catch (Exception e) { 382 throw new RuntimeException( 383 format("Matcher '%s' unable to use: '%s' value: '%s'", matcher, unsafeLabelName, labelValReplacement), e); 384 } 385 } 386 } 387 388 // Add to samples. 389 LOGGER.fine("add metric sample: " + name + " " + labelNames + " " + labelValues + " " + value.doubleValue()); 390 addSample(new MetricFamilySamples.Sample(name, labelNames, labelValues, value.doubleValue()), rule.type, help); 391 return; 392 } 393 } 394 395 } 396 397 public List<MetricFamilySamples> collect() { 398 if (configFile != null) { 399 long mtime = configFile.lastModified(); 400 if (mtime > config.lastUpdate) { 401 LOGGER.fine("Configuration file changed, reloading..."); 402 reloadConfig(); 403 } 404 } 405 406 Receiver receiver = new Receiver(); 407 JmxScraper scraper = new JmxScraper(config.jmxUrl, config.username, config.password, config.ssl, config.whitelistObjectNames, config.blacklistObjectNames, receiver); 408 long start = System.nanoTime(); 409 double error = 0; 410 if ((config.startDelaySeconds > 0) && 411 ((start - createTimeNanoSecs) / 1000000000L < config.startDelaySeconds)) { 412 throw new IllegalStateException("JMXCollector waiting for startDelaySeconds"); 413 } 414 try { 415 scraper.doScrape(); 416 } catch (Exception e) { 417 error = 1; 418 StringWriter sw = new StringWriter(); 419 e.printStackTrace(new PrintWriter(sw)); 420 LOGGER.severe("JMX scrape failed: " + sw.toString()); 421 } 422 List<MetricFamilySamples> mfsList = new ArrayList<MetricFamilySamples>(); 423 mfsList.addAll(receiver.metricFamilySamplesMap.values()); 424 List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>(); 425 samples.add(new MetricFamilySamples.Sample( 426 "jmx_scrape_duration_seconds", new ArrayList<String>(), new ArrayList<String>(), (System.nanoTime() - start) / 1.0E9)); 427 mfsList.add(new MetricFamilySamples("jmx_scrape_duration_seconds", Type.GAUGE, "Time this JMX scrape took, in seconds.", samples)); 428 429 samples = new ArrayList<MetricFamilySamples.Sample>(); 430 samples.add(new MetricFamilySamples.Sample( 431 "jmx_scrape_error", new ArrayList<String>(), new ArrayList<String>(), error)); 432 mfsList.add(new MetricFamilySamples("jmx_scrape_error", Type.GAUGE, "Non-zero if this scrape failed.", samples)); 433 return mfsList; 434 } 435 436 public List<MetricFamilySamples> describe() { 437 List<MetricFamilySamples> sampleFamilies = new ArrayList<MetricFamilySamples>(); 438 sampleFamilies.add(new MetricFamilySamples("jmx_scrape_duration_seconds", Type.GAUGE, "Time this JMX scrape took, in seconds.", new ArrayList<MetricFamilySamples.Sample>())); 439 sampleFamilies.add(new MetricFamilySamples("jmx_scrape_error", Type.GAUGE, "Non-zero if this scrape failed.", new ArrayList<MetricFamilySamples.Sample>())); 440 return sampleFamilies; 441 } 442 443 /** 444 * Convenience function to run standalone. 445 */ 446 public static void main(String[] args) throws Exception { 447 String hostPort = ""; 448 if (args.length > 0) { 449 hostPort = args[0]; 450 } 451 JmxCollector jc = new JmxCollector(("{" 452 + "`hostPort`: `" + hostPort + "`," 453 + "}").replace('`', '"')); 454 for(MetricFamilySamples mfs : jc.collect()) { 455 System.out.println(mfs); 456 } 457 } 458}