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 ExecutorService executorService = null;
107        private PrometheusRegistry registry = null;
108        private Authenticator authenticator = null;
109        private HttpsConfigurator httpsConfigurator = null;
110        private HttpHandler defaultHandler = null;
111
112        private Builder(PrometheusProperties config) {
113            this.config = config;
114        }
115
116        /**
117         * Port to bind to. Default is 0, indicating that a random port will be selected.
118         * You can learn the randomly selected port by calling {@link HTTPServer#getPort()}.
119         */
120        public Builder port(int port) {
121            this.port = port;
122            return this;
123        }
124
125        /**
126         * Use this hostname to resolve the IP address to bind to.
127         * Must not be called together with {@link #inetAddress(InetAddress)}.
128         * Default is empty, indicating that the HTTPServer binds to the wildcard address.
129         */
130        public Builder hostname(String hostname) {
131            this.hostname = hostname;
132            return this;
133        }
134
135        /**
136         * Bind to this IP address.
137         * Must not be called together with {@link #hostname(String)}.
138         * Default is empty, indicating that the HTTPServer binds to the wildcard address.
139         */
140        public Builder inetAddress(InetAddress address) {
141            this.inetAddress = address;
142            return this;
143        }
144
145        /**
146         * Optional: ExecutorService used by the {@code httpServer}.
147         */
148        public Builder executorService(ExecutorService executorService) {
149            this.executorService = executorService;
150            return this;
151        }
152
153        /**
154         * Optional: Default is {@link PrometheusRegistry#defaultRegistry}.
155         */
156        public Builder registry(PrometheusRegistry registry) {
157            this.registry = registry;
158            return this;
159        }
160
161        /**
162         * Optional: {@link Authenticator} for authentication.
163         */
164        public Builder authenticator(Authenticator authenticator) {
165            this.authenticator = authenticator;
166            return this;
167        }
168
169        /**
170         * Optional: {@link HttpsConfigurator} for TLS/SSL
171         */
172        public Builder httpsConfigurator(HttpsConfigurator configurator) {
173            this.httpsConfigurator = configurator;
174            return this;
175        }
176
177        /**
178         * Optional: Override default handler, i.e. the handler that will be registered for the / endpoint.
179         */
180        public Builder defaultHandler(HttpHandler defaultHandler) {
181            this.defaultHandler = defaultHandler;
182            return this;
183        }
184
185        /**
186         * Build and start the HTTPServer.
187         */
188        public HTTPServer buildAndStart() throws IOException {
189            if (registry == null) {
190                registry = PrometheusRegistry.defaultRegistry;
191            }
192            HttpServer httpServer;
193            if (httpsConfigurator != null) {
194                httpServer = HttpsServer.create(makeInetSocketAddress(), 3);
195                ((HttpsServer)httpServer).setHttpsConfigurator(httpsConfigurator);
196            } else {
197                httpServer = HttpServer.create(makeInetSocketAddress(), 3);
198            }
199            ExecutorService executorService = makeExecutorService();
200            httpServer.setExecutor(executorService);
201            return new HTTPServer(config, executorService, httpServer, registry, authenticator, defaultHandler);
202        }
203
204        private InetSocketAddress makeInetSocketAddress() {
205            if (inetAddress != null) {
206                assertNull(hostname, "cannot configure 'inetAddress' and 'hostname' at the same time");
207                return new InetSocketAddress(inetAddress, findPort());
208            } else if (hostname != null) {
209                return new InetSocketAddress(hostname, findPort());
210            } else {
211                return new InetSocketAddress(findPort());
212            }
213        }
214
215        private ExecutorService makeExecutorService() {
216            if (executorService != null) {
217                return executorService;
218            } else {
219                return new ThreadPoolExecutor(
220                                1,
221                                10,
222                                120,
223                                TimeUnit.SECONDS,
224                                new SynchronousQueue<>(true),
225                                NamedDaemonThreadFactory.defaultThreadFactory(true),
226                                new BlockingRejectedExecutionHandler());
227            }
228        }
229
230        private int findPort() {
231            if (config != null && config.getExporterHttpServerProperties() != null) {
232                Integer port = config.getExporterHttpServerProperties().getPort();
233                if (port != null) {
234                    return port;
235                }
236            }
237            if (port != null) {
238                return port;
239            }
240            return 0; // random port will be selected
241        }
242
243        private void assertNull(Object o, String msg) {
244            if (o != null) {
245                throw new IllegalStateException(msg);
246            }
247        }
248    }
249
250    private static class BlockingRejectedExecutionHandler implements RejectedExecutionHandler {
251
252        @Override
253        public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) {
254            if (!threadPoolExecutor.isShutdown()) {
255                try {
256                    threadPoolExecutor.getQueue().put(runnable);
257                } catch (InterruptedException ignored) {
258                }
259            }
260        }
261    }
262}