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