All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.prometheus.jmx.JmxCollector Maven / Gradle / Ivy

There is a newer version: 1.0.1
Show newest version
package io.prometheus.jmx;

import io.prometheus.client.Collector;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import org.yaml.snakeyaml.Yaml;

import static java.lang.String.format;

public class JmxCollector extends Collector {
    private static final Logger LOGGER = Logger.getLogger(JmxCollector.class.getName());

    private static class Rule {
      Pattern pattern;
      String name;
      String help;
      boolean attrNameSnakeCase;
      Type type = Type.GAUGE;
      ArrayList labelNames;
      ArrayList labelValues;
    }

    String jmxUrl;
    String username;
    String password;
    
    boolean lowercaseOutputName;
    boolean lowercaseOutputLabelNames;
    List whitelistObjectNames = new ArrayList();
    List blacklistObjectNames = new ArrayList();
    ArrayList rules = new ArrayList();

    private static final Pattern snakeCasePattern = Pattern.compile("([a-z0-9])([A-Z])");

    public JmxCollector(Reader in) throws IOException, MalformedObjectNameException {
        this((Map)new Yaml().load(in));
    }
    public JmxCollector(String yamlConfig) throws MalformedObjectNameException {
        this((Map)new Yaml().load(yamlConfig));
    }
    private JmxCollector(Map config) throws MalformedObjectNameException {
        if(config == null) {  //Yaml config empty, set config to empty map.
            config = new HashMap();
        }

        if (config.containsKey("hostPort")) {
          if (config.containsKey("jmxUrl")) {
              throw new IllegalArgumentException("At most one of hostPort and jmxUrl must be provided");
          }
          jmxUrl ="service:jmx:rmi:///jndi/rmi://" + (String)config.get("hostPort") + "/jmxrmi";
        } else if (config.containsKey("jmxUrl")) {
          jmxUrl = (String)config.get("jmxUrl");
        } else {
            // Default to local JVM
            jmxUrl = "";
        }

        if (config.containsKey("username")) {
            username = (String)config.get("username");
          } else {
            // Any username.
            username = "";
          }
        
        if (config.containsKey("password")) {
            password = (String)config.get("password");
          } else {
            // Empty password.
            password = "";
          }
        
        if (config.containsKey("lowercaseOutputName")) {
          lowercaseOutputName = (Boolean)config.get("lowercaseOutputName");
        }
        if (config.containsKey("lowercaseOutputLabelNames")) {
          lowercaseOutputLabelNames = (Boolean)config.get("lowercaseOutputLabelNames");
        }

        if (config.containsKey("whitelistObjectNames")) {
          List names = (List) config.get("whitelistObjectNames");
          for(Object name : names) {
            whitelistObjectNames.add(new ObjectName((String)name));
          }
        } else {
          whitelistObjectNames.add(null);
        }
        if (config.containsKey("blacklistObjectNames")) {
          List names = (List) config.get("blacklistObjectNames");
          for (Object name : names) {
            blacklistObjectNames.add(new ObjectName((String)name));
          }
        }

        if (config.containsKey("rules")) {
          List> configRules = (List>) config.get("rules");
          for (Map ruleObject : configRules) {
            Map yamlRule = ruleObject;
            Rule rule = new Rule();
            rules.add(rule);
            if (yamlRule.containsKey("pattern")) {
              rule.pattern = Pattern.compile("^.*" + (String)yamlRule.get("pattern") + ".*$");
            }
            if (yamlRule.containsKey("name")) {
              rule.name = (String)yamlRule.get("name");
            }
            if (yamlRule.containsKey("attrNameSnakeCase")) {
              rule.attrNameSnakeCase = (Boolean)yamlRule.get("attrNameSnakeCase");
            }
            if (yamlRule.containsKey("type")) {
              rule.type = Type.valueOf((String)yamlRule.get("type"));
            }
            if (yamlRule.containsKey("help")) {
              rule.help = (String)yamlRule.get("help");
            }
            if (yamlRule.containsKey("labels")) {
              TreeMap labels = new TreeMap((Map)yamlRule.get("labels"));
              rule.labelNames = new ArrayList();
              rule.labelValues = new ArrayList();
              for (Map.Entry entry : (Set>)labels.entrySet()) {
                rule.labelNames.add(entry.getKey());
                rule.labelValues.add((String)entry.getValue());
              }
            }

            // Validation.
            if ((rule.labelNames != null || rule.help != null) && rule.name == null) {
              throw new IllegalArgumentException("Must provide name, if help or labels are given: " + yamlRule);
            }
            if (rule.name != null && rule.pattern == null) {
              throw new IllegalArgumentException("Must provide pattern, if name is given: " + yamlRule);
            }
          }
        } else {
          // Default to a single default rule.
          rules.add(new Rule());
        }

    }

