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