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