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