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