001package io.prometheus.metrics.exporter.common; 002 003import io.prometheus.metrics.annotations.StableApi; 004import io.prometheus.metrics.config.EscapingScheme; 005import io.prometheus.metrics.config.ExporterFilterProperties; 006import io.prometheus.metrics.config.PrometheusProperties; 007import io.prometheus.metrics.expositionformats.ExpositionFormatWriter; 008import io.prometheus.metrics.expositionformats.ExpositionFormats; 009import io.prometheus.metrics.model.registry.MetricNameFilter; 010import io.prometheus.metrics.model.registry.PrometheusRegistry; 011import io.prometheus.metrics.model.snapshots.MetricSnapshots; 012import java.io.ByteArrayOutputStream; 013import java.io.IOException; 014import java.io.OutputStream; 015import java.nio.charset.StandardCharsets; 016import java.util.ArrayList; 017import java.util.Arrays; 018import java.util.Enumeration; 019import java.util.List; 020import java.util.concurrent.atomic.AtomicInteger; 021import java.util.function.Predicate; 022import java.util.zip.GZIPOutputStream; 023import javax.annotation.Nullable; 024 025/** Prometheus scrape endpoint. */ 026@StableApi 027public class PrometheusScrapeHandler { 028 029 private final PrometheusRegistry registry; 030 private final ExpositionFormats expositionFormats; 031 @Nullable private final Predicate<String> nameFilter; 032 private final AtomicInteger lastResponseSize = new AtomicInteger(2 << 9); // 0.5 MB 033 private final List<String> supportedFormats; 034 private final boolean preferUncompressedResponse; 035 036 public PrometheusScrapeHandler() { 037 this(PrometheusProperties.get(), PrometheusRegistry.defaultRegistry); 038 } 039 040 public PrometheusScrapeHandler(PrometheusRegistry registry) { 041 this(PrometheusProperties.get(), registry); 042 } 043 044 public PrometheusScrapeHandler(PrometheusProperties config) { 045 this(config, PrometheusRegistry.defaultRegistry); 046 } 047 048 public PrometheusScrapeHandler(PrometheusProperties config, PrometheusRegistry registry) { 049 this.expositionFormats = ExpositionFormats.init(config); 050 this.preferUncompressedResponse = 051 config.getExporterHttpServerProperties().isPreferUncompressedResponse(); 052 this.registry = registry; 053 this.nameFilter = makeNameFilter(config.getExporterFilterProperties()); 054 supportedFormats = new ArrayList<>(Arrays.asList("openmetrics", "text")); 055 if (expositionFormats.getPrometheusProtobufWriter().isAvailable()) { 056 supportedFormats.add("prometheus-protobuf"); 057 } 058 } 059 060 public void handleRequest(PrometheusHttpExchange exchange) throws IOException { 061 try { 062 PrometheusHttpRequest request = exchange.getRequest(); 063 String[] includedNames = null; 064 String debugParam = null; 065 try { 066 includedNames = request.getParameterValues("name[]"); 067 debugParam = request.getParameter("debug"); 068 } catch (InvalidQueryParameterException e) { 069 writeInvalidQueryParametersResponse(exchange); 070 return; 071 } 072 MetricSnapshots snapshots = scrape(request, includedNames); 073 String acceptHeader = request.getHeader("Accept"); 074 EscapingScheme escapingScheme = EscapingScheme.fromAcceptHeader(acceptHeader); 075 if (writeDebugResponse(snapshots, escapingScheme, debugParam, exchange)) { 076 return; 077 } 078 ExpositionFormatWriter writer = expositionFormats.findWriter(acceptHeader); 079 PrometheusHttpResponse response = exchange.getResponse(); 080 response.setHeader("Content-Type", writer.getContentType()); 081 082 if (shouldUseCompression(request)) { 083 response.setHeader("Content-Encoding", "gzip"); 084 try (GZIPOutputStream gzipOutputStream = 085 new GZIPOutputStream(response.sendHeadersAndGetBody(200, 0))) { 086 writer.write(gzipOutputStream, snapshots, escapingScheme); 087 } 088 } else { 089 ByteArrayOutputStream responseBuffer = 090 new ByteArrayOutputStream(lastResponseSize.get() + 1024); 091 writer.write(responseBuffer, snapshots, escapingScheme); 092 lastResponseSize.set(responseBuffer.size()); 093 int contentLength = responseBuffer.size(); 094 if (contentLength > 0) { 095 response.setHeader("Content-Length", String.valueOf(contentLength)); 096 } 097 if (request.getMethod().equals("HEAD")) { 098 // The HTTPServer implementation will throw an Exception if we close the output stream 099 // without sending a response body, so let's not close the output stream in case of a HEAD 100 // response. 101 response.sendHeadersAndGetBody(200, -1); 102 } else { 103 try (OutputStream outputStream = response.sendHeadersAndGetBody(200, contentLength)) { 104 responseBuffer.writeTo(outputStream); 105 } 106 } 107 } 108 } catch (IOException e) { 109 exchange.handleException(e); 110 } catch (RuntimeException e) { 111 exchange.handleException(e); 112 } finally { 113 exchange.close(); 114 } 115 } 116 117 @Nullable 118 private Predicate<String> makeNameFilter(ExporterFilterProperties props) { 119 if (props.getAllowedMetricNames() == null 120 && props.getExcludedMetricNames() == null 121 && props.getAllowedMetricNamePrefixes() == null 122 && props.getExcludedMetricNamePrefixes() == null) { 123 return null; 124 } else { 125 return MetricNameFilter.builder() 126 .nameMustBeEqualTo(props.getAllowedMetricNames()) 127 .nameMustNotBeEqualTo(props.getExcludedMetricNames()) 128 .nameMustStartWith(props.getAllowedMetricNamePrefixes()) 129 .nameMustNotStartWith(props.getExcludedMetricNamePrefixes()) 130 .build(); 131 } 132 } 133 134 @Nullable 135 private Predicate<String> makeNameFilter(@Nullable String[] includedNames) { 136 Predicate<String> result = null; 137 if (includedNames != null && includedNames.length > 0) { 138 result = MetricNameFilter.builder().nameMustBeEqualTo(includedNames).build(); 139 } 140 if (result != null && nameFilter != null) { 141 result = result.and(nameFilter); 142 } else if (nameFilter != null) { 143 result = nameFilter; 144 } 145 return result; 146 } 147 148 private MetricSnapshots scrape(PrometheusHttpRequest request, @Nullable String[] includedNames) { 149 150 Predicate<String> filter = makeNameFilter(includedNames); 151 if (filter != null) { 152 return registry.scrape(filter, request); 153 } else { 154 return registry.scrape(request); 155 } 156 } 157 158 private boolean writeDebugResponse( 159 MetricSnapshots snapshots, 160 EscapingScheme escapingScheme, 161 @Nullable String debugParam, 162 PrometheusHttpExchange exchange) 163 throws IOException { 164 PrometheusHttpResponse response = exchange.getResponse(); 165 if (debugParam == null) { 166 return false; 167 } else { 168 response.setHeader("Content-Type", "text/plain; charset=utf-8"); 169 int responseStatus = supportedFormats.contains(debugParam) ? 200 : 500; 170 OutputStream body = response.sendHeadersAndGetBody(responseStatus, 0); 171 switch (debugParam) { 172 case "openmetrics": 173 expositionFormats.getOpenMetricsTextFormatWriter().write(body, snapshots, escapingScheme); 174 break; 175 case "text": 176 expositionFormats.getPrometheusTextFormatWriter().write(body, snapshots, escapingScheme); 177 break; 178 case "prometheus-protobuf": 179 String debugString = 180 expositionFormats 181 .getPrometheusProtobufWriter() 182 .toDebugString(snapshots, escapingScheme); 183 body.write(debugString.getBytes(StandardCharsets.UTF_8)); 184 break; 185 default: 186 body.write( 187 ("debug=" 188 + debugParam 189 + ": Unsupported query parameter. Valid values are 'openmetrics', " 190 + "'text', and 'prometheus-protobuf'.") 191 .getBytes(StandardCharsets.UTF_8)); 192 break; 193 } 194 return true; 195 } 196 } 197 198 private void writeInvalidQueryParametersResponse(PrometheusHttpExchange exchange) 199 throws IOException { 200 PrometheusHttpResponse response = exchange.getResponse(); 201 response.setHeader("Content-Type", "text/plain; charset=utf-8"); 202 byte[] message = "Invalid query parameters".getBytes(StandardCharsets.UTF_8); 203 try (OutputStream outputStream = response.sendHeadersAndGetBody(400, message.length)) { 204 outputStream.write(message); 205 } 206 } 207 208 private boolean shouldUseCompression(PrometheusHttpRequest request) { 209 if (preferUncompressedResponse) { 210 return false; 211 } 212 213 Enumeration<String> encodingHeaders = request.getHeaders("Accept-Encoding"); 214 if (encodingHeaders == null) { 215 return false; 216 } 217 while (encodingHeaders.hasMoreElements()) { 218 String encodingHeader = encodingHeaders.nextElement(); 219 String[] encodings = encodingHeader.split(","); 220 for (String encoding : encodings) { 221 if (encoding.trim().equalsIgnoreCase("gzip")) { 222 return true; 223 } 224 } 225 } 226 return false; 227 } 228}