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 * @param urlString the String representation of the URL. 098 * @return The URL instance. 099 */ 100 private static URL createURLSneakily(final String urlString) { 101 try { 102 return new URL(urlString); 103 } catch (MalformedURLException e) { 104 throw new RuntimeException(e); 105 } 106 } 107 108 /** 109 * Pushes all metrics in a registry, replacing all those with the same job and no grouping key. 110 * <p> 111 * This uses the PUT HTTP method. 112 */ 113 public void push(CollectorRegistry registry, String job) throws IOException { 114 doRequest(registry, job, null, "PUT"); 115 } 116 117 /** 118 * Pushes all metrics in a Collector, replacing all those with the same job and no grouping key. 119 * <p> 120 * This is useful for pushing a single Gauge. 121 * <p> 122 * This uses the PUT HTTP method. 123 */ 124 public void push(Collector collector, String job) throws IOException { 125 CollectorRegistry registry = new CollectorRegistry(); 126 collector.register(registry); 127 push(registry, job); 128 } 129 130 /** 131 * Pushes all metrics in a registry, replacing all those with the same job and grouping key. 132 * <p> 133 * This uses the PUT HTTP method. 134 */ 135 public void push(CollectorRegistry registry, String job, Map<String, String> groupingKey) throws IOException { 136 doRequest(registry, job, groupingKey, "PUT"); 137 } 138 139 /** 140 * Pushes all metrics in a Collector, replacing all those with the same job and grouping key. 141 * <p> 142 * This is useful for pushing a single Gauge. 143 * <p> 144 * This uses the PUT HTTP method. 145 */ 146 public void push(Collector collector, String job, Map<String, String> groupingKey) throws IOException { 147 CollectorRegistry registry = new CollectorRegistry(); 148 collector.register(registry); 149 push(registry, job, groupingKey); 150 } 151 152 /** 153 * Pushes all metrics in a registry, replacing only previously pushed metrics of the same name and job and no grouping key. 154 * <p> 155 * This uses the POST HTTP method. 156 */ 157 public void pushAdd(CollectorRegistry registry, String job) throws IOException { 158 doRequest(registry, job, null, "POST"); 159 } 160 161 /** 162 * Pushes all metrics in a Collector, replacing only previously pushed metrics of the same name and job and no grouping key. 163 * <p> 164 * This is useful for pushing a single Gauge. 165 * <p> 166 * This uses the POST HTTP method. 167 */ 168 public void pushAdd(Collector collector, String job) throws IOException { 169 CollectorRegistry registry = new CollectorRegistry(); 170 collector.register(registry); 171 pushAdd(registry, job); 172 } 173 174 /** 175 * Pushes all metrics in a registry, replacing only previously pushed metrics of the same name, job and grouping key. 176 * <p> 177 * This uses the POST HTTP method. 178 */ 179 public void pushAdd(CollectorRegistry registry, String job, Map<String, String> groupingKey) throws IOException { 180 doRequest(registry, job, groupingKey, "POST"); 181 } 182 183 /** 184 * Pushes all metrics in a Collector, replacing only previously pushed metrics of the same name, job and grouping key. 185 * <p> 186 * This is useful for pushing a single Gauge. 187 * <p> 188 * This uses the POST HTTP method. 189 */ 190 public void pushAdd(Collector collector, String job, Map<String, String> groupingKey) throws IOException { 191 CollectorRegistry registry = new CollectorRegistry(); 192 collector.register(registry); 193 pushAdd(registry, job, groupingKey); 194 } 195 196 197 /** 198 * Deletes metrics from the Pushgateway. 199 * <p> 200 * Deletes metrics with no grouping key and the provided job. 201 * This uses the DELETE HTTP method. 202 */ 203 public void delete(String job) throws IOException { 204 doRequest(null, job, null, "DELETE"); 205 } 206 207 /** 208 * Deletes metrics from the Pushgateway. 209 * <p> 210 * Deletes metrics with the provided job and grouping key. 211 * This uses the DELETE HTTP method. 212 */ 213 public void delete(String job, Map<String, String> groupingKey) throws IOException { 214 doRequest(null, job, groupingKey, "DELETE"); 215 } 216 217 void doRequest(CollectorRegistry registry, String job, Map<String, String> groupingKey, String method) throws IOException { 218 String url = gatewayBaseURL; 219 if (job.contains("/")) { 220 url += "job@base64/" + base64url(job); 221 } else { 222 url += "job/" + URLEncoder.encode(job, "UTF-8"); 223 } 224 225 if (groupingKey != null) { 226 for (Map.Entry<String, String> entry: groupingKey.entrySet()) { 227 if (entry.getValue().isEmpty()) { 228 url += "/" + entry.getKey() + "@base64/="; 229 } else if (entry.getValue().contains("/")) { 230 url += "/" + entry.getKey() + "@base64/" + base64url(entry.getValue()); 231 } else { 232 url += "/" + entry.getKey() + "/" + URLEncoder.encode(entry.getValue(), "UTF-8"); 233 } 234 } 235 } 236 HttpURLConnection connection = connectionFactory.create(url); 237 connection.setRequestProperty("Content-Type", TextFormat.CONTENT_TYPE_004); 238 if (!method.equals("DELETE")) { 239 connection.setDoOutput(true); 240 } 241 connection.setRequestMethod(method); 242 243 connection.setConnectTimeout(10 * MILLISECONDS_PER_SECOND); 244 connection.setReadTimeout(10 * MILLISECONDS_PER_SECOND); 245 connection.connect(); 246 247 try { 248 if (!method.equals("DELETE")) { 249 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), "UTF-8")); 250 TextFormat.write004(writer, registry.metricFamilySamples()); 251 writer.flush(); 252 writer.close(); 253 } 254 255 int response = connection.getResponseCode(); 256 if (response/100 != 2) { 257 String errorMessage; 258 InputStream errorStream = connection.getErrorStream(); 259 if(errorStream != null) { 260 String errBody = readFromStream(errorStream); 261 errorMessage = "Response code from " + url + " was " + response + ", response body: " + errBody; 262 } else { 263 errorMessage = "Response code from " + url + " was " + response; 264 } 265 throw new IOException(errorMessage); 266 } 267 } finally { 268 connection.disconnect(); 269 } 270 } 271 272 private static String base64url(String v) { 273 // Per RFC4648 table 2. We support Java 6, and java.util.Base64 was only added in Java 8, 274 try { 275 return DatatypeConverter.printBase64Binary(v.getBytes("UTF-8")).replace("+", "-").replace("/", "_"); 276 } catch (UnsupportedEncodingException e) { 277 throw new RuntimeException(e); // Unreachable. 278 } 279 } 280 281 /** 282 * Returns a grouping key with the instance label set to the machine's IP address. 283 * <p> 284 * This is a convenience function, and should only be used where you want to 285 * push per-instance metrics rather than cluster/job level metrics. 286 */ 287 public static Map<String, String> instanceIPGroupingKey() throws UnknownHostException { 288 Map<String, String> groupingKey = new HashMap<String, String>(); 289 groupingKey.put("instance", InetAddress.getLocalHost().getHostAddress()); 290 return groupingKey; 291 } 292 293 private static String readFromStream(InputStream is) throws IOException { 294 ByteArrayOutputStream result = new ByteArrayOutputStream(); 295 byte[] buffer = new byte[1024]; 296 int length; 297 while ((length = is.read(buffer)) != -1) { 298 result.write(buffer, 0, length); 299 } 300 return result.toString("UTF-8"); 301 } 302}