/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.datastax.driver.core;

import static java.util.concurrent.TimeUnit.NANOSECONDS;

import com.datastax.driver.core.querybuilder.BuiltStatement;
import com.google.common.annotations.VisibleForTesting;
import java.nio.ByteBuffer;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A configurable {@link LatencyTracker} that logs all executed statements.
 *
 * <p>Typically, client applications would instantiate one single query logger (using its {@link
 * Builder}), configure it and register it on the relevant {@link Cluster} instance, e.g.:
 *
 * <p>
 *
 * <pre>
 * Cluster cluster = ...
 * QueryLogger queryLogger = QueryLogger.builder()
 *     .withConstantThreshold(...)
 *     .withMaxQueryStringLength(...)
 *     .build();
 * cluster.register(queryLogger);
 * </pre>
 *
 * <p>Refer to the {@link Builder} documentation for more information on configuration settings for
 * the query logger.
 *
 * <p>Once registered, the query logger will log every {@link RegularStatement}, {@link
 * BoundStatement} or {@link BatchStatement} executed by the driver; note that it will never log
 * other types of statement, null statements nor any special statement used internally by the
 * driver.
 *
 * <p>There is one log for each request to a Cassandra node; because the driver sometimes retries
 * the same statement on multiple nodes, a single statement execution (for example, a single call to
 * {@link Session#execute(Statement)}) can produce multiple logs on different nodes.
 *
 * <p>For more flexibility, the query logger uses 3 different {@link Logger} instances:
 *
 * <p>
 *
 * <ol>
 *   <li>{@link #NORMAL_LOGGER}: used to log normal queries, i.e., queries that completed
 *       successfully within a configurable threshold in milliseconds.
 *   <li>{@link #SLOW_LOGGER}: used to log slow queries, i.e., queries that completed successfully
 *       but that took longer than a configurable threshold in milliseconds to complete.
 *   <li>{@link #ERROR_LOGGER}: used to log unsuccessful queries, i.e., queries that did not
 *       completed normally and threw an exception. Note this this logger will also print the full
 *       stack trace of the reported exception.
 * </ol>
 *
 * <p>
 *
 * <p>The appropriate logger is chosen according to the following algorithm:
 *
 * <ol>
 *   <li>if an exception has been thrown: use {@link #ERROR_LOGGER};
 *   <li>otherwise, if the reported latency is greater than the configured threshold in
 *       milliseconds: use {@link #SLOW_LOGGER};
 *   <li>otherwise, use {@link #NORMAL_LOGGER}.
 * </ol>
 *
 * <p>
 *
 * <p>All loggers are activated by setting their levels to {@code DEBUG} or {@code TRACE} (including
 * {@link #ERROR_LOGGER}). If the level is set to {@code TRACE} and the statement being logged is a
 * {@link BoundStatement}, then the query parameters (if any) will be logged as well (names and
 * actual values).
 *
 * <p>
 *
 * <p><strong>Constant thresholds vs. Dynamic thresholds</strong>
 *
 * <p>Currently the QueryLogger can track slow queries in two different ways: using a {@link
 * Builder#withConstantThreshold(long)} constant threshold} in milliseconds (which is the default
 * behavior), or using a {@link Builder#withDynamicThreshold(PercentileTracker, double) dynamic
 * threshold} based on latency percentiles.
 *
 * <p>This class is thread-safe.
 *
 * @since 2.0.10
 */
public abstract class QueryLogger implements LatencyTracker {

  /**
   * The default latency threshold in milliseconds beyond which queries are considered 'slow' and
   * logged as such by the driver.
   */
  public static final long DEFAULT_SLOW_QUERY_THRESHOLD_MS = 5000;

  /**
   * The default latency percentile beyond which queries are considered 'slow' and logged as such by
   * the driver.
   */
  public static final double DEFAULT_SLOW_QUERY_THRESHOLD_PERCENTILE = 99.0;

  /**
   * The default maximum length of a CQL query string that can be logged verbatim by the driver.
   * Query strings longer than this value will be truncated when logged.
   */
  public static final int DEFAULT_MAX_QUERY_STRING_LENGTH = 500;

  /**
   * The default maximum length of a query parameter value that can be logged verbatim by the
   * driver. Parameter values longer than this value will be truncated when logged.
   */
  public static final int DEFAULT_MAX_PARAMETER_VALUE_LENGTH = 50;

  /**
   * The default maximum number of query parameters that can be logged by the driver. Queries with a
   * number of parameters higher than this value will not have all their parameters logged.
   */
  public static final int DEFAULT_MAX_LOGGED_PARAMETERS = 50;

  // Loggers

  /**
   * The logger used to log normal queries, i.e., queries that completed successfully within a
   * configurable threshold in milliseconds.
   *
   * <p>This logger is activated by setting its level to {@code DEBUG} or {@code TRACE}.
   * Additionally, if the level is set to {@code TRACE} and the statement being logged is a {@link
   * BoundStatement} or a {@link SimpleStatement}, then the query parameters (if any) will be
   * logged. For a {@link BoundStatement} names and actual values are logged and for a {@link
   * SimpleStatement} values are logged in positional order and named values are logged with names
   * and value.
   *
   * <p>The name of this logger is {@code com.datastax.driver.core.QueryLogger.NORMAL}.
   */
  public static final Logger NORMAL_LOGGER =
      LoggerFactory.getLogger("com.datastax.driver.core.QueryLogger.NORMAL");

  /**
   * The logger used to log slow queries, i.e., queries that completed successfully but whose
   * execution time exceeded a configurable threshold in milliseconds.
   *
   * <p>This logger is activated by setting its level to {@code DEBUG} or {@code TRACE}.
   * Additionally, if the level is set to {@code TRACE} and the statement being logged is a {@link
   * BoundStatement} or a {@link SimpleStatement}, then the query parameters (if any) will be
   * logged. For a {@link BoundStatement} names and actual values are logged and for a {@link
   * SimpleStatement} values are logged in positional order and named values are logged with names
   * and value.
   *
   * <p>The name of this logger is {@code com.datastax.driver.core.QueryLogger.SLOW}.
   */
  public static final Logger SLOW_LOGGER =
      LoggerFactory.getLogger("com.datastax.driver.core.QueryLogger.SLOW");

  /**
   * The logger used to log unsuccessful queries, i.e., queries that did not complete normally and
   * threw an exception.
   *
   * <p>This logger is activated by setting its level to {@code DEBUG} or {@code TRACE}.
   * Additionally, if the level is set to {@code TRACE} and the statement being logged is a {@link
   * BoundStatement} or a {@link SimpleStatement}, then the query parameters (if any) will be
   * logged. For a {@link BoundStatement} names and actual values are logged and for a {@link
   * SimpleStatement} values are logged in positional order and named values are logged with names
   * and value. Note this this logger will also print the full stack trace of the reported
   * exception.
   *
   * <p>The name of this logger is {@code com.datastax.driver.core.QueryLogger.ERROR}.
   */
  public static final Logger ERROR_LOGGER =
      LoggerFactory.getLogger("com.datastax.driver.core.QueryLogger.ERROR");

  // Message templates

  private static final String NORMAL_TEMPLATE =
      "[%s] [%s] Query completed normally, took %s ms: %s";

  private static final String SLOW_TEMPLATE_MILLIS = "[%s] [%s] Query too slow, took %s ms: %s";

  private static final String SLOW_TEMPLATE_PERCENTILE =
      "[%s] [%s] Query too slow, took %s ms (%s percentile = %s ms): %s";

  private static final String ERROR_TEMPLATE = "[%s] [%s] Query error after %s ms: %s";

  @VisibleForTesting static final String TRUNCATED_OUTPUT = "... [truncated output]";

  @VisibleForTesting static final String FURTHER_PARAMS_OMITTED = " [further parameters omitted]";

  protected volatile Cluster cluster;

  private volatile ProtocolVersion protocolVersion;

  protected volatile int maxQueryStringLength;

  protected volatile int maxParameterValueLength;

  protected volatile int maxLoggedParameters;

  /**
   * Private constructor. Instances of QueryLogger should be obtained via the {@link #builder()}
   * method.
   */
  private QueryLogger(
      int maxQueryStringLength, int maxParameterValueLength, int maxLoggedParameters) {
    this.maxQueryStringLength = maxQueryStringLength;
    this.maxParameterValueLength = maxParameterValueLength;
    this.maxLoggedParameters = maxLoggedParameters;
  }

  /**
   * Creates a new {@link QueryLogger.Builder} instance.
   *
   * <p>This is a convenience method for {@code new QueryLogger.Builder()}.
   *
   * @return the new QueryLogger builder.
   * @throws NullPointerException if {@code cluster} is {@code null}.
   */
  public static QueryLogger.Builder builder() {
    return new QueryLogger.Builder();
  }

  @Override
  public void onRegister(Cluster cluster) {
    this.cluster = cluster;
  }

  @Override
  public void onUnregister(Cluster cluster) {
    // nothing to do
  }

  /**
   * A QueryLogger that uses a constant threshold in milliseconds to track slow queries. This
   * implementation is the default and should be preferred to {@link DynamicThresholdQueryLogger}
   * which is still in beta state.
   */
  public static class ConstantThresholdQueryLogger extends QueryLogger {

    private volatile long slowQueryLatencyThresholdMillis;

    private ConstantThresholdQueryLogger(
        int maxQueryStringLength,
        int maxParameterValueLength,
        int maxLoggedParameters,
        long slowQueryLatencyThresholdMillis) {
      super(maxQueryStringLength, maxParameterValueLength, maxLoggedParameters);
      this.setSlowQueryLatencyThresholdMillis(slowQueryLatencyThresholdMillis);
    }

    /**
     * Return the threshold in milliseconds beyond which queries are considered 'slow' and logged as
     * such by the driver. The default value is {@link #DEFAULT_SLOW_QUERY_THRESHOLD_MS}.
     *
     * @return The threshold in milliseconds beyond which queries are considered 'slow' and logged
     *     as such by the driver.
     */
    public long getSlowQueryLatencyThresholdMillis() {
      return slowQueryLatencyThresholdMillis;
    }

    /**
     * Set the threshold in milliseconds beyond which queries are considered 'slow' and logged as
     * such by the driver.
     *
     * @param slowQueryLatencyThresholdMillis Slow queries threshold in milliseconds. It must be
     *     strictly positive.
     * @throws IllegalArgumentException if {@code slowQueryLatencyThresholdMillis <= 0}.
     */
    public void setSlowQueryLatencyThresholdMillis(long slowQueryLatencyThresholdMillis) {
      if (slowQueryLatencyThresholdMillis <= 0)
        throw new IllegalArgumentException(
            "Invalid slowQueryLatencyThresholdMillis, should be > 0, got "
                + slowQueryLatencyThresholdMillis);
      this.slowQueryLatencyThresholdMillis = slowQueryLatencyThresholdMillis;
    }

    @Override
    protected void maybeLogNormalOrSlowQuery(Host host, Statement statement, long latencyMs) {
      if (latencyMs > slowQueryLatencyThresholdMillis) {
        maybeLogSlowQuery(host, statement, latencyMs);
      } else {
        maybeLogNormalQuery(host, statement, latencyMs);
      }
    }

    protected void maybeLogSlowQuery(Host host, Statement statement, long latencyMs) {
      if (SLOW_LOGGER.isDebugEnabled()) {
        String message =
            String.format(
                SLOW_TEMPLATE_MILLIS,
                cluster.getClusterName(),
                host,
                latencyMs,
                statementAsString(statement));
        logQuery(statement, null, SLOW_LOGGER, message);
      }
    }
  }

  /**
   * A QueryLogger that uses a dynamic threshold in milliseconds to track slow queries.
   *
   * <p>Dynamic thresholds are based on per-host latency percentiles, as computed by {@link
   * PercentileTracker}.
   */
  public static class DynamicThresholdQueryLogger extends QueryLogger {

    private volatile double slowQueryLatencyThresholdPercentile;

    private volatile PercentileTracker percentileLatencyTracker;

    private DynamicThresholdQueryLogger(
        int maxQueryStringLength,
        int maxParameterValueLength,
        int maxLoggedParameters,
        double slowQueryLatencyThresholdPercentile,
        PercentileTracker percentileLatencyTracker) {
      super(maxQueryStringLength, maxParameterValueLength, maxLoggedParameters);
      this.setSlowQueryLatencyThresholdPercentile(slowQueryLatencyThresholdPercentile);
      this.setPercentileLatencyTracker(percentileLatencyTracker);
    }

    /**
     * Return the percentile tracker to use for recording per-host latency histograms. Cannot be
     * {@code null}.
     *
     * @return the percentile tracker to use.
     */
    public PercentileTracker getPercentileLatencyTracker() {
      return percentileLatencyTracker;
    }

    /**
     * Set the percentile tracker to use for recording per-host latency histograms. Cannot be {@code
     * null}.
     *
     * @param percentileLatencyTracker the percentile tracker instance to use.
     * @throws IllegalArgumentException if {@code percentileLatencyTracker == null}.
     */
    public void setPercentileLatencyTracker(PercentileTracker percentileLatencyTracker) {
      if (percentileLatencyTracker == null)
        throw new IllegalArgumentException("perHostPercentileLatencyTracker cannot be null");
      this.percentileLatencyTracker = percentileLatencyTracker;
    }

    /**
     * Return the threshold percentile beyond which queries are considered 'slow' and logged as such
     * by the driver. The default value is {@link #DEFAULT_SLOW_QUERY_THRESHOLD_PERCENTILE}.
     *
     * @return threshold percentile beyond which queries are considered 'slow' and logged as such by
     *     the driver.
     */
    public double getSlowQueryLatencyThresholdPercentile() {
      return slowQueryLatencyThresholdPercentile;
    }

    /**
     * Set the threshold percentile beyond which queries are considered 'slow' and logged as such by
     * the driver.
     *
     * @param slowQueryLatencyThresholdPercentile Slow queries threshold percentile. It must be
     *     comprised between 0 inclusive and 100 exclusive.
     * @throws IllegalArgumentException if {@code slowQueryLatencyThresholdPercentile < 0 ||
     *     slowQueryLatencyThresholdPercentile >= 100}.
     */
    public void setSlowQueryLatencyThresholdPercentile(double slowQueryLatencyThresholdPercentile) {
      if (slowQueryLatencyThresholdPercentile < 0.0 || slowQueryLatencyThresholdPercentile >= 100.0)
        throw new IllegalArgumentException(
            "Invalid slowQueryLatencyThresholdPercentile, should be >= 0 and < 100, got "
                + slowQueryLatencyThresholdPercentile);
      this.slowQueryLatencyThresholdPercentile = slowQueryLatencyThresholdPercentile;
    }

    @Override
    protected void maybeLogNormalOrSlowQuery(Host host, Statement statement, long latencyMs) {
      long threshold =
          percentileLatencyTracker.getLatencyAtPercentile(
              host, statement, null, slowQueryLatencyThresholdPercentile);
      if (threshold >= 0 && latencyMs > threshold) {
        maybeLogSlowQuery(host, statement, latencyMs, threshold);
      } else {
        maybeLogNormalQuery(host, statement, latencyMs);
      }
    }

    protected void maybeLogSlowQuery(
        Host host, Statement statement, long latencyMs, long threshold) {
      if (SLOW_LOGGER.isDebugEnabled()) {
        String message =
            String.format(
                SLOW_TEMPLATE_PERCENTILE,
                cluster.getClusterName(),
                host,
                latencyMs,
                slowQueryLatencyThresholdPercentile,
                threshold,
                statementAsString(statement));
        logQuery(statement, null, SLOW_LOGGER, message);
      }
    }

    @Override
    public void onRegister(Cluster cluster) {
      super.onRegister(cluster);
      cluster.register(percentileLatencyTracker);
    }

    // Don't unregister the latency tracker in onUnregister, we can't guess if it's being used by
    // another component
    // or not.
  }

  /** Helper class to build {@link QueryLogger} instances with a fluent API. */
  public static class Builder {

    private int maxQueryStringLength = DEFAULT_MAX_QUERY_STRING_LENGTH;

    private int maxParameterValueLength = DEFAULT_MAX_PARAMETER_VALUE_LENGTH;

    private int maxLoggedParameters = DEFAULT_MAX_LOGGED_PARAMETERS;

    private long slowQueryLatencyThresholdMillis = DEFAULT_SLOW_QUERY_THRESHOLD_MS;

    private double slowQueryLatencyThresholdPercentile = DEFAULT_SLOW_QUERY_THRESHOLD_PERCENTILE;

    private PercentileTracker percentileLatencyTracker;

    private boolean constantThreshold = true;

    /**
     * Enables slow query latency tracking based on constant thresholds.
     *
     * <p>Note: You should either use {@link #withConstantThreshold(long) constant thresholds} or
     * {@link #withDynamicThreshold(PercentileTracker, double) dynamic thresholds}, not both.
     *
     * @param slowQueryLatencyThresholdMillis The threshold in milliseconds beyond which queries are
     *     considered 'slow' and logged as such by the driver. The default value is {@link
     *     #DEFAULT_SLOW_QUERY_THRESHOLD_MS}
     * @return this {@link Builder} instance (for method chaining).
     */
    public Builder withConstantThreshold(long slowQueryLatencyThresholdMillis) {
      this.slowQueryLatencyThresholdMillis = slowQueryLatencyThresholdMillis;
      constantThreshold = true;
      return this;
    }

    /**
     * Enables slow query latency tracking based on dynamic thresholds.
     *
     * <p>Dynamic thresholds are based on latency percentiles, as computed by {@link
     * PercentileTracker}.
     *
     * <p>Note: You should either use {@link #withConstantThreshold(long) constant thresholds} or
     * {@link #withDynamicThreshold(PercentileTracker, double) dynamic thresholds}, not both.
     *
     * @param percentileLatencyTracker the {@link PercentileTracker} instance to use for recording
     *     latency histograms. Cannot be {@code null}. It will get {@link
     *     Cluster#register(LatencyTracker) registered} with the cluster at the same time as this
     *     logger.
     * @param slowQueryLatencyThresholdPercentile Slow queries threshold percentile. It must be
     *     comprised between 0 inclusive and 100 exclusive. The default value is {@link
     *     #DEFAULT_SLOW_QUERY_THRESHOLD_PERCENTILE}
     * @return this {@link Builder} instance (for method chaining).
     */
    public Builder withDynamicThreshold(
        PercentileTracker percentileLatencyTracker, double slowQueryLatencyThresholdPercentile) {
      this.percentileLatencyTracker = percentileLatencyTracker;
      this.slowQueryLatencyThresholdPercentile = slowQueryLatencyThresholdPercentile;
      constantThreshold = false;
      return this;
    }

    /**
     * Set the maximum length of a CQL query string that can be logged verbatim by the driver. Query
     * strings longer than this value will be truncated when logged.
     *
     * @param maxQueryStringLength The maximum length of a CQL query string that can be logged
     *     verbatim by the driver. It must be strictly positive or {@code -1}, in which case the
     *     query is never truncated (use with care). The default value is {@link
     *     #DEFAULT_MAX_QUERY_STRING_LENGTH}.
     * @return this {@link Builder} instance (for method chaining).
     */
    public Builder withMaxQueryStringLength(int maxQueryStringLength) {
      this.maxQueryStringLength = maxQueryStringLength;
      return this;
    }

    /**
     * Set the maximum length of a query parameter value that can be logged verbatim by the driver.
     * Parameter values longer than this value will be truncated when logged.
     *
     * @param maxParameterValueLength The maximum length of a query parameter value that can be
     *     logged verbatim by the driver. It must be strictly positive or {@code -1}, in which case
     *     the parameter value is never truncated (use with care). The default value is {@link
     *     #DEFAULT_MAX_PARAMETER_VALUE_LENGTH}.
     * @return this {@link Builder} instance (for method chaining).
     */
    public Builder withMaxParameterValueLength(int maxParameterValueLength) {
      this.maxParameterValueLength = maxParameterValueLength;
      return this;
    }

    /**
     * Set the maximum number of query parameters that can be logged by the driver. Queries with a
     * number of parameters higher than this value will not have all their parameters logged.
     *
     * @param maxLoggedParameters The maximum number of query parameters that can be logged by the
     *     driver. It must be strictly positive or {@code -1}, in which case all parameters will be
     *     logged, regardless of their number (use with care). The default value is {@link
     *     #DEFAULT_MAX_LOGGED_PARAMETERS}.
     * @return this {@link Builder} instance (for method chaining).
     */
    public Builder withMaxLoggedParameters(int maxLoggedParameters) {
      this.maxLoggedParameters = maxLoggedParameters;
      return this;
    }

    /**
     * Build the {@link QueryLogger} instance.
     *
     * @return the {@link QueryLogger} instance.
     * @throws IllegalArgumentException if the builder is unable to build a valid instance due to
     *     incorrect settings.
     */
    public QueryLogger build() {
      if (constantThreshold) {
        return new ConstantThresholdQueryLogger(
            maxQueryStringLength,
            maxParameterValueLength,
            maxLoggedParameters,
            slowQueryLatencyThresholdMillis);
      } else {
        return new DynamicThresholdQueryLogger(
            maxQueryStringLength,
            maxParameterValueLength,
            maxLoggedParameters,
            slowQueryLatencyThresholdPercentile,
            percentileLatencyTracker);
      }
    }
  }

  // Getters and Setters

  /**
   * Return the maximum length of a CQL query string that can be logged verbatim by the driver.
   * Query strings longer than this value will be truncated when logged. The default value is {@link
   * #DEFAULT_MAX_QUERY_STRING_LENGTH}.
   *
   * @return The maximum length of a CQL query string that can be logged verbatim by the driver.
   */
  public int getMaxQueryStringLength() {
    return maxQueryStringLength;
  }

  /**
   * Set the maximum length of a CQL query string that can be logged verbatim by the driver. Query
   * strings longer than this value will be truncated when logged.
   *
   * @param maxQueryStringLength The maximum length of a CQL query string that can be logged
   *     verbatim by the driver. It must be strictly positive or {@code -1}, in which case the query
   *     is never truncated (use with care).
   * @throws IllegalArgumentException if {@code maxQueryStringLength <= 0 && maxQueryStringLength !=
   *     -1}.
   */
  public void setMaxQueryStringLength(int maxQueryStringLength) {
    if (maxQueryStringLength <= 0 && maxQueryStringLength != -1)
      throw new IllegalArgumentException(
          "Invalid maxQueryStringLength, should be > 0 or -1, got " + maxQueryStringLength);
    this.maxQueryStringLength = maxQueryStringLength;
  }

  /**
   * Return the maximum length of a query parameter value that can be logged verbatim by the driver.
   * Parameter values longer than this value will be truncated when logged. The default value is
   * {@link #DEFAULT_MAX_PARAMETER_VALUE_LENGTH}.
   *
   * @return The maximum length of a query parameter value that can be logged verbatim by the
   *     driver.
   */
  public int getMaxParameterValueLength() {
    return maxParameterValueLength;
  }

  /**
   * Set the maximum length of a query parameter value that can be logged verbatim by the driver.
   * Parameter values longer than this value will be truncated when logged.
   *
   * @param maxParameterValueLength The maximum length of a query parameter value that can be logged
   *     verbatim by the driver. It must be strictly positive or {@code -1}, in which case the
   *     parameter value is never truncated (use with care).
   * @throws IllegalArgumentException if {@code maxParameterValueLength <= 0 &&
   *     maxParameterValueLength != -1}.
   */
  public void setMaxParameterValueLength(int maxParameterValueLength) {
    if (maxParameterValueLength <= 0 && maxParameterValueLength != -1)
      throw new IllegalArgumentException(
          "Invalid maxParameterValueLength, should be > 0 or -1, got " + maxParameterValueLength);
    this.maxParameterValueLength = maxParameterValueLength;
  }

  /**
   * Return the maximum number of query parameters that can be logged by the driver. Queries with a
   * number of parameters higher than this value will not have all their parameters logged. The
   * default value is {@link #DEFAULT_MAX_LOGGED_PARAMETERS}.
   *
   * @return The maximum number of query parameters that can be logged by the driver.
   */
  public int getMaxLoggedParameters() {
    return maxLoggedParameters;
  }

  /**
   * Set the maximum number of query parameters that can be logged by the driver. Queries with a
   * number of parameters higher than this value will not have all their parameters logged.
   *
   * @param maxLoggedParameters the maximum number of query parameters that can be logged by the
   *     driver. It must be strictly positive or {@code -1}, in which case all parameters will be
   *     logged, regardless of their number (use with care).
   * @throws IllegalArgumentException if {@code maxLoggedParameters <= 0 && maxLoggedParameters !=
   *     -1}.
   */
  public void setMaxLoggedParameters(int maxLoggedParameters) {
    if (maxLoggedParameters <= 0 && maxLoggedParameters != -1)
      throw new IllegalArgumentException(
          "Invalid maxLoggedParameters, should be > 0 or -1, got " + maxLoggedParameters);
    this.maxLoggedParameters = maxLoggedParameters;
  }

  /** {@inheritDoc} */
  @Override
  public void update(Host host, Statement statement, Exception exception, long newLatencyNanos) {
    if (cluster == null)
      throw new IllegalStateException(
          "This method should only be called after the logger has been registered with a cluster");

    if (statement instanceof StatementWrapper)
      statement = ((StatementWrapper) statement).getWrappedStatement();

    long latencyMs = NANOSECONDS.toMillis(newLatencyNanos);
    if (exception == null) {
      maybeLogNormalOrSlowQuery(host, statement, latencyMs);
    } else {
      maybeLogErrorQuery(host, statement, exception, latencyMs);
    }
  }

  protected abstract void maybeLogNormalOrSlowQuery(Host host, Statement statement, long latencyMs);

  protected void maybeLogNormalQuery(Host host, Statement statement, long latencyMs) {
    if (NORMAL_LOGGER.isDebugEnabled()) {
      String message =
          String.format(
              NORMAL_TEMPLATE,
              cluster.getClusterName(),
              host,
              latencyMs,
              statementAsString(statement));
      logQuery(statement, null, NORMAL_LOGGER, message);
    }
  }

  protected void maybeLogErrorQuery(
      Host host, Statement statement, Exception exception, long latencyMs) {
    if (ERROR_LOGGER.isDebugEnabled()
        && !(exception instanceof CancelledSpeculativeExecutionException)) {
      String message =
          String.format(
              ERROR_TEMPLATE,
              cluster.getClusterName(),
              host,
              latencyMs,
              statementAsString(statement));
      logQuery(statement, exception, ERROR_LOGGER, message);
    }
  }

  protected void logQuery(Statement statement, Exception exception, Logger logger, String message) {
    boolean showParameterValues = logger.isTraceEnabled();
    if (showParameterValues) {
      StringBuilder params = new StringBuilder();
      if (statement instanceof BoundStatement) {
        appendParameters((BoundStatement) statement, params, maxLoggedParameters);
      } else if (statement instanceof SimpleStatement) {
        appendParameters((SimpleStatement) statement, params, maxLoggedParameters);
      } else if (statement instanceof BatchStatement) {
        BatchStatement batchStatement = (BatchStatement) statement;
        int remaining = maxLoggedParameters;
        for (Statement inner : batchStatement.getStatements()) {
          if (inner instanceof BoundStatement) {
            remaining = appendParameters((BoundStatement) inner, params, remaining);
          } else if (inner instanceof SimpleStatement) {
            remaining = appendParameters((SimpleStatement) inner, params, remaining);
          }
        }
      } else if (statement instanceof BuiltStatement) {
        appendParameters((BuiltStatement) statement, params, maxLoggedParameters);
      }
      if (params.length() > 0) params.append("]");
      logger.trace(message + params, exception);
    } else {
      logger.debug(message, exception);
    }
  }

  protected String statementAsString(Statement statement) {
    StringBuilder sb = new StringBuilder();
    if (statement instanceof BatchStatement) {
      BatchStatement bs = (BatchStatement) statement;
      int statements = bs.getStatements().size();
      int boundValues = countBoundValues(bs);
      sb.append("[" + statements + " statements, " + boundValues + " bound values] ");
    } else if (statement instanceof BoundStatement) {
      int boundValues = ((BoundStatement) statement).wrapper.values.length;
      sb.append("[" + boundValues + " bound values] ");
    } else if (statement instanceof SimpleStatement) {
      int boundValues = ((SimpleStatement) statement).valuesCount();
      sb.append("[" + boundValues + " bound values] ");
    }

    append(statement, sb, maxQueryStringLength);
    return sb.toString();
  }

  protected int countBoundValues(BatchStatement bs) {
    int count = 0;
    for (Statement s : bs.getStatements()) {
      if (s instanceof BoundStatement) count += ((BoundStatement) s).wrapper.values.length;
      else if (s instanceof SimpleStatement) count += ((SimpleStatement) s).valuesCount();
    }
    return count;
  }

  protected int appendParameters(BoundStatement statement, StringBuilder buffer, int remaining) {
    if (remaining == 0) return 0;
    ColumnDefinitions metadata = statement.preparedStatement().getVariables();
    int numberOfParameters = metadata.size();
    if (numberOfParameters > 0) {
      List<ColumnDefinitions.Definition> definitions = metadata.asList();
      int numberOfLoggedParameters;
      if (remaining == -1) {
        numberOfLoggedParameters = numberOfParameters;
      } else {
        numberOfLoggedParameters = Math.min(remaining, numberOfParameters);
        remaining -= numberOfLoggedParameters;
      }
      for (int i = 0; i < numberOfLoggedParameters; i++) {
        if (buffer.length() == 0) buffer.append(" [");
        else buffer.append(", ");
        String value =
            statement.isSet(i)
                ? parameterValueAsString(definitions.get(i), statement.wrapper.values[i])
                : "<UNSET>";
        buffer.append(String.format("%s:%s", metadata.getName(i), value));
      }
      if (numberOfLoggedParameters < numberOfParameters) {
        buffer.append(FURTHER_PARAMS_OMITTED);
      }
    }
    return remaining;
  }

  protected String parameterValueAsString(ColumnDefinitions.Definition definition, ByteBuffer raw) {
    String valueStr;
    if (raw == null || raw.remaining() == 0) {
      valueStr = "NULL";
    } else {
      DataType type = definition.getType();
      CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry();
      TypeCodec<Object> codec = codecRegistry.codecFor(type);
      int maxParameterValueLength = this.maxParameterValueLength;
      if (type.equals(DataType.blob()) && maxParameterValueLength != -1) {
        // prevent large blobs from being converted to strings
        int maxBufferLength = Math.max(2, (maxParameterValueLength - 2) / 2);
        boolean bufferTooLarge = raw.remaining() > maxBufferLength;
        if (bufferTooLarge) {
          raw = (ByteBuffer) raw.duplicate().limit(maxBufferLength);
        }
        Object value = codec.deserialize(raw, protocolVersion());
        valueStr = codec.format(value);
        if (bufferTooLarge) {
          valueStr = valueStr + TRUNCATED_OUTPUT;
        }
      } else {
        Object value = codec.deserialize(raw, protocolVersion());
        valueStr = codec.format(value);
        if (maxParameterValueLength != -1 && valueStr.length() > maxParameterValueLength) {
          valueStr = valueStr.substring(0, maxParameterValueLength) + TRUNCATED_OUTPUT;
        }
      }
    }
    return valueStr;
  }

  protected int appendParameters(SimpleStatement statement, StringBuilder buffer, int remaining) {
    if (remaining == 0) return 0;
    int numberOfParameters = statement.valuesCount();
    if (numberOfParameters > 0) {
      int numberOfLoggedParameters;
      if (remaining == -1) {
        numberOfLoggedParameters = numberOfParameters;
      } else {
        numberOfLoggedParameters = remaining > numberOfParameters ? numberOfParameters : remaining;
        remaining -= numberOfLoggedParameters;
      }
      Iterator<String> valueNames = null;
      if (statement.usesNamedValues()) {
        valueNames = statement.getValueNames().iterator();
      }
      for (int i = 0; i < numberOfLoggedParameters; i++) {
        if (buffer.length() == 0) buffer.append(" [");
        else buffer.append(", ");
        if (valueNames != null && valueNames.hasNext()) {
          String valueName = valueNames.next();
          buffer.append(
              String.format(
                  "%s:%s", valueName, parameterValueAsString(statement.getObject(valueName))));
        } else {
          buffer.append(parameterValueAsString(statement.getObject(i)));
        }
      }
      if (numberOfLoggedParameters < numberOfParameters) {
        buffer.append(FURTHER_PARAMS_OMITTED);
      }
    }
    return remaining;
  }

  protected String parameterValueAsString(Object value) {
    String valueStr;
    if (value == null) {
      valueStr = "NULL";
    } else {
      CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry();
      TypeCodec<Object> codec = codecRegistry.codecFor(value);
      int maxParameterValueLength = this.maxParameterValueLength;
      if (codec.cqlType.equals(DataType.blob()) && maxParameterValueLength != -1) {
        // prevent large blobs from being converted to strings
        ByteBuffer buf = (ByteBuffer) value;
        int maxBufferLength = Math.max(2, (maxParameterValueLength - 2) / 2);
        boolean bufferTooLarge = buf.remaining() > maxBufferLength;
        if (bufferTooLarge) {
          value = (ByteBuffer) buf.duplicate().limit(maxBufferLength);
        }
        valueStr = codec.format(value);
        if (bufferTooLarge) {
          valueStr = valueStr + TRUNCATED_OUTPUT;
        }
      } else {
        valueStr = codec.format(value);
        if (maxParameterValueLength != -1 && valueStr.length() > maxParameterValueLength) {
          valueStr = valueStr.substring(0, maxParameterValueLength) + TRUNCATED_OUTPUT;
        }
      }
    }
    return valueStr;
  }

  protected int appendParameters(BuiltStatement statement, StringBuilder buffer, int remaining) {
    if (remaining == 0) {
      return 0;
    }
    ByteBuffer[] values =
        statement.getValues(protocolVersion(), cluster.getConfiguration().getCodecRegistry());
    int numberOfParameters = values == null ? 0 : values.length;
    if (numberOfParameters > 0) {
      int numberOfLoggedParameters;
      if (remaining == -1) {
        numberOfLoggedParameters = numberOfParameters;
      } else {
        numberOfLoggedParameters = remaining > numberOfParameters ? numberOfParameters : remaining;
        remaining -= numberOfLoggedParameters;
      }

      for (int i = 0; i < numberOfLoggedParameters; i++) {
        if (buffer.length() == 0) {
          buffer.append(" [");
        } else {
          buffer.append(", ");
        }

        buffer.append(parameterValueAsString(statement.getObject(i)));
      }
      if (numberOfLoggedParameters < numberOfParameters) {
        buffer.append(FURTHER_PARAMS_OMITTED);
      }
    }
    return remaining;
  }

  private ProtocolVersion protocolVersion() {
    // Since the QueryLogger can be registered before the Cluster was initialized, we can't retrieve
    // it at construction time. Cache it field at first use (a volatile field is good enough since
    // we
    // don't need mutual exclusion).
    if (protocolVersion == null) {
      protocolVersion = cluster.getConfiguration().getProtocolOptions().getProtocolVersion();
      // At least one connection was established when QueryLogger is invoked
      assert protocolVersion != null : "protocol version should be defined";
    }
    return protocolVersion;
  }

  protected int append(Statement statement, StringBuilder buffer, int remaining) {
    if (statement instanceof RegularStatement) {
      RegularStatement rs = (RegularStatement) statement;
      String query = rs.getQueryString();
      remaining = append(query.trim(), buffer, remaining);
    } else if (statement instanceof BoundStatement) {
      remaining =
          append(
              ((BoundStatement) statement).preparedStatement().getQueryString().trim(),
              buffer,
              remaining);
    } else if (statement instanceof BatchStatement) {
      BatchStatement batchStatement = (BatchStatement) statement;
      remaining = append("BEGIN", buffer, remaining);
      switch (batchStatement.batchType) {
        case UNLOGGED:
          append(" UNLOGGED", buffer, remaining);
          break;
        case COUNTER:
          append(" COUNTER", buffer, remaining);
          break;
      }
      remaining = append(" BATCH", buffer, remaining);
      for (Statement stmt : batchStatement.getStatements()) {
        remaining = append(" ", buffer, remaining);
        remaining = append(stmt, buffer, remaining);
      }
      remaining = append(" APPLY BATCH", buffer, remaining);
    } else {
      // Unknown types of statement
      // Call toString() as a last resort
      remaining = append(statement.toString(), buffer, remaining);
    }
    if (buffer.charAt(buffer.length() - 1) != ';') {
      remaining = append(";", buffer, remaining);
    }
    return remaining;
  }

  protected int append(CharSequence str, StringBuilder buffer, int remaining) {
    if (remaining == -2) {
      // capacity exceeded
    } else if (remaining == -1) {
      // unlimited capacity
      buffer.append(str);
    } else if (str.length() > remaining) {
      buffer.append(str, 0, remaining).append(TRUNCATED_OUTPUT);
      remaining = -2;
    } else {
      buffer.append(str);
      remaining -= str.length();
    }
    return remaining;
  }
}
