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.Map;
009import java.util.Properties;
010import java.util.Set;
011
012/**
013 * The Properties Loader is early stages.
014 *
015 * <p>It would be great to implement a subset of <a
016 * href="https://docs.spring.io/spring-boot/docs/3.1.x/reference/html/features.html#features.external-config">Spring
017 * Boot's Externalized Configuration</a>, like support for YAML, Properties, and env vars, or
018 * support for Spring's naming conventions for properties.
019 */
020public class PrometheusPropertiesLoader {
021
022  /** See {@link PrometheusProperties#get()}. */
023  public static PrometheusProperties load() throws PrometheusPropertiesException {
024    return load(new Properties());
025  }
026
027  public static PrometheusProperties load(Map<Object, Object> externalProperties)
028      throws PrometheusPropertiesException {
029    PropertySource propertySource = loadProperties(externalProperties);
030    PrometheusProperties.MetricPropertiesMap metricsConfigs = loadMetricsConfigs(propertySource);
031    MetricsProperties defaultMetricsProperties =
032        MetricsProperties.load("io.prometheus.metrics", propertySource);
033    ExemplarsProperties exemplarConfig = ExemplarsProperties.load(propertySource);
034    ExporterProperties exporterProperties = ExporterProperties.load(propertySource);
035    ExporterFilterProperties exporterFilterProperties =
036        ExporterFilterProperties.load(propertySource);
037    ExporterHttpServerProperties exporterHttpServerProperties =
038        ExporterHttpServerProperties.load(propertySource);
039    ExporterPushgatewayProperties exporterPushgatewayProperties =
040        ExporterPushgatewayProperties.load(propertySource);
041    ExporterOpenTelemetryProperties exporterOpenTelemetryProperties =
042        ExporterOpenTelemetryProperties.load(propertySource);
043    validateAllPropertiesProcessed(propertySource);
044    return new PrometheusProperties(
045        defaultMetricsProperties,
046        metricsConfigs,
047        exemplarConfig,
048        exporterProperties,
049        exporterFilterProperties,
050        exporterHttpServerProperties,
051        exporterPushgatewayProperties,
052        exporterOpenTelemetryProperties);
053  }
054
055  // This will remove entries from propertySource when they are processed.
056  static PrometheusProperties.MetricPropertiesMap loadMetricsConfigs(
057      PropertySource propertySource) {
058    PrometheusProperties.MetricPropertiesMap result =
059        new PrometheusProperties.MetricPropertiesMap();
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
064    // Get a snapshot of all keys for pattern matching. Entries will be removed
065    // when MetricsProperties.load(...) is called.
066    Set<String> propertyNames = propertySource.getAllKeys();
067    for (String propertyName : propertyNames) {
068      String metricName = null;
069
070      if (propertyName.startsWith("io.prometheus.metrics.")) {
071        // Dot-separated format (from regular properties, system properties, or files)
072        String remainder = propertyName.substring("io.prometheus.metrics.".length());
073        // Try to match against known property suffixes
074        for (String suffix : MetricsProperties.PROPERTY_SUFFIXES) {
075          if (remainder.endsWith("." + suffix)) {
076            // Metric name in dot format, convert dots to underscores for exposition format
077            metricName =
078                remainder.substring(0, remainder.length() - suffix.length() - 1).replace(".", "_");
079            break;
080          }
081        }
082      } else if (propertyName.startsWith("io_prometheus_metrics_")) {
083        // Underscore-separated format (from environment variables)
084        String remainder = propertyName.substring("io_prometheus_metrics_".length());
085        // Try to match against known property suffixes
086        for (String suffix : MetricsProperties.PROPERTY_SUFFIXES) {
087          if (remainder.endsWith("_" + suffix)) {
088            metricName = remainder.substring(0, remainder.length() - suffix.length() - 1);
089            break;
090          }
091        }
092      }
093
094      if (metricName != null && result.get(metricName) == null) {
095        result.put(
096            metricName,
097            MetricsProperties.load("io.prometheus.metrics." + metricName, propertySource));
098      }
099    }
100    return result;
101  }
102
103  // If there are properties left starting with io.prometheus it's likely a typo,
104  // because we didn't use that property.
105  // Throw a config error to let the user know that this property doesn't exist.
106  private static void validateAllPropertiesProcessed(PropertySource propertySource) {
107    for (String key : propertySource.getRemainingKeys()) {
108      if (key.startsWith("io.prometheus") || key.startsWith("io_prometheus")) {
109        throw new PrometheusPropertiesException(key + ": Unknown property");
110      }
111    }
112  }
113
114  private static PropertySource loadProperties(Map<Object, Object> externalProperties) {
115    // Regular properties (lowest priority): classpath, file, system properties
116    Map<Object, Object> regularProperties = new HashMap<>();
117    // Normalize all properties at load time to handle camelCase in files for backward compatibility
118    normalizeAndPutAll(regularProperties, loadPropertiesFromClasspath());
119    normalizeAndPutAll(
120        regularProperties,
121        loadPropertiesFromFile()); // overriding the entries from the classpath file
122    // overriding the entries from the properties file
123    // copy System properties to avoid ConcurrentModificationException
124    // normalize camelCase system properties to snake_case for backward compatibility
125    System.getProperties().stringPropertyNames().stream()
126        .filter(key -> key.startsWith("io.prometheus"))
127        .forEach(key -> regularProperties.put(normalizePropertyKey(key), System.getProperty(key)));
128
129    // Environment variables (second priority): just lowercase, keep underscores
130    Map<Object, Object> envVarProperties = loadPropertiesFromEnvironment();
131
132    // External properties (highest priority): normalize camelCase for backward compatibility
133    Map<Object, Object> normalizedExternalProperties = new HashMap<>();
134    externalProperties.forEach(
135        (key, value) ->
136            normalizedExternalProperties.put(normalizePropertyKey(key.toString()), value));
137
138    return new PropertySource(normalizedExternalProperties, envVarProperties, regularProperties);
139  }
140
141  private static void normalizeAndPutAll(Map<Object, Object> target, Map<Object, Object> source) {
142    source.forEach((key, value) -> target.put(normalizePropertyKey(key.toString()), value));
143  }
144
145  private static Properties loadPropertiesFromClasspath() {
146    Properties properties = new Properties();
147    try (InputStream stream =
148        Thread.currentThread()
149            .getContextClassLoader()
150            .getResourceAsStream("prometheus.properties")) {
151      properties.load(stream);
152    } catch (Exception ignored) {
153      // No properties file found on the classpath.
154    }
155    return properties;
156  }
157
158  private static Properties loadPropertiesFromFile() throws PrometheusPropertiesException {
159    Properties properties = new Properties();
160    String path = System.getProperty("prometheus.config");
161    if (System.getenv("PROMETHEUS_CONFIG") != null) {
162      path = System.getenv("PROMETHEUS_CONFIG");
163    }
164    if (path != null) {
165      try (InputStream stream = Files.newInputStream(Paths.get(path))) {
166        properties.load(stream);
167      } catch (IOException e) {
168        throw new PrometheusPropertiesException(
169            "Failed to read Prometheus properties from " + path + ": " + e.getMessage(), e);
170      }
171    }
172    return properties;
173  }
174
175  /**
176   * Load properties from environment variables.
177   *
178   * <p>Environment variables are converted to lowercase but keep underscores as-is. For example,
179   * the environment variable IO_PROMETHEUS_METRICS_EXEMPLARS_ENABLED becomes
180   * io_prometheus_metrics_exemplars_enabled.
181   *
182   * <p>The transformation to dot notation happens at access time in PropertySource.
183   *
184   * <p>Only environment variables starting with IO_PROMETHEUS are considered.
185   *
186   * @return properties loaded from environment variables (with lowercase keys and underscores)
187   */
188  private static Map<Object, Object> loadPropertiesFromEnvironment() {
189    Map<Object, Object> properties = new HashMap<>();
190    System.getenv()
191        .forEach(
192            (key, value) -> {
193              if (key.startsWith("IO_PROMETHEUS")) {
194                String normalizedKey = key.toLowerCase(java.util.Locale.ROOT);
195                properties.put(normalizedKey, value);
196              }
197            });
198    return properties;
199  }
200
201  /**
202   * Normalize a property key for consistent lookup.
203   *
204   * <p>Converts camelCase property keys to snake_case. This allows both snake_case (preferred) and
205   * camelCase (deprecated) property names to be used.
206   *
207   * <p>For example: exemplarsEnabled → exemplars_enabled exemplars_enabled → exemplars_enabled
208   * (unchanged)
209   *
210   * @param key the property key
211   * @return the normalized property key
212   */
213  static String normalizePropertyKey(String key) {
214    // Insert underscores before uppercase letters to convert camelCase to snake_case
215    String withUnderscores = key.replaceAll("([a-z])([A-Z])", "$1_$2");
216    return withUnderscores.toLowerCase(java.util.Locale.ROOT);
217  }
218}