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}