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}