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