001package io.prometheus.metrics.exporter.httpserver;
002
003import com.sun.net.httpserver.Authenticator;
004import com.sun.net.httpserver.HttpContext;
005import com.sun.net.httpserver.HttpHandler;
006import com.sun.net.httpserver.HttpServer;
007import com.sun.net.httpserver.HttpsConfigurator;
008import com.sun.net.httpserver.HttpsServer;
009import io.prometheus.metrics.config.ExporterHttpServerProperties;
010import io.prometheus.metrics.config.PrometheusProperties;
011import io.prometheus.metrics.model.registry.PrometheusRegistry;
012
013import java.io.Closeable;
014import java.io.IOException;
015import java.net.InetAddress;
016import java.net.InetSocketAddress;
017import java.util.concurrent.ExecutorService;
018import java.util.concurrent.Executors;
019import java.util.concurrent.ThreadPoolExecutor;
020import java.util.concurrent.TimeUnit;
021
022/**
023 * Expose Prometheus metrics using a plain Java HttpServer.
024 * <p>
025 * Example Usage:
026 * <pre>
027 * {@code
028 * HTTPServer server = HTTPServer.newBuilder()
029 *     .withPort(9090)
030 *     .buildAndStart();
031 * }</pre>
032 * */
033public class HTTPServer implements Closeable {
034
035    static {
036        if (!System.getProperties().containsKey("sun.net.httpserver.maxReqTime")) {
037            System.setProperty("sun.net.httpserver.maxReqTime", "60");
038        }
039
040        if (!System.getProperties().containsKey("sun.net.httpserver.maxRspTime")) {
041            System.setProperty("sun.net.httpserver.maxRspTime", "600");
042        }
043    }
044
045    protected final HttpServer server;
046    protected final ExecutorService executorService;
047
048    private HTTPServer(PrometheusProperties config, ExecutorService executorService, HttpServer httpServer, PrometheusRegistry registry, Authenticator authenticator, HttpHandler defaultHandler) {
049        if (httpServer.getAddress() == null) {
050            throw new IllegalArgumentException("HttpServer hasn't been bound to an address");
051        }
052        this.server = httpServer;
053        this.executorService = executorService;
054        registerHandler("/", defaultHandler == null ? new DefaultHandler() : defaultHandler, authenticator);
055        registerHandler("/metrics", new MetricsHandler(config, registry), authenticator);
056        registerHandler("/-/healthy", new HealthyHandler(), authenticator);
057        this.server.start();
058    }
059
060    private void registerHandler(String path, HttpHandler handler, Authenticator authenticator) {
061        HttpContext context = server.createContext(path, handler);
062        if (authenticator != null) {
063            context.setAuthenticator(authenticator);
064        }
065    }
066
067    /**
068     * Stop the HTTP server. Same as {@link #close()}.
069     */
070    public void stop() {
071        close();
072    }
073
074    /**
075     * Stop the HTTPServer. Same as {@link #stop()}.
076     */
077    @Override
078    public void close() {
079        server.stop(0);
080        executorService.shutdown(); // Free any (parked/idle) threads in pool
081    }
082
083    /**
084     * Gets the port number.
085     * This is useful if you did not specify a port and the server picked a free port automatically.
086     */
087    public int getPort() {
088        return server.getAddress().getPort();
089    }
090
091    public static Builder newBuilder() {
092        return new Builder(PrometheusProperties.get());
093    }
094
095    public static Builder newBuilder(PrometheusProperties config) {
096        return new Builder(config);
097    }
098
099    public static class Builder {
100
101        private final PrometheusProperties config;
102        private Integer port = null;
103        private String hostname = null;
104        private InetAddress inetAddress = null;
105        private InetSocketAddress inetSocketAddress = null;
106        private ExecutorService executorService = null;
107        private PrometheusRegistry registry = null;
108        private ExporterHttpServerProperties properties = null;
109        private Authenticator authenticator = null;
110        private HttpsConfigurator httpsConfigurator = null;
111        private HttpHandler defaultHandler = null;
112
113        private Builder(PrometheusProperties config) {
114            this.config = config;
115        }
116
117        /**
118         * Port to bind to. Default is 0, indicating that a random port will be selected.
119         * You can learn the randomly selected port by calling {@link HTTPServer#getPort()}.
120         */
121        public Builder withPort(int port) {
122            this.port = port;
123            return this;
124        }
125
126        /**
127         * Use this hostname to resolve the IP address to bind to.
128         * Must not be called together with {@link #withInetAddress(InetAddress)}.
129         * Default is empty, indicating that the HTTPServer binds to the wildcard address.
130         */
131        public Builder withHostname(String hostname) {
132            this.hostname = hostname;
133            return this;
134        }
135
136        /**
137         * Bind to this IP address.
138         * Must not be called together with {@link #withHostname(String)}.
139         * Default is empty, indicating that the HTTPServer binds to the wildcard address.
140         */
141        public Builder withInetAddress(InetAddress address) {
142            this.inetAddress = address;
143            return this;
144        }
145
146        /**
147         * Optional: ExecutorService used by the {@code httpServer}.
148         */
149        public Builder withExecutorService(ExecutorService executorService) {
150            this.executorService = executorService;
151            return this;
152        }
153
154        /**
155         * Optional: Default is {@link PrometheusRegistry#defaultRegistry}.
156         */
157        public Builder withRegistry(PrometheusRegistry registry) {
158            this.registry = registry;
159            return this;
160        }
161
162        /**
163         * Optional: {@link Authenticator} for authentication.
164         */
165        public Builder withAuthenticator(Authenticator authenticator) {
166            this.authenticator = authenticator;
167            return this;
168        }
169
170        /**
171         * Optional: {@link HttpsConfigurator} for TLS/SSL
172         */
173        public Builder withHttpsConfigurator(HttpsConfigurator configurator) {
174            this.httpsConfigurator = configurator;
175            return this;
176        }
177
178        /**
179         * Optional: Override default handler, i.e. the handler that will be registered for the / endpoint.
180         */
181        public Builder withDefaultHandler(HttpHandler defaultHandler) {
182            this.defaultHandler = defaultHandler;
183            return this;
184        }
185
186        /**
187         * Build and start the HTTPServer.
188         */
189        public HTTPServer buildAndStart() throws IOException {
190            if (registry == null) {
191                registry = PrometheusRegistry.defaultRegistry;
192            }
193            HttpServer httpServer;
194            if (httpsConfigurator != null) {
195                httpServer = HttpsServer.create(makeInetSocketAddress(), 3);
196                ((HttpsServer)httpServer).setHttpsConfigurator(httpsConfigurator);
197            } else {
198                httpServer = HttpServer.create(makeInetSocketAddress(), 3);
199            }
200            ExecutorService executorService = makeExecutorService();
201            httpServer.setExecutor(executorService);
202            return new HTTPServer(config, executorService, httpServer, registry, authenticator, defaultHandler);
203        }
204
205        private InetSocketAddress makeInetSocketAddress() {
206            if (inetSocketAddress != null) {
207                assertNull(port, "cannot configure 'inetSocketAddress' and 'port' at the same time");
208                assertNull(hostname, "cannot configure 'inetSocketAddress' and 'hostname' at the same time");
209                assertNull(inetAddress, "cannot configure 'inetSocketAddress' and 'inetAddress' at the same time");
210                return inetSocketAddress;
211            } else if (inetAddress != null) {
212                assertNull(hostname, "cannot configure 'inetAddress' and 'hostname' at the same time");
213                return new InetSocketAddress(inetAddress, findPort());
214            } else if (hostname != null) {
215                return new InetSocketAddress(hostname, findPort());
216            } else {
217                return new InetSocketAddress(findPort());
218            }
219        }
220
221        private ExecutorService makeExecutorService() {
222            if (executorService != null) {
223                return executorService;
224            } else {
225                ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool(NamedDaemonThreadFactory.defaultThreadFactory(true));
226                executor.setKeepAliveTime(2, TimeUnit.MINUTES);
227                return executor;
228            }
229        }
230
231        private int findPort() {
232            if (properties != null && properties.getPort() != null) {
233                return properties.getPort(); // you can overwrite the hard-coded port with properties.
234            }
235            if (port != null) {
236                return port;
237            }
238            return 0; // random port will be selected
239        }
240
241        private void assertNull(Object o, String msg) {
242            if (o != null) {
243                throw new IllegalStateException(msg);
244            }
245        }
246    }
247}