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