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.HashSet; 009import java.util.Map; 010import java.util.Properties; 011import java.util.Set; 012import java.util.regex.Matcher; 013import java.util.regex.Pattern; 014 015/** 016 * The Properties Loader is early stages. 017 * 018 * <p>It would be great to implement a subset of <a 019 * href="https://docs.spring.io/spring-boot/docs/3.1.x/reference/html/features.html#features.external-config">Spring 020 * Boot's Externalized Configuration</a>, like support for YAML, Properties, and env vars, or 021 * support for Spring's naming conventions for properties. 022 */ 023public class PrometheusPropertiesLoader { 024 025 /** See {@link PrometheusProperties#get()}. */ 026 public static PrometheusProperties load() throws PrometheusPropertiesException { 027 return load(new Properties()); 028 } 029 030 public static PrometheusProperties load(Map<Object, Object> externalProperties) 031 throws PrometheusPropertiesException { 032 Map<Object, Object> properties = loadProperties(externalProperties); 033 Map<String, MetricsProperties> metricsConfigs = loadMetricsConfigs(properties); 034 MetricsProperties defaultMetricsProperties = 035 MetricsProperties.load("io.prometheus.metrics", properties); 036 ExemplarsProperties exemplarConfig = ExemplarsProperties.load(properties); 037 ExporterProperties exporterProperties = ExporterProperties.load(properties); 038 ExporterFilterProperties exporterFilterProperties = ExporterFilterProperties.load(properties); 039 ExporterHttpServerProperties exporterHttpServerProperties = 040 ExporterHttpServerProperties.load(properties); 041 ExporterPushgatewayProperties exporterPushgatewayProperties = 042 ExporterPushgatewayProperties.load(properties); 043 ExporterOpenTelemetryProperties exporterOpenTelemetryProperties = 044 ExporterOpenTelemetryProperties.load(properties); 045 validateAllPropertiesProcessed(properties); 046 return new PrometheusProperties( 047 defaultMetricsProperties, 048 metricsConfigs, 049 exemplarConfig, 050 exporterProperties, 051 exporterFilterProperties, 052 exporterHttpServerProperties, 053 exporterPushgatewayProperties, 054 exporterOpenTelemetryProperties); 055 } 056 057 // This will remove entries from properties when they are processed. 058 private static Map<String, MetricsProperties> loadMetricsConfigs(Map<Object, Object> properties) { 059 Map<String, MetricsProperties> result = new HashMap<>(); 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 Pattern pattern = Pattern.compile("io\\.prometheus\\.metrics\\.([^.]+)\\."); 064 // Create a copy of the keySet() for iterating. We cannot iterate directly over keySet() 065 // because entries are removed when MetricsConfig.load(...) is called. 066 Set<String> propertyNames = new HashSet<>(); 067 for (Object key : properties.keySet()) { 068 propertyNames.add(key.toString()); 069 } 070 for (String propertyName : propertyNames) { 071 Matcher matcher = pattern.matcher(propertyName); 072 if (matcher.find()) { 073 String metricName = matcher.group(1).replace(".", "_"); 074 if (!result.containsKey(metricName)) { 075 result.put( 076 metricName, 077 MetricsProperties.load("io.prometheus.metrics." + metricName, properties)); 078 } 079 } 080 } 081 return result; 082 } 083 084 // If there are properties left starting with io.prometheus it's likely a typo, 085 // because we didn't use that property. 086 // Throw a config error to let the user know that this property doesn't exist. 087 private static void validateAllPropertiesProcessed(Map<Object, Object> properties) { 088 for (Object key : properties.keySet()) { 089 if (key.toString().startsWith("io.prometheus")) { 090 throw new PrometheusPropertiesException(key + ": Unknown property"); 091 } 092 } 093 } 094 095 private static Map<Object, Object> loadProperties(Map<Object, Object> externalProperties) { 096 Map<Object, Object> properties = new HashMap<>(); 097 properties.putAll(loadPropertiesFromClasspath()); 098 properties.putAll(loadPropertiesFromFile()); // overriding the entries from the classpath file 099 // overriding the entries from the properties file 100 // copy System properties to avoid ConcurrentModificationException 101 System.getProperties().stringPropertyNames().stream() 102 .filter(key -> key.startsWith("io.prometheus")) 103 .forEach(key -> properties.put(key, System.getProperty(key))); 104 properties.putAll(externalProperties); // overriding all the entries above 105 // TODO: Add environment variables like EXEMPLARS_ENABLED. 106 return properties; 107 } 108 109 private static Properties loadPropertiesFromClasspath() { 110 Properties properties = new Properties(); 111 try (InputStream stream = 112 Thread.currentThread() 113 .getContextClassLoader() 114 .getResourceAsStream("prometheus.properties")) { 115 properties.load(stream); 116 } catch (Exception ignored) { 117 // No properties file found on the classpath. 118 } 119 return properties; 120 } 121 122 private static Properties loadPropertiesFromFile() throws PrometheusPropertiesException { 123 Properties properties = new Properties(); 124 String path = System.getProperty("prometheus.config"); 125 if (System.getenv("PROMETHEUS_CONFIG") != null) { 126 path = System.getenv("PROMETHEUS_CONFIG"); 127 } 128 if (path != null) { 129 try (InputStream stream = Files.newInputStream(Paths.get(path))) { 130 properties.load(stream); 131 } catch (IOException e) { 132 throw new PrometheusPropertiesException( 133 "Failed to read Prometheus properties from " + path + ": " + e.getMessage(), e); 134 } 135 } 136 return properties; 137 } 138}