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