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