001package io.prometheus.client.exporter; 002 003import io.prometheus.client.CollectorRegistry; 004import io.prometheus.client.SampleNameFilter; 005import io.prometheus.client.Predicate; 006import io.prometheus.client.Supplier; 007import io.prometheus.client.exporter.common.TextFormat; 008 009import java.io.ByteArrayOutputStream; 010import java.io.Closeable; 011import java.io.IOException; 012import java.io.OutputStreamWriter; 013import java.net.HttpURLConnection; 014import java.net.InetAddress; 015import java.net.InetSocketAddress; 016import java.net.URLDecoder; 017import java.nio.charset.Charset; 018import java.util.HashSet; 019import java.util.List; 020import java.util.Set; 021import java.util.concurrent.ExecutionException; 022import java.util.concurrent.ExecutorService; 023import java.util.concurrent.Executors; 024import java.util.concurrent.FutureTask; 025import java.util.concurrent.ThreadFactory; 026import java.util.concurrent.atomic.AtomicInteger; 027import java.util.zip.GZIPOutputStream; 028 029import com.sun.net.httpserver.Authenticator; 030import com.sun.net.httpserver.HttpContext; 031import com.sun.net.httpserver.HttpExchange; 032import com.sun.net.httpserver.HttpHandler; 033import com.sun.net.httpserver.HttpServer; 034 035/** 036 * Expose Prometheus metrics using a plain Java HttpServer. 037 * <p> 038 * Example Usage: 039 * <pre> 040 * {@code 041 * HTTPServer server = new HTTPServer(1234); 042 * } 043 * </pre> 044 * */ 045public class HTTPServer implements Closeable { 046 047 static { 048 if (!System.getProperties().containsKey("sun.net.httpserver.maxReqTime")) { 049 System.setProperty("sun.net.httpserver.maxReqTime", "60"); 050 } 051 052 if (!System.getProperties().containsKey("sun.net.httpserver.maxRspTime")) { 053 System.setProperty("sun.net.httpserver.maxRspTime", "600"); 054 } 055 } 056 057 private static class LocalByteArray extends ThreadLocal<ByteArrayOutputStream> { 058 @Override 059 protected ByteArrayOutputStream initialValue() 060 { 061 return new ByteArrayOutputStream(1 << 20); 062 } 063 } 064 065 /** 066 * Handles Metrics collections from the given registry. 067 */ 068 public static class HTTPMetricHandler implements HttpHandler { 069 private final CollectorRegistry registry; 070 private final LocalByteArray response = new LocalByteArray(); 071 private final Supplier<Predicate<String>> sampleNameFilterSupplier; 072 private final static String HEALTHY_RESPONSE = "Exporter is Healthy."; 073 074 public HTTPMetricHandler(CollectorRegistry registry) { 075 this(registry, null); 076 } 077 078 public HTTPMetricHandler(CollectorRegistry registry, Supplier<Predicate<String>> sampleNameFilterSupplier) { 079 this.registry = registry; 080 this.sampleNameFilterSupplier = sampleNameFilterSupplier; 081 } 082 083 @Override 084 public void handle(HttpExchange t) throws IOException { 085 String query = t.getRequestURI().getRawQuery(); 086 String contextPath = t.getHttpContext().getPath(); 087 ByteArrayOutputStream response = this.response.get(); 088 response.reset(); 089 OutputStreamWriter osw = new OutputStreamWriter(response, Charset.forName("UTF-8")); 090 if ("/-/healthy".equals(contextPath)) { 091 osw.write(HEALTHY_RESPONSE); 092 } else { 093 String contentType = TextFormat.chooseContentType(t.getRequestHeaders().getFirst("Accept")); 094 t.getResponseHeaders().set("Content-Type", contentType); 095 Predicate<String> filter = sampleNameFilterSupplier == null ? null : sampleNameFilterSupplier.get(); 096 filter = SampleNameFilter.restrictToNamesEqualTo(filter, parseQuery(query)); 097 if (filter == null) { 098 TextFormat.writeFormat(contentType, osw, registry.metricFamilySamples()); 099 } else { 100 TextFormat.writeFormat(contentType, osw, registry.filteredMetricFamilySamples(filter)); 101 } 102 } 103 104 osw.close(); 105 106 if (shouldUseCompression(t)) { 107 t.getResponseHeaders().set("Content-Encoding", "gzip"); 108 t.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0); 109 final GZIPOutputStream os = new GZIPOutputStream(t.getResponseBody()); 110 try { 111 response.writeTo(os); 112 } finally { 113 os.close(); 114 } 115 } else { 116 long contentLength = response.size(); 117 t.getResponseHeaders().set("Content-Length", String.valueOf(contentLength)); 118 if (t.getRequestMethod().equals("HEAD")) { 119 contentLength = -1; 120 } 121 t.sendResponseHeaders(HttpURLConnection.HTTP_OK, contentLength); 122 response.writeTo(t.getResponseBody()); 123 } 124 t.close(); 125 } 126 } 127 128 protected static boolean shouldUseCompression(HttpExchange exchange) { 129 List<String> encodingHeaders = exchange.getRequestHeaders().get("Accept-Encoding"); 130 if (encodingHeaders == null) return false; 131 132 for (String encodingHeader : encodingHeaders) { 133 String[] encodings = encodingHeader.split(","); 134 for (String encoding : encodings) { 135 if (encoding.trim().equalsIgnoreCase("gzip")) { 136 return true; 137 } 138 } 139 } 140 return false; 141 } 142 143 protected static Set<String> parseQuery(String query) throws IOException { 144 Set<String> names = new HashSet<String>(); 145 if (query != null) { 146 String[] pairs = query.split("&"); 147 for (String pair : pairs) { 148 int idx = pair.indexOf("="); 149 if (idx != -1 && URLDecoder.decode(pair.substring(0, idx), "UTF-8").equals("name[]")) { 150 names.add(URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); 151 } 152 } 153 } 154 return names; 155 } 156 157 158 static class NamedDaemonThreadFactory implements ThreadFactory { 159 private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1); 160 161 private final int poolNumber = POOL_NUMBER.getAndIncrement(); 162 private final AtomicInteger threadNumber = new AtomicInteger(1); 163 private final ThreadFactory delegate; 164 private final boolean daemon; 165 166 NamedDaemonThreadFactory(ThreadFactory delegate, boolean daemon) { 167 this.delegate = delegate; 168 this.daemon = daemon; 169 } 170 171 @Override 172 public Thread newThread(Runnable r) { 173 Thread t = delegate.newThread(r); 174 t.setName(String.format("prometheus-http-%d-%d", poolNumber, threadNumber.getAndIncrement())); 175 t.setDaemon(daemon); 176 return t; 177 } 178 179 static ThreadFactory defaultThreadFactory(boolean daemon) { 180 return new NamedDaemonThreadFactory(Executors.defaultThreadFactory(), daemon); 181 } 182 } 183 184 protected final HttpServer server; 185 protected final ExecutorService executorService; 186 187 /** 188 * We keep the original constructors of {@link HTTPServer} for compatibility, but new configuration 189 * parameters like {@code sampleNameFilter} must be configured using the Builder. 190 */ 191 public static class Builder { 192 193 private int port = 0; 194 private String hostname = null; 195 private InetAddress inetAddress = null; 196 private InetSocketAddress inetSocketAddress = null; 197 private HttpServer httpServer = null; 198 private CollectorRegistry registry = CollectorRegistry.defaultRegistry; 199 private boolean daemon = false; 200 private Predicate<String> sampleNameFilter; 201 private Supplier<Predicate<String>> sampleNameFilterSupplier; 202 private Authenticator authenticator; 203 204 /** 205 * Port to bind to. Must not be called together with {@link #withInetSocketAddress(InetSocketAddress)} 206 * or {@link #withHttpServer(HttpServer)}. Default is 0, indicating that a random port will be selected. 207 */ 208 public Builder withPort(int port) { 209 this.port = port; 210 return this; 211 } 212 213 /** 214 * Use this hostname to resolve the IP address to bind to. Must not be called together with 215 * {@link #withInetAddress(InetAddress)} or {@link #withInetSocketAddress(InetSocketAddress)} 216 * or {@link #withHttpServer(HttpServer)}. 217 * Default is empty, indicating that the HTTPServer binds to the wildcard address. 218 */ 219 public Builder withHostname(String hostname) { 220 this.hostname = hostname; 221 return this; 222 } 223 224 /** 225 * Bind to this IP address. Must not be called together with {@link #withHostname(String)} or 226 * {@link #withInetSocketAddress(InetSocketAddress)} or {@link #withHttpServer(HttpServer)}. 227 * Default is empty, indicating that the HTTPServer binds to the wildcard address. 228 */ 229 public Builder withInetAddress(InetAddress address) { 230 this.inetAddress = address; 231 return this; 232 } 233 234 /** 235 * Listen on this address. Must not be called together with {@link #withPort(int)}, 236 * {@link #withHostname(String)}, {@link #withInetAddress(InetAddress)}, or {@link #withHttpServer(HttpServer)}. 237 */ 238 public Builder withInetSocketAddress(InetSocketAddress address) { 239 this.inetSocketAddress = address; 240 return this; 241 } 242 243 /** 244 * Use this httpServer. The {@code httpServer} is expected to already be bound to an address. 245 * Must not be called together with {@link #withPort(int)}, or {@link #withHostname(String)}, 246 * or {@link #withInetAddress(InetAddress)}, or {@link #withInetSocketAddress(InetSocketAddress)}. 247 */ 248 public Builder withHttpServer(HttpServer httpServer) { 249 this.httpServer = httpServer; 250 return this; 251 } 252 253 /** 254 * By default, the {@link HTTPServer} uses non-daemon threads. Set this to {@code true} to 255 * run the {@link HTTPServer} with daemon threads. 256 */ 257 public Builder withDaemonThreads(boolean daemon) { 258 this.daemon = daemon; 259 return this; 260 } 261 262 /** 263 * Optional: Only export time series where {@code sampleNameFilter.test(name)} returns true. 264 * <p> 265 * Use this if the sampleNameFilter remains the same throughout the lifetime of the HTTPServer. 266 * If the sampleNameFilter changes during runtime, use {@link #withSampleNameFilterSupplier(Supplier)}. 267 */ 268 public Builder withSampleNameFilter(Predicate<String> sampleNameFilter) { 269 this.sampleNameFilter = sampleNameFilter; 270 return this; 271 } 272 273 /** 274 * Optional: Only export time series where {@code sampleNameFilter.test(name)} returns true. 275 * <p> 276 * Use this if the sampleNameFilter may change during runtime, like for example if you have a 277 * hot reload mechanism for your filter config. 278 * If the sampleNameFilter remains the same throughout the lifetime of the HTTPServer, 279 * use {@link #withSampleNameFilter(Predicate)} instead. 280 */ 281 public Builder withSampleNameFilterSupplier(Supplier<Predicate<String>> sampleNameFilterSupplier) { 282 this.sampleNameFilterSupplier = sampleNameFilterSupplier; 283 return this; 284 } 285 286 /** 287 * Optional: Default is {@link CollectorRegistry#defaultRegistry}. 288 */ 289 public Builder withRegistry(CollectorRegistry registry) { 290 this.registry = registry; 291 return this; 292 } 293 294 /** 295 * Optional: {@link Authenticator} to use to support authentication. 296 */ 297 public Builder withAuthenticator(Authenticator authenticator) { 298 this.authenticator = authenticator; 299 return this; 300 } 301 302 /** 303 * Build the HTTPServer 304 * @throws IOException 305 */ 306 public HTTPServer build() throws IOException { 307 if (sampleNameFilter != null) { 308 assertNull(sampleNameFilterSupplier, "cannot configure 'sampleNameFilter' and 'sampleNameFilterSupplier' at the same time"); 309 sampleNameFilterSupplier = SampleNameFilterSupplier.of(sampleNameFilter); 310 } 311 if (httpServer != null) { 312 assertZero(port, "cannot configure 'httpServer' and 'port' at the same time"); 313 assertNull(hostname, "cannot configure 'httpServer' and 'hostname' at the same time"); 314 assertNull(inetAddress, "cannot configure 'httpServer' and 'inetAddress' at the same time"); 315 assertNull(inetSocketAddress, "cannot configure 'httpServer' and 'inetSocketAddress' at the same time"); 316 return new HTTPServer(httpServer, registry, daemon, sampleNameFilterSupplier, authenticator); 317 } else if (inetSocketAddress != null) { 318 assertZero(port, "cannot configure 'inetSocketAddress' and 'port' at the same time"); 319 assertNull(hostname, "cannot configure 'inetSocketAddress' and 'hostname' at the same time"); 320 assertNull(inetAddress, "cannot configure 'inetSocketAddress' and 'inetAddress' at the same time"); 321 } else if (inetAddress != null) { 322 assertNull(hostname, "cannot configure 'inetAddress' and 'hostname' at the same time"); 323 inetSocketAddress = new InetSocketAddress(inetAddress, port); 324 } else if (hostname != null) { 325 inetSocketAddress = new InetSocketAddress(hostname, port); 326 } else { 327 inetSocketAddress = new InetSocketAddress(port); 328 } 329 return new HTTPServer(HttpServer.create(inetSocketAddress, 3), registry, daemon, sampleNameFilterSupplier, authenticator); 330 } 331 332 private void assertNull(Object o, String msg) { 333 if (o != null) { 334 throw new IllegalStateException(msg); 335 } 336 } 337 338 private void assertZero(int i, String msg) { 339 if (i != 0) { 340 throw new IllegalStateException(msg); 341 } 342 } 343 } 344 345 /** 346 * Start an HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}. 347 * The {@code httpServer} is expected to already be bound to an address 348 */ 349 public HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon) throws IOException { 350 this(httpServer, registry, daemon, null, null); 351 } 352 353 /** 354 * Start an HTTP server serving Prometheus metrics from the given registry. 355 */ 356 public HTTPServer(InetSocketAddress addr, CollectorRegistry registry, boolean daemon) throws IOException { 357 this(HttpServer.create(addr, 3), registry, daemon); 358 } 359 360 /** 361 * Start an HTTP server serving Prometheus metrics from the given registry using non-daemon threads. 362 */ 363 public HTTPServer(InetSocketAddress addr, CollectorRegistry registry) throws IOException { 364 this(addr, registry, false); 365 } 366 367 /** 368 * Start an HTTP server serving the default Prometheus registry. 369 */ 370 public HTTPServer(int port, boolean daemon) throws IOException { 371 this(new InetSocketAddress(port), CollectorRegistry.defaultRegistry, daemon); 372 } 373 374 /** 375 * Start an HTTP server serving the default Prometheus registry using non-daemon threads. 376 */ 377 public HTTPServer(int port) throws IOException { 378 this(port, false); 379 } 380 381 /** 382 * Start an HTTP server serving the default Prometheus registry. 383 */ 384 public HTTPServer(String host, int port, boolean daemon) throws IOException { 385 this(new InetSocketAddress(host, port), CollectorRegistry.defaultRegistry, daemon); 386 } 387 388 /** 389 * Start an HTTP server serving the default Prometheus registry using non-daemon threads. 390 */ 391 public HTTPServer(String host, int port) throws IOException { 392 this(new InetSocketAddress(host, port), CollectorRegistry.defaultRegistry, false); 393 } 394 395 private HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon, Supplier<Predicate<String>> sampleNameFilterSupplier, Authenticator authenticator) { 396 if (httpServer.getAddress() == null) 397 throw new IllegalArgumentException("HttpServer hasn't been bound to an address"); 398 399 server = httpServer; 400 HttpHandler mHandler = new HTTPMetricHandler(registry, sampleNameFilterSupplier); 401 HttpContext mContext = server.createContext("/", mHandler); 402 if (authenticator != null) { 403 mContext.setAuthenticator(authenticator); 404 } 405 mContext = server.createContext("/metrics", mHandler); 406 if (authenticator != null) { 407 mContext.setAuthenticator(authenticator); 408 } 409 mContext = server.createContext("/-/healthy", mHandler); 410 if (authenticator != null) { 411 mContext.setAuthenticator(authenticator); 412 } 413 executorService = Executors.newFixedThreadPool(5, NamedDaemonThreadFactory.defaultThreadFactory(daemon)); 414 server.setExecutor(executorService); 415 start(daemon); 416 } 417 418 /** 419 * Start an HTTP server by making sure that its background thread inherit proper daemon flag. 420 */ 421 private void start(boolean daemon) { 422 if (daemon == Thread.currentThread().isDaemon()) { 423 server.start(); 424 } else { 425 FutureTask<Void> startTask = new FutureTask<Void>(new Runnable() { 426 @Override 427 public void run() { 428 server.start(); 429 } 430 }, null); 431 NamedDaemonThreadFactory.defaultThreadFactory(daemon).newThread(startTask).start(); 432 try { 433 startTask.get(); 434 } catch (ExecutionException e) { 435 throw new RuntimeException("Unexpected exception on starting HTTPSever", e); 436 } catch (InterruptedException e) { 437 // This is possible only if the current tread has been interrupted, 438 // but in real use cases this should not happen. 439 // In any case, there is nothing to do, except to propagate interrupted flag. 440 Thread.currentThread().interrupt(); 441 } 442 } 443 } 444 445 /** 446 * Stop the HTTP server. 447 * @deprecated renamed to close(), so that the HTTPServer can be used in try-with-resources. 448 */ 449 public void stop() { 450 close(); 451 } 452 453 /** 454 * Stop the HTTPServer. 455 */ 456 @Override 457 public void close() { 458 server.stop(0); 459 executorService.shutdown(); // Free any (parked/idle) threads in pool 460 } 461 462 /** 463 * Gets the port number. 464 */ 465 public int getPort() { 466 return server.getAddress().getPort(); 467 } 468}