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