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