001package io.prometheus.metrics.exporter.httpserver;
002
003import io.prometheus.metrics.annotations.StableApi;
004import java.io.PrintWriter;
005import java.io.StringWriter;
006import java.nio.charset.StandardCharsets;
007import java.util.function.Consumer;
008import java.util.logging.Level;
009import java.util.logging.Logger;
010import javax.annotation.Nullable;
011
012/**
013 * Controls how the {@link HTTPServer} handles exceptions raised while scraping metrics.
014 *
015 * <p>The default policy built by {@link #builder()} does not expose exception details and does not
016 * report the exception. Configure the builder to route diagnostic details to an
017 * application-appropriate sink.
018 */
019@StableApi
020public final class HttpErrorHandlingPolicy {
021
022  private static final Logger logger = Logger.getLogger(HttpErrorHandlingPolicy.class.getName());
023
024  private static final byte[] GENERIC_RESPONSE =
025      ("An internal error occurred while scraping metrics. "
026              + "Configure an HTTP error reporter for details.\n")
027          .getBytes(StandardCharsets.UTF_8);
028
029  private final boolean unsafeDebugResponse;
030  @Nullable private final Consumer<? super Exception> errorReporter;
031
032  private HttpErrorHandlingPolicy(
033      boolean unsafeDebugResponse, @Nullable Consumer<? super Exception> errorReporter) {
034    this.unsafeDebugResponse = unsafeDebugResponse;
035    this.errorReporter = errorReporter;
036  }
037
038  /**
039   * Returns a builder for configuring scrape error handling.
040   *
041   * <p>The builder defaults to a generic HTTP 500 response with no error reporter. This avoids
042   * exposing exception details to scrape clients or adding an implicit dependency on an
043   * application's logging configuration.
044   */
045  public static Builder builder() {
046    return new Builder();
047  }
048
049  byte[] getErrorResponse(Exception exception) {
050    if (!unsafeDebugResponse) {
051      return GENERIC_RESPONSE;
052    }
053    StringWriter stringWriter = new StringWriter();
054    PrintWriter printWriter = new PrintWriter(stringWriter);
055    printWriter.write("An Exception occurred while scraping metrics: ");
056    exception.printStackTrace(printWriter);
057    return stringWriter.toString().getBytes(StandardCharsets.UTF_8);
058  }
059
060  void report(Exception error) {
061    if (errorReporter != null) {
062      errorReporter.accept(error);
063    }
064  }
065
066  boolean hasErrorReporter() {
067    return errorReporter != null;
068  }
069
070  /**
071   * Returns a synchronous reporter that logs scrape exceptions at {@link Level#SEVERE} using JUL.
072   *
073   * <p>Reporting is opt-in; the default policy does not log scrape exceptions.
074   */
075  public static Consumer<Throwable> julReporter() {
076    return error -> logger.log(Level.SEVERE, "Prometheus scrape failed", error);
077  }
078
079  /** Builder for {@link HttpErrorHandlingPolicy}. */
080  public static final class Builder {
081
082    private boolean unsafeDebugResponse = false;
083    @Nullable private Consumer<? super Exception> errorReporter;
084
085    private Builder() {}
086
087    /**
088     * Pass scrape exceptions to {@code errorReporter}.
089     *
090     * <p>The reporter runs synchronously on the HTTP request thread. It should return promptly and
091     * must be safe to call concurrently. Runtime exceptions thrown by the reporter are isolated
092     * from HTTP response handling.
093     */
094    public Builder errorReporter(Consumer<? super Exception> errorReporter) {
095      if (errorReporter == null) {
096        throw new NullPointerException("errorReporter");
097      }
098      this.errorReporter = errorReporter;
099      return this;
100    }
101
102    /**
103     * Configure whether the HTTP 500 response includes the full exception stack trace.
104     *
105     * <p><strong>Security warning:</strong> Setting this to {@code true} exposes internal exception
106     * information to scrape clients. Do not enable it for endpoints reachable by untrusted clients.
107     *
108     * <p>This setting is independent of {@link #errorReporter(Consumer)}.
109     */
110    public Builder unsafeDebugResponse(boolean unsafeDebugResponse) {
111      this.unsafeDebugResponse = unsafeDebugResponse;
112      return this;
113    }
114
115    /** Build the policy. */
116    public HttpErrorHandlingPolicy build() {
117      return new HttpErrorHandlingPolicy(unsafeDebugResponse, errorReporter);
118    }
119  }
120}