001package io.prometheus.metrics.config;
002
003import java.io.IOException;
004import java.io.InputStream;
005import java.nio.file.Files;
006import java.nio.file.Paths;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.Map;
010import java.util.Properties;
011import java.util.Set;
012import java.util.regex.Matcher;
013import java.util.regex.Pattern;
014
015/**
016 * The Properties Loader is early stages.
017 * <p>
018 * It would be great to implement a subset of
019 * <a href="https://docs.spring.io/spring-boot/docs/3.1.x/reference/html/features.html#features.external-config">Spring Boot's Externalized Configuration</a>,
020 * like support for YAML, Properties, and env vars, or support for Spring's naming conventions for properties.
021 */
022public class PrometheusPropertiesLoader {
023
024    /**
025     * See {@link PrometheusProperties#get()}.
026     */
027    public static PrometheusProperties load() throws PrometheusPropertiesException {
028        Map<Object, Object> properties = loadProperties();
029        Map<String, MetricsProperties> metricsConfigs = loadMetricsConfigs(properties);
030        MetricsProperties defaultMetricsProperties = MetricsProperties.load("io.prometheus.metrics", properties);
031        ExemplarsProperties exemplarConfig = ExemplarsProperties.load("io.prometheus.exemplars", properties);
032        ExporterProperties exporterProperties = ExporterProperties.load("io.prometheus.exporter", properties);
033        ExporterFilterProperties exporterFilterProperties = ExporterFilterProperties.load("io.prometheus.exporter.filter", properties);
034        ExporterHttpServerProperties exporterHttpServerProperties = ExporterHttpServerProperties.load("io.prometheus.exporter.httpServer", properties);
035        validateAllPropertiesProcessed(properties);
036        return new PrometheusProperties(defaultMetricsProperties, metricsConfigs, exemplarConfig, exporterProperties, exporterFilterProperties, exporterHttpServerProperties);
037    }
038
039    // This will remove entries from properties when they are processed.
040    private static Map<String, MetricsProperties> loadMetricsConfigs(Map<Object, Object> properties) {
041        Map<String, MetricsProperties> result = new HashMap<>();
042        // Note that the metric name in the properties file must be as exposed in the Prometheus exposition formats,
043        // i.e. all dots replaced with underscores.
044        Pattern pattern = Pattern.compile("io\\.prometheus\\.metrics\\.([^.]+)\\.");
045        // Create a copy of the keySet() for iterating. We cannot iterate directly over keySet()
046        // because entries are removed when MetricsConfig.load(...) is called.
047        Set<String> propertyNames = new HashSet<>();
048        for (Object key : properties.keySet()) {
049            propertyNames.add(key.toString());
050        }
051        for (String propertyName : propertyNames) {
052            Matcher matcher = pattern.matcher(propertyName);
053            if (matcher.find()) {
054                String metricName = matcher.group(1).replace(".", "_");
055                if (!result.containsKey(metricName)) {
056                    result.put(metricName, MetricsProperties.load("io.prometheus.metrics." + metricName, properties));
057                }
058            }
059        }
060        return result;
061    }
062
063    // If there are properties left starting with io.prometheus it's likely a typo,
064    // because we didn't use that property.
065    // Throw a config error to let the user know that this property doesn't exist.
066    private static void validateAllPropertiesProcessed(Map<Object, Object> properties) {
067        for (Object key : properties.keySet()) {
068            if (key.toString().startsWith("io.prometheus")) {
069                throw new PrometheusPropertiesException(key + ": Unknown property");
070            }
071        }
072    }
073
074    private static Map<Object, Object> loadProperties() {
075        Map<Object, Object> properties = new HashMap<>();
076        properties.putAll(loadPropertiesFromClasspath());
077        properties.putAll(loadPropertiesFromFile()); // overriding the entries from the classpath file
078        properties.putAll(System.getProperties()); // overriding the entries from the properties file
079        // TODO: Add environment variables like EXEMPLARS_ENABLED.
080        return properties;
081    }
082
083    private static Properties loadPropertiesFromClasspath() {
084        Properties properties = new Properties();
085        try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("prometheus.properties")) {
086            properties.load(stream);
087        } catch (Exception ignored) {
088        }
089        return properties;
090    }
091
092    private static Properties loadPropertiesFromFile() throws PrometheusPropertiesException {
093        Properties properties = new Properties();
094        String path = System.getProperty("prometheus.config");
095        if (System.getenv("PROMETHEUS_CONFIG") != null) {
096            path = System.getenv("PROMETHEUS_CONFIG");
097        }
098        if (path != null) {
099            try (InputStream stream = Files.newInputStream(Paths.get(path))) {
100                properties.load(stream);
101            } catch (IOException e) {
102                throw new PrometheusPropertiesException("Failed to read Prometheus properties from " + path + ": " + e.getMessage(), e);
103            }
104        }
105        return properties;
106    }
107}