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