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 InetSocketAddress inetSocketAddress = null; 107 private ExecutorService executorService = null; 108 private PrometheusRegistry registry = null; 109 private ExporterHttpServerProperties properties = null; 110 private Authenticator authenticator = null; 111 private HttpsConfigurator httpsConfigurator = null; 112 private HttpHandler defaultHandler = null; 113 114 private Builder(PrometheusProperties config) { 115 this.config = config; 116 } 117 118 /** 119 * Port to bind to. Default is 0, indicating that a random port will be selected. 120 * You can learn the randomly selected port by calling {@link HTTPServer#getPort()}. 121 */ 122 public Builder port(int port) { 123 this.port = port; 124 return this; 125 } 126 127 /** 128 * Use this hostname to resolve the IP address to bind to. 129 * Must not be called together with {@link #inetAddress(InetAddress)}. 130 * Default is empty, indicating that the HTTPServer binds to the wildcard address. 131 */ 132 public Builder hostname(String hostname) { 133 this.hostname = hostname; 134 return this; 135 } 136 137 /** 138 * Bind to this IP address. 139 * Must not be called together with {@link #hostname(String)}. 140 * Default is empty, indicating that the HTTPServer binds to the wildcard address. 141 */ 142 public Builder inetAddress(InetAddress address) { 143 this.inetAddress = address; 144 return this; 145 } 146 147 /** 148 * Optional: ExecutorService used by the {@code httpServer}. 149 */ 150 public Builder executorService(ExecutorService executorService) { 151 this.executorService = executorService; 152 return this; 153 } 154 155 /** 156 * Optional: Default is {@link PrometheusRegistry#defaultRegistry}. 157 */ 158 public Builder registry(PrometheusRegistry registry) { 159 this.registry = registry; 160 return this; 161 } 162 163 /** 164 * Optional: {@link Authenticator} for authentication. 165 */ 166 public Builder authenticator(Authenticator authenticator) { 167 this.authenticator = authenticator; 168 return this; 169 } 170 171 /** 172 * Optional: {@link HttpsConfigurator} for TLS/SSL 173 */ 174 public Builder httpsConfigurator(HttpsConfigurator configurator) { 175 this.httpsConfigurator = configurator; 176 return this; 177 } 178 179 /** 180 * Optional: Override default handler, i.e. the handler that will be registered for the / endpoint. 181 */ 182 public Builder defaultHandler(HttpHandler defaultHandler) { 183 this.defaultHandler = defaultHandler; 184 return this; 185 } 186 187 /** 188 * Build and start the HTTPServer. 189 */ 190 public HTTPServer buildAndStart() throws IOException { 191 if (registry == null) { 192 registry = PrometheusRegistry.defaultRegistry; 193 } 194 HttpServer httpServer; 195 if (httpsConfigurator != null) { 196 httpServer = HttpsServer.create(makeInetSocketAddress(), 3); 197 ((HttpsServer)httpServer).setHttpsConfigurator(httpsConfigurator); 198 } else { 199 httpServer = HttpServer.create(makeInetSocketAddress(), 3); 200 } 201 ExecutorService executorService = makeExecutorService(); 202 httpServer.setExecutor(executorService); 203 return new HTTPServer(config, executorService, httpServer, registry, authenticator, defaultHandler); 204 } 205 206 private InetSocketAddress makeInetSocketAddress() { 207 if (inetSocketAddress != null) { 208 assertNull(port, "cannot configure 'inetSocketAddress' and 'port' at the same time"); 209 assertNull(hostname, "cannot configure 'inetSocketAddress' and 'hostname' at the same time"); 210 assertNull(inetAddress, "cannot configure 'inetSocketAddress' and 'inetAddress' at the same time"); 211 return inetSocketAddress; 212 } else if (inetAddress != null) { 213 assertNull(hostname, "cannot configure 'inetAddress' and 'hostname' at the same time"); 214 return new InetSocketAddress(inetAddress, findPort()); 215 } else if (hostname != null) { 216 return new InetSocketAddress(hostname, findPort()); 217 } else { 218 return new InetSocketAddress(findPort()); 219 } 220 } 221 222 private ExecutorService makeExecutorService() { 223 if (executorService != null) { 224 return executorService; 225 } else { 226 return new ThreadPoolExecutor( 227 1, 228 10, 229 120, 230 TimeUnit.SECONDS, 231 new SynchronousQueue<>(true), 232 NamedDaemonThreadFactory.defaultThreadFactory(true), 233 new BlockingRejectedExecutionHandler()); 234 } 235 } 236 237 private int findPort() { 238 if (properties != null && properties.getPort() != null) { 239 return properties.getPort(); // you can overwrite the hard-coded port with properties. 240 } 241 if (port != null) { 242 return port; 243 } 244 return 0; // random port will be selected 245 } 246 247 private void assertNull(Object o, String msg) { 248 if (o != null) { 249 throw new IllegalStateException(msg); 250 } 251 } 252 } 253 254 private static class BlockingRejectedExecutionHandler implements RejectedExecutionHandler { 255 256 @Override 257 public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) { 258 if (!threadPoolExecutor.isShutdown()) { 259 try { 260 threadPoolExecutor.getQueue().put(runnable); 261 } catch (InterruptedException ignored) { 262 } 263 } 264 } 265 } 266}