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}