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.net.HttpURLConnection; 009import java.net.InetAddress; 010import java.net.MalformedURLException; 011import java.net.URI; 012import java.net.URL; 013import java.net.URLEncoder; 014import java.net.UnknownHostException; 015import java.util.Collections; 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/job/") 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 * TODO: Remove this along with other deprecated methods before version 1.0 is released. 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 218 /** 219 * Pushes all metrics in a registry, replacing all those with the same job and instance. 220 * <p> 221 * This uses the PUT HTTP method. 222 * @deprecated use {@link #push(CollectorRegistry, String, Map)} 223 */ 224 @Deprecated 225 public void push(CollectorRegistry registry, String job, String instance) throws IOException { 226 push(registry, job, Collections.singletonMap("instance", instance)); 227 } 228 229 /** 230 * Pushes all metrics in a Collector, replacing all those with the same job and instance. 231 * <p> 232 * This is useful for pushing a single Gauge. 233 * <p> 234 * This uses the PUT HTTP method. 235 * @deprecated use {@link #push(Collector, String, Map)} 236 */ 237 @Deprecated 238 public void push(Collector collector, String job, String instance) throws IOException { 239 push(collector, job, Collections.singletonMap("instance", instance)); 240 } 241 242 /** 243 * Pushes all metrics in a registry, replacing only previously pushed metrics of the same name. 244 * <p> 245 * This uses the POST HTTP method. 246 * @deprecated use {@link #pushAdd(CollectorRegistry, String, Map)} 247 */ 248 @Deprecated 249 public void pushAdd(CollectorRegistry registry, String job, String instance) throws IOException { 250 pushAdd(registry, job, Collections.singletonMap("instance", instance)); 251 } 252 253 /** 254 * Pushes all metrics in a Collector, replacing only previously pushed metrics of the same name. 255 * <p> 256 * This is useful for pushing a single Gauge. 257 * <p> 258 * This uses the POST HTTP method. 259 * @deprecated use {@link #pushAdd(Collector, String, Map)} 260 */ 261 @Deprecated 262 public void pushAdd(Collector collector, String job, String instance) throws IOException { 263 pushAdd(collector, job, Collections.singletonMap("instance", instance)); 264 } 265 266 /** 267 * Deletes metrics from the Pushgateway. 268 * <p> 269 * This uses the DELETE HTTP method. 270 * @deprecated use {@link #delete(String, Map)} 271 */ 272 @Deprecated 273 public void delete(String job, String instance) throws IOException { 274 delete(job, Collections.singletonMap("instance", instance)); 275 } 276 277 void doRequest(CollectorRegistry registry, String job, Map<String, String> groupingKey, String method) throws IOException { 278 String url = gatewayBaseURL + URLEncoder.encode(job, "UTF-8"); 279 280 if (groupingKey != null) { 281 for (Map.Entry<String, String> entry: groupingKey.entrySet()) { 282 url += "/" + entry.getKey() + "/" + URLEncoder.encode(entry.getValue(), "UTF-8"); 283 } 284 } 285 HttpURLConnection connection = connectionFactory.create(url); 286 connection.setRequestProperty("Content-Type", TextFormat.CONTENT_TYPE_004); 287 if (!method.equals("DELETE")) { 288 connection.setDoOutput(true); 289 } 290 connection.setRequestMethod(method); 291 292 connection.setConnectTimeout(10 * MILLISECONDS_PER_SECOND); 293 connection.setReadTimeout(10 * MILLISECONDS_PER_SECOND); 294 connection.connect(); 295 296 try { 297 if (!method.equals("DELETE")) { 298 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream(), "UTF-8")); 299 TextFormat.write004(writer, registry.metricFamilySamples()); 300 writer.flush(); 301 writer.close(); 302 } 303 304 int response = connection.getResponseCode(); 305 if (response != HttpURLConnection.HTTP_ACCEPTED) { 306 String errorMessage; 307 InputStream errorStream = connection.getErrorStream(); 308 if(response >= 400 && errorStream != null) { 309 String errBody = readFromStream(errorStream); 310 errorMessage = "Response code from " + url + " was " + response + ", response body: " + errBody; 311 } else { 312 errorMessage = "Response code from " + url + " was " + response; 313 } 314 throw new IOException(errorMessage); 315 } 316 } finally { 317 connection.disconnect(); 318 } 319 } 320 321 /** 322 * Returns a grouping key with the instance label set to the machine's IP address. 323 * <p> 324 * This is a convenience function, and should only be used where you want to 325 * push per-instance metrics rather than cluster/job level metrics. 326 */ 327 public static Map<String, String> instanceIPGroupingKey() throws UnknownHostException { 328 Map<String, String> groupingKey = new HashMap<String, String>(); 329 groupingKey.put("instance", InetAddress.getLocalHost().getHostAddress()); 330 return groupingKey; 331 } 332 333 private static String readFromStream(InputStream is) throws IOException { 334 ByteArrayOutputStream result = new ByteArrayOutputStream(); 335 byte[] buffer = new byte[1024]; 336 int length; 337 while ((length = is.read(buffer)) != -1) { 338 result.write(buffer, 0, length); 339 } 340 return result.toString("UTF-8"); 341 } 342}