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