001package io.prometheus.metrics.exporter.httpserver; 002 003import com.sun.net.httpserver.Authenticator; 004import com.sun.net.httpserver.HttpContext; 005import com.sun.net.httpserver.HttpHandler; 006import com.sun.net.httpserver.HttpServer; 007import com.sun.net.httpserver.HttpsConfigurator; 008import com.sun.net.httpserver.HttpsServer; 009import io.prometheus.metrics.config.ExporterHttpServerProperties; 010import io.prometheus.metrics.config.PrometheusProperties; 011import io.prometheus.metrics.model.registry.PrometheusRegistry; 012 013import java.io.Closeable; 014import java.io.IOException; 015import java.net.InetAddress; 016import java.net.InetSocketAddress; 017import java.util.concurrent.ExecutorService; 018import java.util.concurrent.RejectedExecutionHandler; 019import java.util.concurrent.SynchronousQueue; 020import java.util.concurrent.ThreadPoolExecutor; 021import java.util.concurrent.TimeUnit; 022 023/** 024 * Expose Prometheus metrics using a plain Java HttpServer. 025 * <p> 026 * Example Usage: 027 * <pre> 028 * {@code 029 * HTTPServer server = HTTPServer.builder() 030 * .port(9090) 031 * .buildAndStart(); 032 * }</pre> 033 * */ 034public class HTTPServer implements Closeable { 035 036 static { 037 if (!System.getProperties().containsKey("sun.net.httpserver.maxReqTime")) { 038 System.setProperty("sun.net.httpserver.maxReqTime", "60"); 039 } 040 041 if (!System.getProperties().containsKey("sun.net.httpserver.maxRspTime")) { 042 System.setProperty("sun.net.httpserver.maxRspTime", "600"); 043 } 044 } 045 046 protected final HttpServer server; 047 protected final ExecutorService executorService; 048 049 private HTTPServer(PrometheusProperties config, ExecutorService executorService, HttpServer httpServer, PrometheusRegistry registry, Authenticator authenticator, HttpHandler defaultHandler) { 050 if (httpServer.getAddress() == null) { 051 throw new IllegalArgumentException("HttpServer hasn't been bound to an address"); 052 } 053 this.server = httpServer; 054 this.executorService = executorService; 055 registerHandler("/", defaultHandler == null ? new DefaultHandler() : defaultHandler, authenticator); 056 registerHandler("/metrics", new MetricsHandler(config, registry), authenticator); 057 registerHandler("/-/healthy", new HealthyHandler(), authenticator); 058 this.server.start(); 059 } 060 061 private void registerHandler(String path, HttpHandler handler, Authenticator authenticator) { 062 HttpContext context = server.createContext(path, handler); 063 if (authenticator != null) { 064 context.setAuthenticator(authenticator); 065 } 066 } 067 068 /** 069 * Stop the HTTP server. Same as {@link #close()}. 070 */ 071 public void stop() { 072 close(); 073 } 074 075 /** 076 * Stop the HTTPServer. Same as {@link #stop()}. 077 */ 078 @Override 079 public void close() { 080 server.stop(0); 081 executorService.shutdown(); // Free any (parked/idle) threads in pool 082 } 083 084 /** 085 * Gets the port number. 086 * This is useful if you did not specify a port and the server picked a free port automatically. 087 */ 088 public int getPort() { 089 return server.getAddress().getPort(); 090 } 091 092 public static Builder builder() { 093 return new Builder(PrometheusProperties.get()); 094 } 095 096 public static Builder builder(PrometheusProperties config) { 097 return new Builder(config); 098 } 099 100 public static class Builder { 101 102 private final PrometheusProperties config; 103 private Integer port = null; 104 private String hostname = null; 105 private InetAddress inetAddress = null; 106 private ExecutorService executorService = null; 107 private PrometheusRegistry registry = null; 108 private Authenticator authenticator = null; 109 private HttpsConfigurator httpsConfigurator = null; 110 private HttpHandler defaultHandler = null; 111 112 private Builder(PrometheusProperties config) { 113 this.config = config; 114 } 115 116 /** 117 * Port to bind to. Default is 0, indicating that a random port will be selected. 118 * You can learn the randomly selected port by calling {@link HTTPServer#getPort()}. 119 */ 120 public Builder port(int port) { 121 this.port = port; 122 return this; 123 } 124 125 /** 126 * Use this hostname to resolve the IP address to bind to. 127 * Must not be called together with {@link #inetAddress(InetAddress)}. 128 * Default is empty, indicating that the HTTPServer binds to the wildcard address. 129 */ 130 public Builder hostname(String hostname) { 131 this.hostname = hostname; 132 return this; 133 } 134 135 /** 136 * Bind to this IP address. 137 * Must not be called together with {@link #hostname(String)}. 138 * Default is empty, indicating that the HTTPServer binds to the wildcard address. 139 */ 140 public Builder inetAddress(InetAddress address) { 141 this.inetAddress = address; 142 return this; 143 } 144 145 /** 146 * Optional: ExecutorService used by the {@code httpServer}. 147 */ 148 public Builder executorService(ExecutorService executorService) { 149 this.executorService = executorService; 150 return this; 151 } 152 153 /** 154 * Optional: Default is {@link PrometheusRegistry#defaultRegistry}. 155 */ 156 public Builder registry(PrometheusRegistry registry) { 157 this.registry = registry; 158 return this; 159 } 160 161 /** 162 * Optional: {@link Authenticator} for authentication. 163 */ 164 public Builder authenticator(Authenticator authenticator) { 165 this.authenticator = authenticator; 166 return this; 167 } 168 169 /** 170 * Optional: {@link HttpsConfigurator} for TLS/SSL 171 */ 172 public Builder httpsConfigurator(HttpsConfigurator configurator) { 173 this.httpsConfigurator = configurator; 174 return this; 175 } 176 177 /** 178 * Optional: Override default handler, i.e. the handler that will be registered for the / endpoint. 179 */ 180 public Builder defaultHandler(HttpHandler defaultHandler) { 181 this.defaultHandler = defaultHandler; 182 return this; 183 } 184 185 /** 186 * Build and start the HTTPServer. 187 */ 188 public HTTPServer buildAndStart() throws IOException { 189 if (registry == null) { 190 registry = PrometheusRegistry.defaultRegistry; 191 } 192 HttpServer httpServer; 193 if (httpsConfigurator != null) { 194 httpServer = HttpsServer.create(makeInetSocketAddress(), 3); 195 ((HttpsServer)httpServer).setHttpsConfigurator(httpsConfigurator); 196 } else { 197 httpServer = HttpServer.create(makeInetSocketAddress(), 3); 198 } 199 ExecutorService executorService = makeExecutorService(); 200 httpServer.setExecutor(executorService); 201 return new HTTPServer(config, executorService, httpServer, registry, authenticator, defaultHandler); 202 } 203 204 private InetSocketAddress makeInetSocketAddress() { 205 if (inetAddress != null) { 206 assertNull(hostname, "cannot configure 'inetAddress' and 'hostname' at the same time"); 207 return new InetSocketAddress(inetAddress, findPort()); 208 } else if (hostname != null) { 209 return new InetSocketAddress(hostname, findPort()); 210 } else { 211 return new InetSocketAddress(findPort()); 212 } 213 } 214 215 private ExecutorService makeExecutorService() { 216 if (executorService != null) { 217 return executorService; 218 } else { 219 return new ThreadPoolExecutor( 220 1, 221 10, 222 120, 223 TimeUnit.SECONDS, 224 new SynchronousQueue<>(true), 225 NamedDaemonThreadFactory.defaultThreadFactory(true), 226 new BlockingRejectedExecutionHandler()); 227 } 228 } 229 230 private int findPort() { 231 if (config != null && config.getExporterHttpServerProperties() != null) { 232 Integer port = config.getExporterHttpServerProperties().getPort(); 233 if (port != null) { 234 return port; 235 } 236 } 237 if (port != null) { 238 return port; 239 } 240 return 0; // random port will be selected 241 } 242 243 private void assertNull(Object o, String msg) { 244 if (o != null) { 245 throw new IllegalStateException(msg); 246 } 247 } 248 } 249 250 private static class BlockingRejectedExecutionHandler implements RejectedExecutionHandler { 251 252 @Override 253 public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) { 254 if (!threadPoolExecutor.isShutdown()) { 255 try { 256 threadPoolExecutor.getQueue().put(runnable); 257 } catch (InterruptedException ignored) { 258 } 259 } 260 } 261 } 262}