001package io.prometheus.jmx;
002
003import io.prometheus.client.Collector;
004import io.prometheus.client.Counter;
005import org.yaml.snakeyaml.Yaml;
006
007import javax.management.MalformedObjectNameException;
008import javax.management.ObjectName;
009import java.io.File;
010import java.io.FileReader;
011import java.io.IOException;
012import java.io.InputStream;
013import java.io.PrintWriter;
014import java.io.StringWriter;
015import java.util.ArrayList;
016import java.util.HashMap;
017import java.util.HashSet;
018import java.util.Iterator;
019import java.util.LinkedHashMap;
020import java.util.LinkedList;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024import java.util.TreeMap;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030import static java.lang.String.format;
031
032public class JmxCollector extends Collector implements Collector.Describable {
033
034    public enum Mode {
035      AGENT,
036      STANDALONE
037    }
038
039    private final Mode mode;
040
041    static final Counter configReloadSuccess = Counter.build()
042      .name("jmx_config_reload_success_total")
043      .help("Number of times configuration have successfully been reloaded.").register();
044
045    static final Counter configReloadFailure = Counter.build()
046      .name("jmx_config_reload_failure_total")
047      .help("Number of times configuration have failed to be reloaded.").register();
048
049    private static final Logger LOGGER = Logger.getLogger(JmxCollector.class.getName());
050
051    static class Rule {
052      Pattern pattern;
053      String name;
054      String value;
055      Double valueFactor = 1.0;
056      String help;
057      boolean attrNameSnakeCase;
058      boolean cache = false;
059      Type type = Type.UNKNOWN;
060      ArrayList<String> labelNames;
061      ArrayList<String> labelValues;
062    }
063
064    private static class Config {
065      Integer startDelaySeconds = 0;
066      String jmxUrl = "";
067      String username = "";
068      String password = "";
069      boolean ssl = false;
070      boolean lowercaseOutputName;
071      boolean lowercaseOutputLabelNames;
072      List<ObjectName> whitelistObjectNames = new ArrayList<ObjectName>();
073      List<ObjectName> blacklistObjectNames = new ArrayList<ObjectName>();
074      List<Rule> rules = new ArrayList<Rule>();
075      long lastUpdate = 0L;
076
077      MatchedRulesCache rulesCache;
078    }
079
080    private Config config;
081    private File configFile;
082    private long createTimeNanoSecs = System.nanoTime();
083
084    private final JmxMBeanPropertyCache jmxMBeanPropertyCache = new JmxMBeanPropertyCache();
085
086    public JmxCollector(File in) throws IOException, MalformedObjectNameException {
087        this(in, null);
088    }
089
090    public JmxCollector(File in, Mode mode) throws IOException, MalformedObjectNameException {
091        configFile = in;
092        this.mode = mode;
093        config = loadConfig((Map<String, Object>)new Yaml().load(new FileReader(in)));
094        config.lastUpdate = configFile.lastModified();
095        exitOnConfigError();
096    }
097
098    public JmxCollector(String yamlConfig) throws MalformedObjectNameException {
099        config = loadConfig((Map<String, Object>)new Yaml().load(yamlConfig));
100        mode = null;
101    }
102
103    public JmxCollector(InputStream inputStream) throws MalformedObjectNameException {
104        config = loadConfig((Map<String, Object>)new Yaml().load(inputStream));
105        mode = null;
106    }
107
108    private void exitOnConfigError() {
109        if (mode == Mode.AGENT && !config.jmxUrl.isEmpty()) {
110            LOGGER.severe("Configuration error: When running jmx_exporter as a Java agent, you must not configure 'jmxUrl' or 'hostPort' because you don't want to monitor a remote JVM.");
111            System.exit(-1);
112        }
113        if (mode == Mode.STANDALONE && config.jmxUrl.isEmpty()) {
114            LOGGER.severe("Configuration error: When running jmx_exporter in standalone mode (using jmx_prometheus_httpserver-*.jar) you must configure 'jmxUrl' or 'hostPort'.");
115            System.exit(-1);
116        }
117    }
118
119    private void reloadConfig() {
120      try {
121        FileReader fr = new FileReader(configFile);
122
123        try {
124          Map<String, Object> newYamlConfig = (Map<String, Object>)new Yaml().load(fr);
125          config = loadConfig(newYamlConfig);
126          config.lastUpdate = configFile.lastModified();
127          configReloadSuccess.inc();
128        } catch (Exception e) {
129          LOGGER.severe("Configuration reload failed: " + e.toString());
130          configReloadFailure.inc();
131        } finally {
132          fr.close();
133        }
134
135      } catch (IOException e) {
136        LOGGER.severe("Configuration reload failed: " + e.toString());
137        configReloadFailure.inc();
138      }
139    }
140
141    private synchronized Config getLatestConfig() {
142      if (configFile != null) {
143          long mtime = configFile.lastModified();
144          if (mtime > config.lastUpdate) {
145              LOGGER.fine("Configuration file changed, reloading...");
146              reloadConfig();
147          }
148      }
149      exitOnConfigError();
150      return config;
151    }
152
153  private Config loadConfig(Map<String, Object> yamlConfig) throws MalformedObjectNameException {
154        Config cfg = new Config();
155
156        if (yamlConfig == null) {  // Yaml config empty, set config to empty map.
157          yamlConfig = new HashMap<String, Object>();
158        }
159
160        if (yamlConfig.containsKey("startDelaySeconds")) {
161          try {
162            cfg.startDelaySeconds = (Integer) yamlConfig.get("startDelaySeconds");
163          } catch (NumberFormatException e) {
164            throw new IllegalArgumentException("Invalid number provided for startDelaySeconds", e);
165          }
166        }
167        if (yamlConfig.containsKey("hostPort")) {
168          if (yamlConfig.containsKey("jmxUrl")) {
169            throw new IllegalArgumentException("At most one of hostPort and jmxUrl must be provided");
170          }
171          cfg.jmxUrl ="service:jmx:rmi:///jndi/rmi://" + (String)yamlConfig.get("hostPort") + "/jmxrmi";
172        } else if (yamlConfig.containsKey("jmxUrl")) {
173          cfg.jmxUrl = (String)yamlConfig.get("jmxUrl");
174        }
175
176        if (yamlConfig.containsKey("username")) {
177          cfg.username = (String)yamlConfig.get("username");
178        }
179
180        if (yamlConfig.containsKey("password")) {
181          cfg.password = (String)yamlConfig.get("password");
182        }
183
184        if (yamlConfig.containsKey("ssl")) {
185          cfg.ssl = (Boolean)yamlConfig.get("ssl");
186        }
187
188        if (yamlConfig.containsKey("lowercaseOutputName")) {
189          cfg.lowercaseOutputName = (Boolean)yamlConfig.get("lowercaseOutputName");
190        }
191
192        if (yamlConfig.containsKey("lowercaseOutputLabelNames")) {
193          cfg.lowercaseOutputLabelNames = (Boolean)yamlConfig.get("lowercaseOutputLabelNames");
194        }
195
196        if (yamlConfig.containsKey("whitelistObjectNames")) {
197          List<Object> names = (List<Object>) yamlConfig.get("whitelistObjectNames");
198          for(Object name : names) {
199            cfg.whitelistObjectNames.add(new ObjectName((String)name));
200          }
201        } else {
202          cfg.whitelistObjectNames.add(null);
203        }
204
205        if (yamlConfig.containsKey("blacklistObjectNames")) {
206          List<Object> names = (List<Object>) yamlConfig.get("blacklistObjectNames");
207          for (Object name : names) {
208            cfg.blacklistObjectNames.add(new ObjectName((String)name));
209          }
210        }
211
212      if (yamlConfig.containsKey("rules")) {
213          List<Map<String,Object>> configRules = (List<Map<String,Object>>) yamlConfig.get("rules");
214          for (Map<String, Object> ruleObject : configRules) {
215            Map<String, Object> yamlRule = ruleObject;
216            Rule rule = new Rule();
217            cfg.rules.add(rule);
218            if (yamlRule.containsKey("pattern")) {
219              rule.pattern = Pattern.compile("^.*(?:" + (String)yamlRule.get("pattern") + ").*$");
220            }
221            if (yamlRule.containsKey("name")) {
222              rule.name = (String)yamlRule.get("name");
223            }
224            if (yamlRule.containsKey("value")) {
225              rule.value = String.valueOf(yamlRule.get("value"));
226            }
227            if (yamlRule.containsKey("valueFactor")) {
228              String valueFactor = String.valueOf(yamlRule.get("valueFactor"));
229              try {
230                rule.valueFactor = Double.valueOf(valueFactor);
231              } catch (NumberFormatException e) {
232                // use default value
233              }
234            }
235            if (yamlRule.containsKey("attrNameSnakeCase")) {
236              rule.attrNameSnakeCase = (Boolean)yamlRule.get("attrNameSnakeCase");
237            }
238            if (yamlRule.containsKey("cache")) {
239              rule.cache = (Boolean)yamlRule.get("cache");
240            }
241            if (yamlRule.containsKey("type")) {
242              String t = (String)yamlRule.get("type");
243              // Gracefully handle switch to OM data model.
244              if ("UNTYPED".equals(t)) {
245                t = "UNKNOWN";
246              }
247              rule.type = Type.valueOf(t);
248            }
249            if (yamlRule.containsKey("help")) {
250              rule.help = (String)yamlRule.get("help");
251            }
252            if (yamlRule.containsKey("labels")) {
253              TreeMap labels = new TreeMap((Map<String, Object>)yamlRule.get("labels"));
254              rule.labelNames = new ArrayList<String>();
255              rule.labelValues = new ArrayList<String>();
256              for (Map.Entry<String, Object> entry : (Set<Map.Entry<String, Object>>)labels.entrySet()) {
257                rule.labelNames.add(entry.getKey());
258                rule.labelValues.add((String)entry.getValue());
259              }
260            }
261
262            // Validation.
263            if ((rule.labelNames != null || rule.help != null) && rule.name == null) {
264              throw new IllegalArgumentException("Must provide name, if help or labels are given: " + yamlRule);
265            }
266            if (rule.name != null && rule.pattern == null) {
267              throw new IllegalArgumentException("Must provide pattern, if name is given: " + yamlRule);
268            }
269          }
270        } else {
271          // Default to a single default rule.
272          cfg.rules.add(new Rule());
273        }
274
275        cfg.rulesCache = new MatchedRulesCache(cfg.rules);
276
277        return cfg;
278
279    }
280
281    static String toSnakeAndLowerCase(String attrName) {
282      if (attrName == null || attrName.isEmpty()) {
283        return attrName;
284      }
285      char firstChar = attrName.subSequence(0, 1).charAt(0);
286      boolean prevCharIsUpperCaseOrUnderscore = Character.isUpperCase(firstChar) || firstChar == '_';
287      StringBuilder resultBuilder = new StringBuilder(attrName.length()).append(Character.toLowerCase(firstChar));
288      for (char attrChar : attrName.substring(1).toCharArray()) {
289        boolean charIsUpperCase = Character.isUpperCase(attrChar);
290        if (!prevCharIsUpperCaseOrUnderscore && charIsUpperCase) {
291          resultBuilder.append("_");
292        }
293        resultBuilder.append(Character.toLowerCase(attrChar));
294        prevCharIsUpperCaseOrUnderscore = charIsUpperCase || attrChar == '_';
295      }
296      return resultBuilder.toString();
297    }
298
299  /**
300   * Change invalid chars to underscore, and merge underscores.
301   * @param name Input string
302   * @return
303   */
304  static String safeName(String name) {
305      if (name == null) {
306        return null;
307      }
308      boolean prevCharIsUnderscore = false;
309      StringBuilder safeNameBuilder = new StringBuilder(name.length());
310      if (!name.isEmpty() && Character.isDigit(name.charAt(0))) {
311        // prevent a numeric prefix.
312        safeNameBuilder.append("_");
313      }
314      for (char nameChar : name.toCharArray()) {
315        boolean isUnsafeChar = !JmxCollector.isLegalCharacter(nameChar);
316        if ((isUnsafeChar || nameChar == '_')) {
317          if (prevCharIsUnderscore) {
318            continue;
319          } else {
320            safeNameBuilder.append("_");
321            prevCharIsUnderscore = true;
322          }
323        } else {
324          safeNameBuilder.append(nameChar);
325          prevCharIsUnderscore = false;
326        }
327      }
328
329      return safeNameBuilder.toString();
330    }
331
332  private static boolean isLegalCharacter(char input) {
333    return ((input == ':') ||
334            (input == '_') ||
335            (input >= 'a' && input <= 'z') ||
336            (input >= 'A' && input <= 'Z') ||
337            (input >= '0' && input <= '9'));
338  }
339
340    /**
341     * A sample is uniquely identified by its name, labelNames and labelValues
342     */
343    private static class SampleKey {
344      private final String name;
345      private final List<String> labelNames;
346      private final List<String> labelValues;
347
348      private SampleKey(String name, List<String> labelNames, List<String> labelValues) {
349        this.name = name;
350        this.labelNames = labelNames;
351        this.labelValues = labelValues;
352      }
353
354      private static SampleKey of(MetricFamilySamples.Sample sample) {
355        return new SampleKey(sample.name, sample.labelNames, sample.labelValues);
356      }
357
358      @Override
359      public boolean equals(Object o) {
360        if (this == o) return true;
361        if (o == null || getClass() != o.getClass()) return false;
362
363        SampleKey sampleKey = (SampleKey) o;
364
365        if (name != null ? !name.equals(sampleKey.name) : sampleKey.name != null) return false;
366        if (labelValues != null ? !labelValues.equals(sampleKey.labelValues) : sampleKey.labelValues != null) return false;
367        return labelNames != null ? labelNames.equals(sampleKey.labelNames) : sampleKey.labelNames == null;
368      }
369
370      @Override
371      public int hashCode() {
372        int result = name != null ? name.hashCode() : 0;
373        result = 31 * result + (labelNames != null ? labelNames.hashCode() : 0);
374        result = 31 * result + (labelValues != null ? labelValues.hashCode() : 0);
375        return result;
376      }
377
378    }
379
380    static class Receiver implements JmxScraper.MBeanReceiver {
381      Map<String, MetricFamilySamples> metricFamilySamplesMap =
382        new HashMap<String, MetricFamilySamples>();
383      Set<SampleKey> sampleKeys = new HashSet<SampleKey>();
384
385      Config config;
386      MatchedRulesCache.StalenessTracker stalenessTracker;
387
388      private static final char SEP = '_';
389
390      Receiver(Config config, MatchedRulesCache.StalenessTracker stalenessTracker) {
391        this.config = config;
392        this.stalenessTracker = stalenessTracker;
393      }
394
395      // [] and () are special in regexes, so swtich to <>.
396      private String angleBrackets(String s) {
397        return "<" + s.substring(1, s.length() - 1) + ">";
398      }
399
400      void addSample(MetricFamilySamples.Sample sample, Type type, String help) {
401        MetricFamilySamples mfs = metricFamilySamplesMap.get(sample.name);
402        if (mfs == null) {
403          // JmxScraper.MBeanReceiver is only called from one thread,
404          // so there's no race here.
405          mfs = new MetricFamilySamples(sample.name, type, help, new ArrayList<MetricFamilySamples.Sample>());
406          metricFamilySamplesMap.put(sample.name, mfs);
407        }
408        SampleKey sampleKey = SampleKey.of(sample);
409        boolean exists = sampleKeys.contains(sampleKey);
410        if (exists) {
411          if (LOGGER.isLoggable(Level.FINE)) {
412            String labels = "{";
413            for (int i = 0; i < sample.labelNames.size(); i++) {
414              labels += sample.labelNames.get(i) + "=" + sample.labelValues.get(i) + ",";
415            }
416            labels += "}";
417            LOGGER.fine("Metric " + sample.name + labels + " was created multiple times. Keeping the first occurrence. Dropping the others.");
418          }
419        } else {
420            mfs.samples.add(sample);
421            sampleKeys.add(sampleKey);
422        }
423      }
424
425      // Add the matched rule to the cached rules and tag it as not stale
426      // if the rule is configured to be cached
427      private void addToCache(final Rule rule, final String cacheKey, final MatchedRule matchedRule) {
428        if (rule.cache) {
429          config.rulesCache.put(rule, cacheKey, matchedRule);
430          stalenessTracker.add(rule, cacheKey);
431        }
432      }
433
434      private MatchedRule defaultExport(
435          String matchName,
436          String domain,
437          LinkedHashMap<String, String> beanProperties,
438          LinkedList<String> attrKeys,
439          String attrName,
440          String help,
441          Double value,
442          double valueFactor,
443          Type type) {
444        StringBuilder name = new StringBuilder();
445        name.append(domain);
446        if (beanProperties.size() > 0) {
447            name.append(SEP);
448            name.append(beanProperties.values().iterator().next());
449        }
450        for (String k : attrKeys) {
451            name.append(SEP);
452            name.append(k);
453        }
454        name.append(SEP);
455        name.append(attrName);
456        String fullname = safeName(name.toString());
457
458        if (config.lowercaseOutputName) {
459          fullname = fullname.toLowerCase();
460        }
461
462        List<String> labelNames = new ArrayList<String>();
463        List<String> labelValues = new ArrayList<String>();
464        if (beanProperties.size() > 1) {
465            Iterator<Map.Entry<String, String>> iter = beanProperties.entrySet().iterator();
466            // Skip the first one, it's been used in the name.
467            iter.next();
468            while (iter.hasNext()) {
469              Map.Entry<String, String> entry = iter.next();
470              String labelName = safeName(entry.getKey());
471              if (config.lowercaseOutputLabelNames) {
472                labelName = labelName.toLowerCase();
473              }
474              labelNames.add(labelName);
475              labelValues.add(entry.getValue());
476            }
477        }
478
479        return new MatchedRule(fullname, matchName, type, help, labelNames, labelValues, value, valueFactor);
480      }
481
482      public void recordBean(
483          String domain,
484          LinkedHashMap<String, String> beanProperties,
485          LinkedList<String> attrKeys,
486          String attrName,
487          String attrType,
488          String attrDescription,
489          Object beanValue) {
490
491        String beanName = domain + angleBrackets(beanProperties.toString()) + angleBrackets(attrKeys.toString());
492
493        // Build the HELP string from the bean metadata.
494        String help = domain + ":name=" + beanProperties.get("name") + ",type=" + beanProperties.get("type") + ",attribute=" + attrName;
495        // Add the attrDescription to the HELP if it exists and is useful.
496        if (attrDescription != null && !attrDescription.equals(attrName)) {
497          help = attrDescription + " " + help;
498        }
499
500        String attrNameSnakeCase = toSnakeAndLowerCase(attrName);
501
502        MatchedRule matchedRule = MatchedRule.unmatched();
503
504        for (Rule rule : config.rules) {
505          // Rules with bean values cannot be properly cached (only the value from the first scrape will be cached).
506          // If caching for the rule is enabled, replace the value with a dummy <cache> to avoid caching different values at different times.
507          Object matchBeanValue = rule.cache ? "<cache>" : beanValue;
508
509          String matchName = beanName + (rule.attrNameSnakeCase ? attrNameSnakeCase : attrName) + ": " + matchBeanValue;
510
511          if (rule.cache) {
512            MatchedRule cachedRule = config.rulesCache.get(rule, matchName);
513            if (cachedRule != null) {
514              stalenessTracker.add(rule, matchName);
515              if (cachedRule.isMatched()) {
516                matchedRule = cachedRule;
517                break;
518              }
519
520              // The bean was cached earlier, but did not match the current rule.
521              // Skip it to avoid matching against the same pattern again
522              continue;
523            }
524          }
525
526          Matcher matcher = null;
527          if (rule.pattern != null) {
528            matcher = rule.pattern.matcher(matchName);
529            if (!matcher.matches()) {
530              addToCache(rule, matchName, MatchedRule.unmatched());
531              continue;
532            }
533          }
534
535          Double value = null;
536          if (rule.value != null && !rule.value.isEmpty()) {
537            String val = matcher.replaceAll(rule.value);
538            try {
539              value = Double.valueOf(val);
540            } catch (NumberFormatException e) {
541              LOGGER.fine("Unable to parse configured value '" + val + "' to number for bean: " + beanName + attrName + ": " + beanValue);
542              return;
543            }
544          }
545
546          // If there's no name provided, use default export format.
547          if (rule.name == null) {
548            matchedRule = defaultExport(matchName, domain, beanProperties, attrKeys, rule.attrNameSnakeCase ? attrNameSnakeCase : attrName, help, value, rule.valueFactor, rule.type);
549            addToCache(rule, matchName, matchedRule);
550            break;
551          }
552
553          // Matcher is set below here due to validation in the constructor.
554          String name = safeName(matcher.replaceAll(rule.name));
555          if (name.isEmpty()) {
556            return;
557          }
558          if (config.lowercaseOutputName) {
559            name = name.toLowerCase();
560          }
561
562          // Set the help.
563          if (rule.help != null) {
564            help = matcher.replaceAll(rule.help);
565          }
566
567          // Set the labels.
568          ArrayList<String> labelNames = new ArrayList<String>();
569          ArrayList<String> labelValues = new ArrayList<String>();
570          if (rule.labelNames != null) {
571            for (int i = 0; i < rule.labelNames.size(); i++) {
572              final String unsafeLabelName = rule.labelNames.get(i);
573              final String labelValReplacement = rule.labelValues.get(i);
574              try {
575                String labelName = safeName(matcher.replaceAll(unsafeLabelName));
576                String labelValue = matcher.replaceAll(labelValReplacement);
577                if (config.lowercaseOutputLabelNames) {
578                  labelName = labelName.toLowerCase();
579                }
580                if (!labelName.isEmpty() && !labelValue.isEmpty()) {
581                  labelNames.add(labelName);
582                  labelValues.add(labelValue);
583                }
584              } catch (Exception e) {
585                throw new RuntimeException(
586                        format("Matcher '%s' unable to use: '%s' value: '%s'", matcher, unsafeLabelName, labelValReplacement), e);
587              }
588            }
589          }
590
591          matchedRule = new MatchedRule(name, matchName, rule.type, help, labelNames, labelValues, value, rule.valueFactor);
592          addToCache(rule, matchName, matchedRule);
593          break;
594        }
595
596        if (matchedRule.isUnmatched()) {
597          return;
598        }
599
600        Number value;
601        if (matchedRule.value != null) {
602          beanValue = matchedRule.value;
603        }
604
605        if (beanValue instanceof Number) {
606          value = ((Number) beanValue).doubleValue() * matchedRule.valueFactor;
607        } else if (beanValue instanceof Boolean) {
608          value = (Boolean) beanValue ? 1 : 0;
609        } else {
610          LOGGER.fine("Ignoring unsupported bean: " + beanName + attrName + ": " + beanValue);
611          return;
612        }
613
614        // Add to samples.
615        LOGGER.fine("add metric sample: " + matchedRule.name + " " + matchedRule.labelNames + " " + matchedRule.labelValues + " " + value.doubleValue());
616        addSample(new MetricFamilySamples.Sample(matchedRule.name, matchedRule.labelNames, matchedRule.labelValues, value.doubleValue()), matchedRule.type, matchedRule.help);
617      }
618
619    }
620
621  public List<MetricFamilySamples> collect() {
622      // Take a reference to the current config and collect with this one
623      // (to avoid race conditions in case another thread reloads the config in the meantime)
624      Config config = getLatestConfig();
625
626      MatchedRulesCache.StalenessTracker stalenessTracker = new MatchedRulesCache.StalenessTracker();
627      Receiver receiver = new Receiver(config, stalenessTracker);
628      JmxScraper scraper = new JmxScraper(config.jmxUrl, config.username, config.password, config.ssl,
629              config.whitelistObjectNames, config.blacklistObjectNames, receiver, jmxMBeanPropertyCache);
630      long start = System.nanoTime();
631      double error = 0;
632      if ((config.startDelaySeconds > 0) &&
633        ((start - createTimeNanoSecs) / 1000000000L < config.startDelaySeconds)) {
634        throw new IllegalStateException("JMXCollector waiting for startDelaySeconds");
635      }
636      try {
637        scraper.doScrape();
638      } catch (Exception e) {
639        error = 1;
640        StringWriter sw = new StringWriter();
641        e.printStackTrace(new PrintWriter(sw));
642        LOGGER.severe("JMX scrape failed: " + sw.toString());
643      }
644      config.rulesCache.evictStaleEntries(stalenessTracker);
645
646      List<MetricFamilySamples> mfsList = new ArrayList<MetricFamilySamples>();
647      mfsList.addAll(receiver.metricFamilySamplesMap.values());
648      List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>();
649      samples.add(new MetricFamilySamples.Sample(
650          "jmx_scrape_duration_seconds", new ArrayList<String>(), new ArrayList<String>(), (System.nanoTime() - start) / 1.0E9));
651      mfsList.add(new MetricFamilySamples("jmx_scrape_duration_seconds", Type.GAUGE, "Time this JMX scrape took, in seconds.", samples));
652
653      samples = new ArrayList<MetricFamilySamples.Sample>();
654      samples.add(new MetricFamilySamples.Sample(
655          "jmx_scrape_error", new ArrayList<String>(), new ArrayList<String>(), error));
656      mfsList.add(new MetricFamilySamples("jmx_scrape_error", Type.GAUGE, "Non-zero if this scrape failed.", samples));
657      samples = new ArrayList<MetricFamilySamples.Sample>();
658      samples.add(new MetricFamilySamples.Sample(
659              "jmx_scrape_cached_beans", new ArrayList<String>(), new ArrayList<String>(), stalenessTracker.cachedCount()));
660      mfsList.add(new MetricFamilySamples("jmx_scrape_cached_beans", Type.GAUGE, "Number of beans with their matching rule cached", samples));
661      return mfsList;
662    }
663
664    public List<MetricFamilySamples> describe() {
665      List<MetricFamilySamples> sampleFamilies = new ArrayList<MetricFamilySamples>();
666      sampleFamilies.add(new MetricFamilySamples("jmx_scrape_duration_seconds", Type.GAUGE, "Time this JMX scrape took, in seconds.", new ArrayList<MetricFamilySamples.Sample>()));
667      sampleFamilies.add(new MetricFamilySamples("jmx_scrape_error", Type.GAUGE, "Non-zero if this scrape failed.", new ArrayList<MetricFamilySamples.Sample>()));
668      sampleFamilies.add(new MetricFamilySamples("jmx_scrape_cached_beans", Type.GAUGE, "Number of beans with their matching rule cached", new ArrayList<MetricFamilySamples.Sample>()));
669      return sampleFamilies;
670    }
671
672    /**
673     * Convenience function to run standalone.
674     */
675    public static void main(String[] args) throws Exception {
676      String hostPort = "";
677      if (args.length > 0) {
678        hostPort = args[0];
679      }
680      JmxCollector jc = new JmxCollector(("{"
681      + "`hostPort`: `" + hostPort + "`,"
682      + "}").replace('`', '"'));
683      for(MetricFamilySamples mfs : jc.collect()) {
684        System.out.println(mfs);
685      }
686    }
687}