    class Receiver implements JmxScraper.MBeanReceiver {
      Map metricFamilySamplesMap =
        new HashMap();

      private static final char SEP = '_';

      private final Pattern unsafeChars = Pattern.compile("[^a-zA-Z0-9:_]");
      private final Pattern multipleUnderscores = Pattern.compile("__+");

      // [] and () are special in regexes, so swtich to <>.
      private String angleBrackets(String s) {
        return "<" + s.substring(1, s.length() - 1) + ">";
      }

      private String safeName(String s) {
        // Change invalid chars to underscore, and merge underscores.
        return multipleUnderscores.matcher(unsafeChars.matcher(s).replaceAll("_")).replaceAll("_");
      }

      void addSample(MetricFamilySamples.Sample sample, Type type, String help) {
        MetricFamilySamples mfs = metricFamilySamplesMap.get(sample.name);
        if (mfs == null) {
          // JmxScraper.MBeanReceiver is only called from one thread,
          // so there's no race here.
          mfs = new MetricFamilySamples(sample.name, type, help, new ArrayList());
          metricFamilySamplesMap.put(sample.name, mfs);
        }
        mfs.samples.add(sample);
      }

      private void defaultExport(
          String domain,
          LinkedHashMap beanProperties,
          LinkedList attrKeys,
          String attrName,
          String attrType,
          String help,
          Object value) {
        StringBuilder name = new StringBuilder();
        name.append(domain);
        if (beanProperties.size() > 0) {
            name.append(SEP);
            name.append(beanProperties.values().iterator().next());
        }
        for (String k : attrKeys) {
            name.append(SEP);
            name.append(k);
        }
        name.append(SEP);
        name.append(attrName);
        String fullname = safeName(name.toString());

        if (lowercaseOutputName) {
          fullname = fullname.toLowerCase();
        }

        List labelNames = new ArrayList();
        List labelValues = new ArrayList();
        if (beanProperties.size() > 1) {
            Iterator> iter = beanProperties.entrySet().iterator();
            // Skip the first one, it's been used in the name.
            iter.next();
            while (iter.hasNext()) {
              Map.Entry entry = iter.next();
              String labelName = safeName(entry.getKey());
              if (lowercaseOutputLabelNames) {
                labelName = labelName.toLowerCase();
              }
              labelNames.add(labelName);
              labelValues.add(entry.getValue());
            }
        }

        addSample(new MetricFamilySamples.Sample(fullname, labelNames, labelValues, ((Number)value).doubleValue()),
          Type.GAUGE, help);
      }

