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