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