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