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}