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