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        Pattern pattern = Pattern.compile("io\\.prometheus\\.metrics\\.([^.]+)\\.");
043        // Create a copy of the keySet() for iterating. We cannot iterate directly over keySet()
044        // because entries are removed when MetricsConfig.load(...) is called.
045        Set<String> propertyNames = new HashSet<>();
046        for (Object key : properties.keySet()) {
047            propertyNames.add(key.toString());
048        }
049        for (String propertyName : propertyNames) {
050            Matcher matcher = pattern.matcher(propertyName);
051            if (matcher.find()) {
052                String metricName = matcher.group(1);
053                if (!result.containsKey(metricName)) {
054                    result.put(metricName, MetricsProperties.load("io.prometheus.metrics." + metricName, properties));
055                }
056            }
057        }
058        return result;
059    }
060
061    // If there are properties left starting with io.prometheus it's likely a typo,
062    // because we didn't use that property.
063    // Throw a config error to let the user know that this property doesn't exist.
064    private static void validateAllPropertiesProcessed(Map<Object, Object> properties) {
065        for (Object key : properties.keySet()) {
066            if (key.toString().startsWith("io.prometheus")) {
067                throw new PrometheusPropertiesException(key + ": Unknown property");
068            }
069        }
070    }
071
072    private static Map<Object, Object> loadProperties() {
073        Map<Object, Object> properties = new HashMap<>();
074        properties.putAll(loadPropertiesFromClasspath());
075        properties.putAll(loadPropertiesFromFile()); // overriding the entries from the classpath file
076        properties.putAll(System.getProperties()); // overriding the entries from the properties file
077        // TODO: Add environment variables like EXEMPLARS_ENABLED.
078        return properties;
079    }
080
081    private static Properties loadPropertiesFromClasspath() {
082        Properties properties = new Properties();
083        try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("prometheus.properties")) {
084            properties.load(stream);
085        } catch (Exception ignored) {
086        }
087        return properties;
088    }
089
090    private static Properties loadPropertiesFromFile() throws PrometheusPropertiesException {
091        Properties properties = new Properties();
092        String path = System.getProperty("prometheus.config");
093        if (System.getenv("PROMETHEUS_CONFIG") != null) {
094            path = System.getenv("PROMETHEUS_CONFIG");
095        }
096        if (path != null) {
097            try (InputStream stream = Files.newInputStream(Paths.get(path))) {
098                properties.load(stream);
099            } catch (IOException e) {
100                throw new PrometheusPropertiesException("Failed to read Prometheus properties from " + path + ": " + e.getMessage(), e);
101            }
102        }
103        return properties;
104    }
105}