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}