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