001package io.prometheus.metrics.exporter.httpserver; 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.metrics.config.PrometheusProperties; 011import io.prometheus.metrics.model.registry.PrometheusRegistry; 012import java.io.Closeable; 013import java.io.IOException; 014import java.io.InputStream; 015import java.net.InetAddress; 016import java.net.InetSocketAddress; 017import java.security.PrivilegedActionException; 018import java.security.PrivilegedExceptionAction; 019import java.util.concurrent.ExecutionException; 020import java.util.concurrent.ExecutorService; 021import java.util.concurrent.SynchronousQueue; 022import java.util.concurrent.ThreadPoolExecutor; 023import java.util.concurrent.TimeUnit; 024import javax.security.auth.Subject; 025 026/** 027 * Expose Prometheus metrics using a plain Java HttpServer. 028 * 029 * <p>Example Usage: 030 * 031 * <pre>{@code 032 * HTTPServer server = HTTPServer.builder() 033 * .port(9090) 034 * .buildAndStart(); 035 * }</pre> 036 */ 037public class HTTPServer implements Closeable { 038 039 static { 040 if (!System.getProperties().containsKey("sun.net.httpserver.maxReqTime")) { 041 System.setProperty("sun.net.httpserver.maxReqTime", "60"); 042 } 043 044 if (!System.getProperties().containsKey("sun.net.httpserver.maxRspTime")) { 045 System.setProperty("sun.net.httpserver.maxRspTime", "600"); 046 } 047 } 048 049 protected final HttpServer server; 050 protected final ExecutorService executorService; 051 052 private HTTPServer( 053 PrometheusProperties config, 054 ExecutorService executorService, 055 HttpServer httpServer, 056 PrometheusRegistry registry, 057 Authenticator authenticator, 058 String authenticatedSubjectAttributeName, 059 HttpHandler defaultHandler) { 060 if (httpServer.getAddress() == null) { 061 throw new IllegalArgumentException("HttpServer hasn't been bound to an address"); 062 } 063 this.server = httpServer; 064 this.executorService = executorService; 065 registerHandler( 066 "/", 067 defaultHandler == null ? new DefaultHandler() : defaultHandler, 068 authenticator, 069 authenticatedSubjectAttributeName); 070 registerHandler( 071 "/metrics", 072 new MetricsHandler(config, registry), 073 authenticator, 074 authenticatedSubjectAttributeName); 075 registerHandler( 076 "/-/healthy", new HealthyHandler(), authenticator, authenticatedSubjectAttributeName); 077 try { 078 // HttpServer.start() starts the HttpServer in a new background thread. 079 // If we call HttpServer.start() from a thread of the executorService, 080 // the background thread will inherit the "daemon" property, 081 // i.e. the server will run as a Daemon thread. 082 // See https://github.com/prometheus/client_java/pull/955 083 this.executorService.submit(this.server::start).get(); 084 // calling .get() on the Future here to avoid silently discarding errors 085 } catch (InterruptedException | ExecutionException e) { 086 throw new RuntimeException(e); 087 } 088 } 089 090 private void registerHandler( 091 String path, HttpHandler handler, Authenticator authenticator, String subjectAttributeName) { 092 HttpContext context = server.createContext(path, wrapWithDoAs(handler, subjectAttributeName)); 093 if (authenticator != null) { 094 context.setAuthenticator(authenticator); 095 } 096 } 097 098 private HttpHandler wrapWithDoAs(HttpHandler handler, String subjectAttributeName) { 099 if (subjectAttributeName == null) { 100 return handler; 101 } 102 103 // invoke handler using the subject.doAs from the named attribute 104 return new HttpHandler() { 105 @Override 106 public void handle(HttpExchange exchange) throws IOException { 107 Object authSubject = exchange.getAttribute(subjectAttributeName); 108 if (authSubject instanceof Subject) { 109 try { 110 Subject.doAs( 111 (Subject) authSubject, 112 (PrivilegedExceptionAction<IOException>) 113 () -> { 114 handler.handle(exchange); 115 return null; 116 }); 117 } catch (PrivilegedActionException e) { 118 if (e.getException() != null) { 119 throw new IOException(e.getException()); 120 } else { 121 throw new IOException(e); 122 } 123 } 124 } else { 125 drainInputAndClose(exchange); 126 exchange.sendResponseHeaders(403, -1); 127 } 128 } 129 }; 130 } 131 132 private void drainInputAndClose(HttpExchange httpExchange) throws IOException { 133 InputStream inputStream = httpExchange.getRequestBody(); 134 byte[] b = new byte[4096]; 135 while (inputStream.read(b) != -1) { 136 // nop 137 } 138 inputStream.close(); 139 } 140 141 /** Stop the HTTP server. Same as {@link #close()}. */ 142 public void stop() { 143 close(); 144 } 145 146 /** Stop the HTTPServer. Same as {@link #stop()}. */ 147 @Override 148 public void close() { 149 server.stop(0); 150 executorService.shutdown(); // Free any (parked/idle) threads in pool 151 } 152 153 /** 154 * Gets the port number. This is useful if you did not specify a port and the server picked a free 155 * port automatically. 156 */ 157 public int getPort() { 158 return server.getAddress().getPort(); 159 } 160 161 public static Builder builder() { 162 return new Builder(PrometheusProperties.get()); 163 } 164 165 public static Builder builder(PrometheusProperties config) { 166 return new Builder(config); 167 } 168 169 public static class Builder { 170 171 private final PrometheusProperties config; 172 private Integer port = null; 173 private String hostname = null; 174 private InetAddress inetAddress = null; 175 private ExecutorService executorService = null; 176 private PrometheusRegistry registry = null; 177 private Authenticator authenticator = null; 178 private HttpsConfigurator httpsConfigurator = null; 179 private HttpHandler defaultHandler = null; 180 private String authenticatedSubjectAttributeName = null; 181 182 private Builder(PrometheusProperties config) { 183 this.config = config; 184 } 185 186 /** 187 * Port to bind to. Default is 0, indicating that a random port will be selected. You can learn 188 * the randomly selected port by calling {@link HTTPServer#getPort()}. 189 */ 190 public Builder port(int port) { 191 this.port = port; 192 return this; 193 } 194 195 /** 196 * Use this hostname to resolve the IP address to bind to. Must not be called together with 197 * {@link #inetAddress(InetAddress)}. Default is empty, indicating that the HTTPServer binds to 198 * the wildcard address. 199 */ 200 public Builder hostname(String hostname) { 201 this.hostname = hostname; 202 return this; 203 } 204 205 /** 206 * Bind to this IP address. Must not be called together with {@link #hostname(String)}. Default 207 * is empty, indicating that the HTTPServer binds to the wildcard address. 208 */ 209 public Builder inetAddress(InetAddress address) { 210 this.inetAddress = address; 211 return this; 212 } 213 214 /** Optional: ExecutorService used by the {@code httpServer}. */ 215 public Builder executorService(ExecutorService executorService) { 216 this.executorService = executorService; 217 return this; 218 } 219 220 /** Optional: Default is {@link PrometheusRegistry#defaultRegistry}. */ 221 public Builder registry(PrometheusRegistry registry) { 222 this.registry = registry; 223 return this; 224 } 225 226 /** Optional: {@link Authenticator} for authentication. */ 227 public Builder authenticator(Authenticator authenticator) { 228 this.authenticator = authenticator; 229 return this; 230 } 231 232 /** Optional: the attribute name of a Subject from a custom authenticator. */ 233 public Builder authenticatedSubjectAttributeName(String authenticatedSubjectAttributeName) { 234 this.authenticatedSubjectAttributeName = authenticatedSubjectAttributeName; 235 return this; 236 } 237 238 /** Optional: {@link HttpsConfigurator} for TLS/SSL */ 239 public Builder httpsConfigurator(HttpsConfigurator configurator) { 240 this.httpsConfigurator = configurator; 241 return this; 242 } 243 244 /** 245 * Optional: Override default handler, i.e. the handler that will be registered for the / 246 * endpoint. 247 */ 248 public Builder defaultHandler(HttpHandler defaultHandler) { 249 this.defaultHandler = defaultHandler; 250 return this; 251 } 252 253 /** Build and start the HTTPServer. */ 254 public HTTPServer buildAndStart() throws IOException { 255 if (registry == null) { 256 registry = PrometheusRegistry.defaultRegistry; 257 } 258 HttpServer httpServer; 259 if (httpsConfigurator != null) { 260 httpServer = HttpsServer.create(makeInetSocketAddress(), 3); 261 ((HttpsServer) httpServer).setHttpsConfigurator(httpsConfigurator); 262 } else { 263 httpServer = HttpServer.create(makeInetSocketAddress(), 3); 264 } 265 ExecutorService executorService = makeExecutorService(); 266 httpServer.setExecutor(executorService); 267 return new HTTPServer( 268 config, 269 executorService, 270 httpServer, 271 registry, 272 authenticator, 273 authenticatedSubjectAttributeName, 274 defaultHandler); 275 } 276 277 private InetSocketAddress makeInetSocketAddress() { 278 if (inetAddress != null) { 279 assertNull(hostname, "cannot configure 'inetAddress' and 'hostname' at the same time"); 280 return new InetSocketAddress(inetAddress, findPort()); 281 } else if (hostname != null) { 282 return new InetSocketAddress(hostname, findPort()); 283 } else { 284 return new InetSocketAddress(findPort()); 285 } 286 } 287 288 private ExecutorService makeExecutorService() { 289 if (executorService != null) { 290 return executorService; 291 } else { 292 return new ThreadPoolExecutor( 293 1, 294 10, 295 120, 296 TimeUnit.SECONDS, 297 new SynchronousQueue<>(true), 298 NamedDaemonThreadFactory.defaultThreadFactory(true), 299 new BlockingRejectedExecutionHandler()); 300 } 301 } 302 303 private int findPort() { 304 if (config != null && config.getExporterHttpServerProperties() != null) { 305 Integer port = config.getExporterHttpServerProperties().getPort(); 306 if (port != null) { 307 return port; 308 } 309 } 310 if (port != null) { 311 return port; 312 } 313 return 0; // random port will be selected 314 } 315 316 private void assertNull(Object o, String msg) { 317 if (o != null) { 318 throw new IllegalStateException(msg); 319 } 320 } 321 } 322}