      public void recordBean(
          String domain,
          LinkedHashMap beanProperties,
          LinkedList attrKeys,
          String attrName,
          String attrType,
          String attrDescription,
          Object value) {

        if (!(value instanceof Number)) {
          LOGGER.fine("Ignoring non-Number bean: " + domain + beanProperties.toString() + attrKeys.toString() + attrName + ": " + value);
          return;
        }
        String beanName = domain +
                   angleBrackets(beanProperties.toString()) +
                   angleBrackets(attrKeys.toString());
        // attrDescription tends not to be useful, so give the fully qualified name too.
        String help = attrDescription + " (" + beanName + attrName + ")";

        String attrNameSnakeCase = snakeCasePattern.matcher(attrName).replaceAll("$1_$2").toLowerCase();

        for (Rule rule : rules) {
          Matcher matcher = null;
          String matchName = beanName + (rule.attrNameSnakeCase ? attrNameSnakeCase : attrName);
          if (rule.pattern != null) {
            matcher = rule.pattern.matcher(matchName + ": " + value);
            if (!matcher.matches()) {
              continue;
            }
          }
          // If there's no name provided, use default export format.
          if (rule.name == null) {
            defaultExport(domain, beanProperties, attrKeys, rule.attrNameSnakeCase ? attrNameSnakeCase : attrName, attrType, help, value);
            return;
          }
          // Matcher is set below here due to validation in the constructor.
          String name = safeName(matcher.replaceAll(rule.name));
          if (name.isEmpty()) {
            return;
          }
          if (lowercaseOutputName) {
            name = name.toLowerCase();
          }
          // Set the help.
          if (rule.help != null) {
            help = matcher.replaceAll(rule.help);
          }
          // Set the labels.
          ArrayList labelNames = new ArrayList();
          ArrayList labelValues = new ArrayList();
          if (rule.labelNames != null) {
            for (int i = 0; i < rule.labelNames.size(); i++) {
              final String unsafeLabelName = rule.labelNames.get(i);
              final String labelValReplacement = rule.labelValues.get(i);
              try {
                String labelName = safeName(matcher.replaceAll(unsafeLabelName));
                String labelValue = matcher.replaceAll(labelValReplacement);
                if (lowercaseOutputLabelNames) {
                  labelName = labelName.toLowerCase();
                }
                if (!labelName.isEmpty() && !labelValue.isEmpty()) {
                  labelNames.add(labelName);
                  labelValues.add(labelValue);
                }
              } catch (Exception e) {
                throw new RuntimeException(
                  format("Matcher '%s' unable to use: '%s' value: '%s'", matcher, unsafeLabelName, labelValReplacement), e);
              }
            }
          }
          // Add to samples.
          LOGGER.fine("add metric sample: " + name + " " + labelNames + " " + labelValues + " " + ((Number)value).doubleValue());
          addSample(new MetricFamilySamples.Sample(name, labelNames, labelValues, ((Number)value).doubleValue()),
              rule.type, help);
          return;
        }
      }

    }

    public List collect() {
      Receiver receiver = new Receiver();
      JmxScraper scraper = new JmxScraper(jmxUrl, username, password, whitelistObjectNames, blacklistObjectNames, receiver);
      long start = System.nanoTime();
      double error = 0;
      try {
        scraper.doScrape();
      } catch (Exception e) {
        error = 1;
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        LOGGER.severe("JMX scrape failed: " + sw.toString());
      }
      List mfsList = new ArrayList();
      mfsList.addAll(receiver.metricFamilySamplesMap.values());
      List samples = new ArrayList();
      samples.add(new MetricFamilySamples.Sample(
          "jmx_scrape_duration_seconds", new ArrayList(), new ArrayList(), (System.nanoTime() - start) / 1.0E9));
      mfsList.add(new MetricFamilySamples("jmx_scrape_duration_seconds", Type.GAUGE, "Time this JMX scrape took, in seconds.", samples));

      samples = new ArrayList();
      samples.add(new MetricFamilySamples.Sample(
          "jmx_scrape_error", new ArrayList(), new ArrayList(), error));
      mfsList.add(new MetricFamilySamples("jmx_scrape_error", Type.GAUGE, "Non-zero if this scrape failed.", samples));
      return mfsList;
    }

    /**
     * Convenience function to run standalone.
     */
    public static void main(String[] args) throws Exception {
      String hostPort = "";
      if (args.length > 0) {
        hostPort = args[0];
      }
      JmxCollector jc = new JmxCollector(("{"
      + "`hostPort`: `" + hostPort + "`,"
      + "}").replace('`', '"'));
      for(MetricFamilySamples mfs : jc.collect()) {
        System.out.println(mfs);
      }
    }
}