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}