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