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