class RemoteRuleMetrics {
  private static final Logger logger = LoggerFactory.getLogger(RemoteRuleMetrics.class);

  private RemoteRuleMetrics() {
    throw new IllegalStateException("RemoteRuleMetrics should only be used via static methods.");
  }

  public enum RequestResult {
    SUCCESS,
    SKIPPED,
    TIMEOUT,
    INTERRUPTED,
    DOWN,
    ERROR
  }

  // TODO: provide configuration as info?

  private static final double[] WAIT_BUCKETS = {
    .05, .1, .2, .3, .4, .5, .75, 1., 2., 5., 7.5, 10., 15.
  };

  private static final double[] LATENCY_BUCKETS = {
    0.025, 0.05, .1, .25, .5, .75, 1., 2., 4., 6., 8., 10., 15.
  };

  private static final double[] SIZE_BUCKETS = {
    25, 100, 500, 1000, 2500, 5000, 10000, 20000, 40000
  };


  private static final Histogram wait = Histogram
    .build("languagetool_remote_rule_wait_seconds", "Time spent waiting on remote rule results/timeouts")
    .labelNames("language")
    .buckets(WAIT_BUCKETS)
    .register();

  private static final Histogram requestLatency = Histogram
    .build("languagetool_remote_rule_request_latency_seconds", "Request duration summary")
    .labelNames("rule_id", "result")
    .buckets(LATENCY_BUCKETS)
    .register();

  private static final Histogram requestThroughput = Histogram
    .build("languagetool_remote_rule_request_throughput_characters", "Request size summary")
    .labelNames("rule_id", "result")
    .buckets(SIZE_BUCKETS)
    .register();

  public static void request(String rule, long startNanos, long characters, RequestResult result) {
    long delta = System.nanoTime() - startNanos;
    requestLatency.labels(rule, result.name().toLowerCase()).observe((double) delta / 1e9);
    requestThroughput.labels(rule, result.name().toLowerCase()).observe(characters);
  }

  public static void wait(String langCode, long milliseconds) {
    wait.labels(langCode).observe(milliseconds / 1000.0);
  }

  @ApiStatus.Internal
  @Nullable
  public static <T> T inCircuitBreaker(long deadlineStartNanos, CircuitBreaker circuitBreaker, String ruleKey, long chars, Callable<T> fetchResults) throws InterruptedException {<FILL_FUNCTION_BODY>}
}

    try {
      return circuitBreaker.executeCallable(fetchResults);
    } catch (InterruptedException e) {
      logger.info("Failed to fetch result from remote rule '{}' - interrupted.", ruleKey);
      request(ruleKey, deadlineStartNanos, chars, RequestResult.INTERRUPTED);
      throw e;
    } catch (CancellationException e) {
      logger.info("Failed to fetch result from remote rule '{}' - cancelled.", ruleKey);
      request(ruleKey, deadlineStartNanos, chars, RequestResult.INTERRUPTED);
    } catch (TimeoutException e) {
      long timeout = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - deadlineStartNanos);
      logger.info("Failed to fetch result from remote rule '{}' - timed out ({}ms, {} chars).",
        ruleKey, timeout, chars);
      request(ruleKey, deadlineStartNanos, chars, RequestResult.TIMEOUT);
    } catch (CallNotPermittedException e) {
      logger.info("Failed to fetch result from remote rule '{}' - circuitbreaker active, rule marked as down.", ruleKey);
      request(ruleKey, deadlineStartNanos, chars, RequestResult.DOWN);
    } catch (Exception e) {
      if (ExceptionUtils.indexOfThrowable(e, TimeoutException.class) != -1) {
        long timeout = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - deadlineStartNanos);
        logger.info("Failed to fetch result from remote rule '{}' - timed out with exception {} ({}ms, {} chars).",
          ruleKey, e, timeout, chars);
        request(ruleKey, deadlineStartNanos, chars, RequestResult.TIMEOUT);
      } else {
        logger.warn("Failed to fetch result from remote rule '" + ruleKey + "' - error while executing rule.", e);
        request(ruleKey, deadlineStartNanos, chars, RequestResult.ERROR);
      }
    }
    return null;
  
<no_super_class>