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