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