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 *
018 * <p>It would be great to implement a subset of <a
019 * href="https://docs.spring.io/spring-boot/docs/3.1.x/reference/html/features.html#features.external-config">Spring
020 * Boot's Externalized Configuration</a>, like support for YAML, Properties, and env vars, or
021 * support for Spring's naming conventions for properties.
022 */
023public class PrometheusPropertiesLoader {
024
025  /** See {@link PrometheusProperties#get()}. */
026  public static PrometheusProperties load() throws PrometheusPropertiesException {
027    return load(new Properties());
028  }
029
030  public static PrometheusProperties load(Map<Object, Object> externalProperties)
031      throws PrometheusPropertiesException {
032    Map<Object, Object> properties = loadProperties(externalProperties);
033    Map<String, MetricsProperties> metricsConfigs = loadMetricsConfigs(properties);
034    MetricsProperties defaultMetricsProperties =
035        MetricsProperties.load("io.prometheus.metrics", properties);
036    ExemplarsProperties exemplarConfig = ExemplarsProperties.load(properties);
037    ExporterProperties exporterProperties = ExporterProperties.load(properties);
038    ExporterFilterProperties exporterFilterProperties = ExporterFilterProperties.load(properties);
039    ExporterHttpServerProperties exporterHttpServerProperties =
040        ExporterHttpServerProperties.load(properties);
041    ExporterPushgatewayProperties exporterPushgatewayProperties =
042        ExporterPushgatewayProperties.load(properties);
043    ExporterOpenTelemetryProperties exporterOpenTelemetryProperties =
044        ExporterOpenTelemetryProperties.load(properties);
045    validateAllPropertiesProcessed(properties);
046    return new PrometheusProperties(
047        defaultMetricsProperties,
048        metricsConfigs,
049        exemplarConfig,
050        exporterProperties,
051        exporterFilterProperties,
052        exporterHttpServerProperties,
053        exporterPushgatewayProperties,
054        exporterOpenTelemetryProperties);
055  }
056
057  // This will remove entries from properties when they are processed.
058  private static Map<String, MetricsProperties> loadMetricsConfigs(Map<Object, Object> properties) {
059    Map<String, MetricsProperties> result = new HashMap<>();
060    // Note that the metric name in the properties file must be as exposed in the Prometheus
061    // exposition formats,
062    // i.e. all dots replaced with underscores.
063    Pattern pattern = Pattern.compile("io\\.prometheus\\.metrics\\.([^.]+)\\.");
064    // Create a copy of the keySet() for iterating. We cannot iterate directly over keySet()
065    // because entries are removed when MetricsConfig.load(...) is called.
066    Set<String> propertyNames = new HashSet<>();
067    for (Object key : properties.keySet()) {
068      propertyNames.add(key.toString());
069    }
070    for (String propertyName : propertyNames) {
071      Matcher matcher = pattern.matcher(propertyName);
072      if (matcher.find()) {
073        String metricName = matcher.group(1).replace(".", "_");
074        if (!result.containsKey(metricName)) {
075          result.put(
076              metricName,
077              MetricsProperties.load("io.prometheus.metrics." + metricName, properties));
078        }
079      }
080    }
081    return result;
082  }
083
084  // If there are properties left starting with io.prometheus it's likely a typo,
085  // because we didn't use that property.
086  // Throw a config error to let the user know that this property doesn't exist.
087  private static void validateAllPropertiesProcessed(Map<Object, Object> properties) {
088    for (Object key : properties.keySet()) {
089      if (key.toString().startsWith("io.prometheus")) {
090        throw new PrometheusPropertiesException(key + ": Unknown property");
091      }
092    }
093  }
094
095  private static Map<Object, Object> loadProperties(Map<Object, Object> externalProperties) {
096    Map<Object, Object> properties = new HashMap<>();
097    properties.putAll(loadPropertiesFromClasspath());
098    properties.putAll(loadPropertiesFromFile()); // overriding the entries from the classpath file
099    // overriding the entries from the properties file
100    // copy System properties to avoid ConcurrentModificationException
101    System.getProperties().stringPropertyNames().stream()
102        .filter(key -> key.startsWith("io.prometheus"))
103        .forEach(key -> properties.put(key, System.getProperty(key)));
104    properties.putAll(externalProperties); // overriding all the entries above
105    // TODO: Add environment variables like EXEMPLARS_ENABLED.
106    return properties;
107  }
108
109  private static Properties loadPropertiesFromClasspath() {
110    Properties properties = new Properties();
111    try (InputStream stream =
112        Thread.currentThread()
113            .getContextClassLoader()
114            .getResourceAsStream("prometheus.properties")) {
115      properties.load(stream);
116    } catch (Exception ignored) {
117      // No properties file found on the classpath.
118    }
119    return properties;
120  }
121
122  private static Properties loadPropertiesFromFile() throws PrometheusPropertiesException {
123    Properties properties = new Properties();
124    String path = System.getProperty("prometheus.config");
125    if (System.getenv("PROMETHEUS_CONFIG") != null) {
126      path = System.getenv("PROMETHEUS_CONFIG");
127    }
128    if (path != null) {
129      try (InputStream stream = Files.newInputStream(Paths.get(path))) {
130        properties.load(stream);
131      } catch (IOException e) {
132        throw new PrometheusPropertiesException(
133            "Failed to read Prometheus properties from " + path + ": " + e.getMessage(), e);
134      }
135    }
136    return properties;
137  }
138}