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