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