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.util.List;
013import java.util.Set;
014import java.util.HashSet;
015import java.util.concurrent.ExecutorService;
016import java.util.concurrent.ExecutionException;
017import java.util.concurrent.Executors;
018import java.util.concurrent.FutureTask;
019import java.util.concurrent.ThreadFactory;
020import java.util.zip.GZIPOutputStream;
021
022import com.sun.net.httpserver.HttpHandler;
023import com.sun.net.httpserver.HttpServer;
024import com.sun.net.httpserver.HttpExchange;
025
026/**
027 * Expose Prometheus metrics using a plain Java HttpServer.
028 * <p>
029 * Example Usage:
030 * <pre>
031 * {@code
032 * HTTPServer server = new HTTPServer(1234);
033 * }
034 * </pre>
035 * */
036public class HTTPServer {
037    private static class LocalByteArray extends ThreadLocal<ByteArrayOutputStream> {
038        protected ByteArrayOutputStream initialValue()
039        {
040            return new ByteArrayOutputStream(1 << 20);
041        }
042    }
043
044    static class HTTPMetricHandler implements HttpHandler {
045        private CollectorRegistry registry;
046        private final LocalByteArray response = new LocalByteArray();
047
048        HTTPMetricHandler(CollectorRegistry registry) {
049          this.registry = registry;
050        }
051
052
053        public void handle(HttpExchange t) throws IOException {
054            String query = t.getRequestURI().getRawQuery();
055
056            ByteArrayOutputStream response = this.response.get();
057            response.reset();
058            OutputStreamWriter osw = new OutputStreamWriter(response);
059            TextFormat.write004(osw,
060                    registry.filteredMetricFamilySamples(parseQuery(query)));
061            osw.flush();
062            osw.close();
063            response.flush();
064            response.close();
065
066            t.getResponseHeaders().set("Content-Type",
067                    TextFormat.CONTENT_TYPE_004);
068            t.getResponseHeaders().set("Content-Length",
069                    String.valueOf(response.size()));
070            if (shouldUseCompression(t)) {
071                t.getResponseHeaders().set("Content-Encoding", "gzip");
072                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, 0);
073                final GZIPOutputStream os = new GZIPOutputStream(t.getResponseBody());
074                response.writeTo(os);
075                os.finish();
076            } else {
077                t.sendResponseHeaders(HttpURLConnection.HTTP_OK, response.size());
078                response.writeTo(t.getResponseBody());
079            }
080            t.close();
081        }
082
083    }
084
085    protected static boolean shouldUseCompression(HttpExchange exchange) {
086        List<String> encodingHeaders = exchange.getRequestHeaders().get("Accept-Encoding");
087        if (encodingHeaders == null) return false;
088
089        for (String encodingHeader : encodingHeaders) {
090            String[] encodings = encodingHeader.split(",");
091            for (String encoding : encodings) {
092                if (encoding.trim().toLowerCase().equals("gzip")) {
093                    return true;
094                }
095            }
096        }
097        return false;
098    }
099
100    protected static Set<String> parseQuery(String query) throws IOException {
101        Set<String> names = new HashSet<String>();
102        if (query != null) {
103            String[] pairs = query.split("&");
104            for (String pair : pairs) {
105                int idx = pair.indexOf("=");
106                if (idx != -1 && URLDecoder.decode(pair.substring(0, idx), "UTF-8").equals("name[]")) {
107                    names.add(URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
108                }
109            }
110        }
111        return names;
112    }
113
114
115    static class DaemonThreadFactory implements ThreadFactory {
116        private ThreadFactory delegate;
117        private final boolean daemon;
118
119        DaemonThreadFactory(ThreadFactory delegate, boolean daemon) {
120            this.delegate = delegate;
121            this.daemon = daemon;
122        }
123
124        @Override
125        public Thread newThread(Runnable r) {
126            Thread t = delegate.newThread(r);
127            t.setDaemon(daemon);
128            return t;
129        }
130
131        static ThreadFactory defaultThreadFactory(boolean daemon) {
132            return new DaemonThreadFactory(Executors.defaultThreadFactory(), daemon);
133        }
134    }
135
136    protected final HttpServer server;
137    protected final ExecutorService executorService;
138
139
140    /**
141     * Start a HTTP server serving Prometheus metrics from the given registry.
142     */
143    public HTTPServer(InetSocketAddress addr, CollectorRegistry registry, boolean daemon) throws IOException {
144        server = HttpServer.create();
145        server.bind(addr, 3);
146        HttpHandler mHandler = new HTTPMetricHandler(registry);
147        server.createContext("/", mHandler);
148        server.createContext("/metrics", mHandler);
149        executorService = Executors.newFixedThreadPool(5, DaemonThreadFactory.defaultThreadFactory(daemon));
150        server.setExecutor(executorService);
151        start(daemon);
152    }
153
154    /**
155     * Start a HTTP server serving Prometheus metrics from the given registry using non-daemon threads.
156     */
157    public HTTPServer(InetSocketAddress addr, CollectorRegistry registry) throws IOException {
158        this(addr, registry, false);
159    }
160
161    /**
162     * Start a HTTP server serving the default Prometheus registry.
163     */
164    public HTTPServer(int port, boolean daemon) throws IOException {
165        this(new InetSocketAddress(port), CollectorRegistry.defaultRegistry, daemon);
166    }
167
168    /**
169     * Start a HTTP server serving the default Prometheus registry using non-daemon threads.
170     */
171    public HTTPServer(int port) throws IOException {
172        this(port, false);
173    }
174
175    /**
176     * Start a HTTP server serving the default Prometheus registry.
177     */
178    public HTTPServer(String host, int port, boolean daemon) throws IOException {
179        this(new InetSocketAddress(host, port), CollectorRegistry.defaultRegistry, daemon);
180    }
181
182    /**
183     * Start a HTTP server serving the default Prometheus registry using non-daemon threads.
184     */
185    public HTTPServer(String host, int port) throws IOException {
186        this(new InetSocketAddress(host, port), CollectorRegistry.defaultRegistry, false);
187    }
188
189    /**
190     * Start a HTTP server by making sure that its background thread inherit proper daemon flag.
191     */
192    private void start(boolean daemon) {
193        if (daemon == Thread.currentThread().isDaemon()) {
194            server.start();
195        } else {
196            FutureTask<Void> startTask = new FutureTask<Void>(new Runnable() {
197                @Override
198                public void run() {
199                    server.start();
200                }
201            }, null);
202            DaemonThreadFactory.defaultThreadFactory(daemon).newThread(startTask).start();
203            try {
204                startTask.get();
205            } catch (ExecutionException e) {
206                throw new RuntimeException("Unexpected exception on starting HTTPSever", e);
207            } catch (InterruptedException e) {
208                // This is possible only if the current tread has been interrupted,
209                // but in real use cases this should not happen.
210                // In any case, there is nothing to do, except to propagate interrupted flag.
211                Thread.currentThread().interrupt();
212            }
213        }
214    }
215
216    /**
217     * Stop the HTTP server.
218     */
219    public void stop() {
220        server.stop(0);
221        executorService.shutdown(); // Free any (parked/idle) threads in pool
222    }
223}
224