001package io.prometheus.client.exporter; 002 003import java.io.BufferedWriter; 004import java.io.ByteArrayOutputStream; 005import java.io.IOException; 006import java.io.InputStream; 007import java.io.OutputStreamWriter; 008import java.io.UnsupportedEncodingException; 009import java.net.HttpURLConnection; 010import java.net.InetAddress; 011import java.net.MalformedURLException; 012import java.net.URI; 013import java.net.URL; 014import java.net.URLEncoder; 015import java.net.UnknownHostException; 016import java.util.HashMap; 017import java.util.Map; 018 019import io.prometheus.client.Collector; 020import io.prometheus.client.CollectorRegistry; 021import io.prometheus.client.exporter.common.TextFormat; 022 023/** 024 * Export metrics via the Prometheus Pushgateway. 025 * <p> 026 * The Prometheus Pushgateway exists to allow ephemeral and batch jobs to expose their metrics to Prometheus. 027 * Since these kinds of jobs may not exist long enough to be scraped, they can instead push their metrics 028 * to a Pushgateway. This class allows pushing the contents of a {@link CollectorRegistry} to 029 * a Pushgateway. 030 * <p> 031 * Example usage: 032 * <pre> 033 * {@code 034 * void executeBatchJob() throws Exception { 035 * CollectorRegistry registry = new CollectorRegistry(); 036 * Gauge duration = Gauge.build() 037 * .name("my_batch_job_duration_seconds").help("Duration of my batch job in seconds.").register(registry); 038 * Gauge.Timer durationTimer = duration.startTimer(); 039 * try { 040 * // Your code here. 041 * 042 * // This is only added to the registry after success, 043 * // so that a previous success in the Pushgateway isn't overwritten on failure. 044 * Gauge lastSuccess = Gauge.build() 045 * .name("my_batch_job_last_success").help("Last time my batch job succeeded, in unixtime.").register(registry); 046 * lastSuccess.setToCurrentTime(); 047 * } finally { 048 * durationTimer.setDuration(); 049 * PushGateway pg = new PushGateway("127.0.0.1:9091"); 050 * pg.pushAdd(registry, "my_batch_job"); 051 * } 052 * } 053 * } 054 * </pre> 055 * <p> 056 * See <a href="https://github.com/prometheus/pushgateway">https://github.com/prometheus/pushgateway</a> 057 */ 058public class PushGateway { 059 060 private static final int MILLISECONDS_PER_SECOND = 1000; 061 062 // Visible for testing. 063 protected final String gatewayBaseURL; 064 065 private HttpConnectionFactory connectionFactory = new DefaultHttpConnectionFactory(); 066 067 /** 068 * Construct a Pushgateway, with the given address. 069 * <p> 070 * @param address host:port or ip:port of the Pushgateway. 071 */ 072 public PushGateway(String address) { 073 this(createURLSneakily("http://" + address)); 074 } 075 076 /** 077 * Construct a Pushgateway, with the given URL. 078 * <p> 079 * @param serverBaseURL the base URL and optional context path of the Pushgateway server. 080 */ 081 public PushGateway(URL serverBaseURL) { 082 this.gatewayBaseURL = URI.create(serverBaseURL.toString() + "/metrics/") 083 .normalize() 084 .toString(); 085 } 086 087 public void setConnectionFactory(HttpConnectionFactory connectionFactory) { 088 this.connectionFactory = connectionFactory; 089 } 090 091 /** 092 * Creates a URL instance from a String representation of a URL without throwing a checked exception. 093 * Required because you can't wrap a call to another constructor in a try statement. 094 * 095 * @param urlString the String representation of the URL. 096 * @return The URL instance. 097 */ 098 private static URL createURLSneakily(final String urlString) { 099 try { 100 return new URL(urlString); 101 } catch (MalformedURLException e) { 102 throw new RuntimeException(e); 103 } 104 } 105 106 /** 107 * Pushes all metrics in a registry, replacing all those with the same job and no grouping key. 108 * <p> 109 * This uses the PUT HTTP method. 110 */ 111 public void push(CollectorRegistry registry, String job) throws IOException { 112 doRequest(registry, job, null, "PUT"); 113 } 114 115 /** 116 * Pushes all metrics in a Collector, replacing all those with the same job and no grouping key. 117 * <p> 118 * This is useful for pushing a single Gauge. 119 * <p> 120 * This uses the PUT HTTP method. 121 */ 122 public void push(Collector collector, String job) throws IOException { 123 CollectorRegistry registry = new CollectorRegistry(); 124 collector.register(registry); 125 push(registry, job); 126 } 127 128 /** 129 * Pushes all metrics in a registry, replacing all those with the same job and grouping key. 130 * <p> 131 * This uses the PUT HTTP method. 132 */ 133 public void push(CollectorRegistry registry, String job, Map<String, String> groupingKey) throws IOException { 134 doRequest(registry, job, groupingKey, "PUT"); 135 } 136 137 /** 138 * Pushes all metrics in a Collector, replacing all those with the same job and grouping key. 139 * <p> 140 * This is useful for pushing a single Gauge. 141 * <p> 142 * This uses the PUT HTTP method. 143 */ 144 public void push(Collector collector, String job, Map<String, String> groupingKey) throws IOException { 145 CollectorRegistry registry = new CollectorRegistry(); 146 collector.register(registry); 147 push(registry, job, groupingKey); 148 } 149 150 /** 151 * Pushes all metrics in a registry, replacing only previously pushed metrics of the same name and job and no grouping key. 152 * <p> 153 * This uses the POST HTTP method. 154 */ 155 public void pushAdd(CollectorRegistry registry, String job) throws IOException { 156 doRequest(registry, job, null, "POST"); 157 } 158 159 /** 160 * Pushes all metrics in a Collector, replacing only previously pushed metrics of the same name and job and no grouping key. 161 * <p> 162 * This is useful for pushing a single Gauge. 163 * <p> 164 * This uses the POST HTTP method. 165 */ 166 public void pushAdd(Collector collector, String job) throws IOException { 167 CollectorRegistry registry = new CollectorRegistry(); 168 collector.register(registry); 169 pushAdd(registry, job); 170 } 171 172 /** 173 * Pushes all metrics in a registry, replacing only previously pushed metrics of the same name, job and grouping key. 174 * <p> 175 * This uses the POST HTTP method. 176 */ 177 public void pushAdd(CollectorRegistry registry, String job, Map<String, String> groupingKey) throws IOException { 178 doRequest(registry, job, groupingKey, "POST"); 179 } 180 181 /** 182 * Pushes all metrics in a Collector, replacing only previously pushed metrics of the same name, job and grouping key. 183 * <p> 184 * This is useful for pushing a single Gauge. 185 * <p> 186 * This uses the POST HTTP method. 187 */ 188 public void pushAdd(Collector collector, String job, Map<String, String> groupingKey) throws IOException { 189 CollectorRegistry registry = new CollectorRegistry(); 190 collector.register(registry); 191 pushAdd(registry, job, groupingKey); 192 } 193 194 195 /** 196 * Deletes metrics from the Pushgateway. 197 * <p> 198 * Deletes metrics with no grouping key and the provided job. 199 * This uses the DELETE HTTP method. 200 */ 201 public void delete(String job) throws IOException { 202 doRequest(null, job, null, "DELETE"); 203 } 204 205 /** 206 * Deletes metrics from the Pushgateway. 207 * <p> 208 * Deletes metrics with the provided job and grouping key. 209 * This uses the DELETE HTTP method. 210 */ 211 public void delete(String job, Map<String, String> groupingKey) throws IOException { 212 doRequest(null, job, groupingKey, "DELETE"); 213 } 214 215 void doRequest(CollectorRegistry registry, String job, Map<String, String> groupingKey, String method) throws IOException { 216 String url = gatewayBaseURL; 217 if (job.contains("/")) { 218 url += "job@base64/" + base64url(job); 219 } else { 220 url += "job/" + URLEncoder.encode(job, "UTF-8"); 221 } 222 223 if (groupingKey != null) { 224 for (Map.Entry<String, String> entry: groupingKey.entrySet()) { 225 if (entry.getValue().isEmpty()) { 226 url += "/" + entry.getKey() + "@base64/="; 227 } else if (entry.getValue().contains("/")) { 228 url += "/" + entry.getKey() + "@base64/" + base64url(entry.getValue()); 229 } else { 230 url += "/" + entry.getKey() + "/" + URLEncoder.encode(entry.getValue(), "UTF-8"); 231 } 232 } 233 } 234 HttpURLConnection connection = connectionFactory.create(url); 235 connection.setRequestProperty("Content-Type", TextFormat.CONTENT_TYPE_004); 236 if (!method.equals("DELETE")) { 237 connection.setDoOutput(true); 238 } 239 connection.setRequestMethod(method); 240 241 connection.setConnectTimeout(10 * MILLISECONDS_PER_SECOND); 242 connection.setReadTimeout(10 * MILLISECONDS_PER_SECOND); 243 connection.connect(); 244 245 try { 246 if (!method.equals("DELETE")) { 247 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), "UTF-8")); 248 TextFormat.write004(writer, registry.metricFamilySamples()); 249 writer.flush(); 250 writer.close(); 251 } 252 253 int response = connection.getResponseCode(); 254 if (response/100 != 2) { 255 String errorMessage; 256 InputStream errorStream = connection.getErrorStream(); 257 if(errorStream != null) { 258 String errBody = readFromStream(errorStream); 259 errorMessage = "Response code from " + url + " was " + response + ", response body: " + errBody; 260 } else { 261 errorMessage = "Response code from " + url + " was " + response; 262 } 263 throw new IOException(errorMessage); 264 } 265 } finally { 266 connection.disconnect(); 267 } 268 } 269 270 private static String base64url(String v) { 271 // Per RFC4648 table 2. We support Java 6, and java.util.Base64 was only added in Java 8, 272 try { 273 return Base64.encodeToString(v.getBytes("UTF-8")).replace("+", "-").replace("/", "_"); 274 } catch (UnsupportedEncodingException e) { 275 throw new RuntimeException(e); // Unreachable. 276 } 277 } 278 279 /** 280 * Returns a grouping key with the instance label set to the machine's IP address. 281 * <p> 282 * This is a convenience function, and should only be used where you want to 283 * push per-instance metrics rather than cluster/job level metrics. 284 */ 285 public static Map<String, String> instanceIPGroupingKey() throws UnknownHostException { 286 Map<String, String> groupingKey = new HashMap<String, String>(); 287 groupingKey.put("instance", InetAddress.getLocalHost().getHostAddress()); 288 return groupingKey; 289 } 290 291 private static String readFromStream(InputStream is) throws IOException { 292 ByteArrayOutputStream result = new ByteArrayOutputStream(); 293 byte[] buffer = new byte[1024]; 294 int length; 295 while ((length = is.read(buffer)) != -1) { 296 result.write(buffer, 0, length); 297 } 298 return result.toString("UTF-8"); 299 } 300}