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