001package io.prometheus.client.exporter;
002
003import io.prometheus.client.CollectorRegistry;
004import io.prometheus.client.exporter.common.TextFormat;
005
006import java.io.ByteArrayOutputStream;
007import java.io.IOException;
008import java.io.OutputStreamWriter;
009import java.net.HttpURLConnection;
010import java.net.InetSocketAddress;
011import java.net.URLDecoder;
012import java.nio.charset.Charset;
013import java.util.HashSet;
014import java.util.List;
015import java.util.Set;
016import java.util.concurrent.ExecutionException;
017import java.util.concurrent.ExecutorService;
018import java.util.concurrent.Executors;
019import java.util.concurrent.FutureTask;
020import java.util.concurrent.ThreadFactory;
021import java.util.concurrent.atomic.AtomicInteger;
022import java.util.zip.GZIPOutputStream;
023
024import com.sun.net.httpserver.HttpExchange;
025import com.sun.net.httpserver.HttpHandler;
026import com.sun.net.httpserver.HttpServer;
027
028/**
029 * Expose Prometheus metrics using a plain Java HttpServer.
030 * <p>
031 * Example Usage:
032 * <pre>
033 * {@code
034 * HTTPServer server = new HTTPServer(1234);
035 * }
036 * </pre>
037 * */
038public class HTTPServer {
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    private static class LocalByteArray extends ThreadLocal<ByteArrayOutputStream> {
051        @Override
052        protected ByteArrayOutputStream initialValue()
053        {
054            return new ByteArrayOutputStream(1 << 20);
055        }
056    }
057
058    /**
059     * Handles Metrics collections from the given registry.
060     */
061    public static class HTTPMetricHandler implements HttpHandler {
062        private final CollectorRegistry registry;
063        private final LocalByteArray response = new LocalByteArray();
064        private final static String HEALTHY_RESPONSE = "Exporter is Healthy.";
065
066        HTTPMetricHandler(CollectorRegistry registry) {
067          this.registry = registry;
068        }
069
070        @Override
071        public void handle(HttpExchange t) throws IOException {
072            String query = t.getRequestURI().getRawQuery();
073
074            String contextPath = t.getHttpContext().getPath();
075            ByteArrayOutputStream response = this.response.get();
076            response.reset();
077            OutputStreamWriter osw = new OutputStreamWriter(response, Charset.forName("UTF-8"));
078            if ("/-/healthy".equals(contextPath)) {
079                osw.write(HEALTHY_RESPONSE);
080            } else {
081                String contentType = TextFormat.chooseContentType(t.getRequestHeaders().getFirst("Accept"));
082                t.getResponseHeaders().set("Content-Type", contentType);
083                TextFormat.writeFormat(contentType, osw,
084                        registry.filteredMetricFamilySamples(parseQuery(query)));
085            }
086
087            osw.close();
088
089            if (shouldUseCompression(t)) {
090                t.getResponseHeaders().set("Content-Encoding", "gzip");
091                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
092                final GZIPOutputStream os = new GZIPOutputStream(t.getResponseBody());
093                try {
094                    response.writeTo(os);
095                } finally {
096                    os.close();
097                }
098            } else {
099                t.getResponseHeaders().set("Content-Length",
100                        String.valueOf(response.size()));
101                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.size());
102                response.writeTo(t.getResponseBody());
103            }
104            t.close();
105        }
106
107    }
108
109    protected static boolean shouldUseCompression(HttpExchange exchange) {
110        List<String> encodingHeaders = exchange.getRequestHeaders().get("Accept-Encoding");
111        if (encodingHeaders == null) return false;
112
113        for (String encodingHeader : encodingHeaders) {
114            String[] encodings = encodingHeader.split(",");
115            for (String encoding : encodings) {
116                if (encoding.trim().equalsIgnoreCase("gzip")) {
117                    return true;
118                }
119            }
120        }
121        return false;
122    }
123
124    protected static Set<String> parseQuery(String query) throws IOException {
125        Set<String> names = new HashSet<String>();
126        if (query != null) {
127            String[] pairs = query.split("&");
128            for (String pair : pairs) {
129                int idx = pair.indexOf("=");
130                if (idx != -1 && URLDecoder.decode(pair.substring(0, idx), "UTF-8").equals("name[]")) {
131                    names.add(URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
132                }
133            }
134        }
135        return names;
136    }
137
138
139    static class NamedDaemonThreadFactory implements ThreadFactory {
140        private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
141
142        private final int poolNumber = POOL_NUMBER.getAndIncrement();
143        private final AtomicInteger threadNumber = new AtomicInteger(1);
144        private final ThreadFactory delegate;
145        private final boolean daemon;
146
147        NamedDaemonThreadFactory(ThreadFactory delegate, boolean daemon) {
148            this.delegate = delegate;
149            this.daemon = daemon;
150        }
151
152        @Override
153        public Thread newThread(Runnable r) {
154            Thread t = delegate.newThread(r);
155            t.setName(String.format("prometheus-http-%d-%d", poolNumber, threadNumber.getAndIncrement()));
156            t.setDaemon(daemon);
157            return t;
158        }
159
160        static ThreadFactory defaultThreadFactory(boolean daemon) {
161            return new NamedDaemonThreadFactory(Executors.defaultThreadFactory(), daemon);
162        }
163    }
164
165    protected final HttpServer server;
166    protected final ExecutorService executorService;
167
168    /**
169     * Start a HTTP server serving Prometheus metrics from the given registry using the given {@link HttpServer}.
170     * The {@code httpServer} is expected to already be bound to an address
171     */
172    public HTTPServer(HttpServer httpServer, CollectorRegistry registry, boolean daemon) throws IOException {
173        if (httpServer.getAddress() == null)
174            throw new IllegalArgumentException("HttpServer hasn't been bound to an address");
175
176        server = httpServer;
177        HttpHandler mHandler = new HTTPMetricHandler(registry);
178        server.createContext("/", mHandler);
179        server.createContext("/metrics", mHandler);
180        server.createContext("/-/healthy", mHandler);
181        executorService = Executors.newFixedThreadPool(5, NamedDaemonThreadFactory.defaultThreadFactory(daemon));
182        server.setExecutor(executorService);
183        start(daemon);
184    }
185
186    /**
187     * Start a HTTP server serving Prometheus metrics from the given registry.
188     */
189    public HTTPServer(InetSocketAddress addr, CollectorRegistry registry, boolean daemon) throws IOException {
190        this(HttpServer.create(addr, 3), registry, daemon);
191    }
192
193    /**
194     * Start a HTTP server serving Prometheus metrics from the given registry using non-daemon threads.
195     */
196    public HTTPServer(InetSocketAddress addr, CollectorRegistry registry) throws IOException {
197        this(addr, registry, false);
198    }
199
200    /**
201     * Start a HTTP server serving the default Prometheus registry.
202     */
203    public HTTPServer(int port, boolean daemon) throws IOException {
204        this(new InetSocketAddress(port), CollectorRegistry.defaultRegistry, daemon);
205    }
206
207    /**
208     * Start a HTTP server serving the default Prometheus registry using non-daemon threads.
209     */
210    public HTTPServer(int port) throws IOException {
211        this(port, false);
212    }
213
214    /**
215     * Start a HTTP server serving the default Prometheus registry.
216     */
217    public HTTPServer(String host, int port, boolean daemon) throws IOException {
218        this(new InetSocketAddress(host, port), CollectorRegistry.defaultRegistry, daemon);
219    }
220
221    /**
222     * Start a HTTP server serving the default Prometheus registry using non-daemon threads.
223     */
224    public HTTPServer(String host, int port) throws IOException {
225        this(new InetSocketAddress(host, port), CollectorRegistry.defaultRegistry, false);
226    }
227
228    /**
229     * Start a HTTP server by making sure that its background thread inherit proper daemon flag.
230     */
231    private void start(boolean daemon) {
232        if (daemon == Thread.currentThread().isDaemon()) {
233            server.start();
234        } else {
235            FutureTask<Void> startTask = new FutureTask<Void>(new Runnable() {
236                @Override
237                public void run() {
238                    server.start();
239                }
240            }, null);
241            NamedDaemonThreadFactory.defaultThreadFactory(daemon).newThread(startTask).start();
242            try {
243                startTask.get();
244            } catch (ExecutionException e) {
245                throw new RuntimeException("Unexpected exception on starting HTTPSever", e);
246            } catch (InterruptedException e) {
247                // This is possible only if the current tread has been interrupted,
248                // but in real use cases this should not happen.
249                // In any case, there is nothing to do, except to propagate interrupted flag.
250                Thread.currentThread().interrupt();
251            }
252        }
253    }
254
255    /**
256     * Stop the HTTP server.
257     */
258    public void stop() {
259        server.stop(0);
260        executorService.shutdown(); // Free any (parked/idle) threads in pool
261    }
262
263    /**
264     * Gets the port number.
265     */
266    public int getPort() {
267        return server.getAddress().getPort();
268    }
269}