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