diff --git a/src/java/org/apache/cassandra/db/AbstractReadQuery.java b/src/java/org/apache/cassandra/db/AbstractReadQuery.java index 7eec41148f08..d1df5a0f253f 100644 --- a/src/java/org/apache/cassandra/db/AbstractReadQuery.java +++ b/src/java/org/apache/cassandra/db/AbstractReadQuery.java @@ -19,6 +19,8 @@ import java.util.Set; +import com.google.common.annotations.VisibleForTesting; + import org.apache.cassandra.cql3.ColumnIdentifier; import org.apache.cassandra.cql3.CqlBuilder; import org.apache.cassandra.cql3.statements.SelectOptions; @@ -62,7 +64,7 @@ public TableMetadata metadata() // Monitorable interface public String name() { - return toCQLString(); + return toRedactedCQLString(); } @Override @@ -96,23 +98,53 @@ public ColumnFilter columnFilter() } /** - * Recreate the CQL string corresponding to this query. + * Recreates the CQL string corresponding to this query, representing any specific values with '?', + * to prevent leaking sensitive data. + * @see #toCQLString(boolean) + */ + public String toRedactedCQLString() + { + return toCQLString(true); + } + + /** + * Recreates the CQL string corresponding to this query, printing specific values without any redaction. + * This might leak sensitive data if the query string ends up in logs or any other unprotected place, so this only + * should be used for debugging purposes or to present the query string to the same end user that created the query. + * @see #toCQLString(boolean) + */ + public String toUnredactedCQLString() + { + return toCQLString(false); + } + + /** + * Recreates the CQL string corresponding to this query. + *

+ * If the {@code redact} parameter is set to {@code true}, the query string will be redacted, replacing any specific + * column values with '?'. If set to {@code false}, the query string will not be redacted, and it might expose the + * queried column values which might contain sensitive data. The latter will be problematic if the query string ends + * up in logs or any other unprotected place. Therefore, non-redaction should only be used for debugging purposes or + * to present the query string to the same end user that created the query. *

* Note that in general the returned string will not be exactly the original user string, first - * because there isn't always a single syntax for a given query, but also because we don't have + * because there isn't always a single syntax for a given query, but also because we don't have * all the information needed (we know the non-PK columns queried but not the PK ones as internally - * we query them all). So this shouldn't be relied too strongly, but this should be good enough for - * debugging purpose which is what this is for. + * we query them all). So this shouldn't be relied upon too strongly, but this should be good enough for + * debugging purposes which is what this is for. + * + * @param redact whether to redact the queried column values. */ - public String toCQLString() + @VisibleForTesting + protected String toCQLString(boolean redact) { CqlBuilder builder = new CqlBuilder(); - builder.append("SELECT ").append(columnFilter().toCQLString()); + builder.append("SELECT ").append(columnFilter().toCQLString(redact)); builder.append(" FROM ").append(ColumnIdentifier.maybeQuote(metadata().keyspace)) .append('.') .append(ColumnIdentifier.maybeQuote(metadata().name)); - appendCQLWhereClause(builder); + appendCQLWhereClause(builder, redact); if (limits() != DataLimits.NONE) builder.append(' ').append(limits()); @@ -132,5 +164,5 @@ public String toCQLString() return builder.toString(); } - protected abstract void appendCQLWhereClause(CqlBuilder builder); + protected abstract void appendCQLWhereClause(CqlBuilder builder, boolean redact); } diff --git a/src/java/org/apache/cassandra/db/Clustering.java b/src/java/org/apache/cassandra/db/Clustering.java index d062837a3e27..32efdee715de 100644 --- a/src/java/org/apache/cassandra/db/Clustering.java +++ b/src/java/org/apache/cassandra/db/Clustering.java @@ -78,14 +78,14 @@ public default String toString(TableMetadata metadata) return sb.toString(); } - public default String toCQLString(TableMetadata metadata) + default String toCQLString(TableMetadata metadata, boolean redact) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < size(); i++) { ColumnMetadata c = metadata.clusteringColumns().get(i); ByteBuffer value = bufferAt(i); - sb.append(i == 0 ? "" : ", ").append(c.type.toCQLString(value)); + sb.append(i == 0 ? "" : ", ").append(c.type.toCQLString(value, redact)); } return sb.toString(); } diff --git a/src/java/org/apache/cassandra/db/DataRange.java b/src/java/org/apache/cassandra/db/DataRange.java index 71c1cef5492d..353344a49b4f 100644 --- a/src/java/org/apache/cassandra/db/DataRange.java +++ b/src/java/org/apache/cassandra/db/DataRange.java @@ -271,7 +271,7 @@ public String toString(TableMetadata metadata) return String.format("range=%s pfilter=%s", keyRange.getString(metadata.partitionKeyType), clusteringIndexFilter.toString(metadata)); } - public String toCQLString(TableMetadata metadata) + public String toCQLString(TableMetadata metadata, boolean redact) { if (isUnrestricted()) return ""; @@ -283,20 +283,20 @@ public String toCQLString(TableMetadata metadata) * key are the same. If that is the case, we want to print the query as an equality on the partition key * rather than a token range, as if it was a partition query, for better readability. */ - return ((DecoratedKey) startKey()).toCQLString(metadata); + return ((DecoratedKey) startKey()).toCQLString(metadata, redact); } else { StringBuilder builder = new StringBuilder(); if (!startKey().isMinimum()) { - appendCQLClause(startKey(), builder, metadata, true, keyRange.isStartInclusive()); + appendCQLClause(startKey(), builder, metadata, true, keyRange.isStartInclusive(), redact); } if (!stopKey().isMinimum()) { if (builder.length() > 0) builder.append(" AND "); - appendCQLClause(stopKey(), builder, metadata, false, keyRange.isEndInclusive()); + appendCQLClause(stopKey(), builder, metadata, false, keyRange.isEndInclusive(), redact); } return builder.toString(); } @@ -306,7 +306,8 @@ private void appendCQLClause(PartitionPosition pos, StringBuilder builder, TableMetadata metadata, boolean isStart, - boolean isInclusive) + boolean isInclusive, + boolean redact) { builder.append("token("); builder.append(ColumnMetadata.toCQLString(metadata.partitionKeyColumns())); @@ -315,14 +316,14 @@ private void appendCQLClause(PartitionPosition pos, { builder.append(getOperator(isStart, isInclusive)).append(' '); builder.append("token("); - appendKeyString(builder, metadata.partitionKeyType, ((DecoratedKey)pos).getKey()); + appendKeyString(builder, metadata.partitionKeyType, ((DecoratedKey)pos).getKey(), redact); builder.append(')'); } else { Token.KeyBound keyBound = (Token.KeyBound) pos; - builder.append(getOperator(isStart, isStart == keyBound.isMinimumBound)).append(" "); - builder.append(keyBound.getToken()); + builder.append(getOperator(isStart, isStart == keyBound.isMinimumBound)).append(' '); + builder.append(redact ? "?" : keyBound.getToken()); } } @@ -335,18 +336,18 @@ private static String getOperator(boolean isStart, boolean isInclusive) // TODO: this is reused in SinglePartitionReadCommand but this should not really be here. Ideally // we need a more "native" handling of composite partition keys. - public static void appendKeyString(StringBuilder builder, AbstractType type, ByteBuffer key) + public static void appendKeyString(StringBuilder builder, AbstractType type, ByteBuffer key, boolean redact) { if (type instanceof CompositeType) { CompositeType ct = (CompositeType)type; ByteBuffer[] values = ct.split(key); for (int i = 0; i < ct.subTypes().size(); i++) - builder.append(i == 0 ? "" : ", ").append(ct.subTypes().get(i).toCQLString(values[i])); + builder.append(i == 0 ? "" : ", ").append(ct.subTypes().get(i).toCQLString(values[i], redact)); } else { - builder.append(type.toCQLString(key)); + builder.append(type.toCQLString(key, redact)); } } diff --git a/src/java/org/apache/cassandra/db/DecoratedKey.java b/src/java/org/apache/cassandra/db/DecoratedKey.java index a47cf0165c60..1ed11f60765d 100644 --- a/src/java/org/apache/cassandra/db/DecoratedKey.java +++ b/src/java/org/apache/cassandra/db/DecoratedKey.java @@ -172,26 +172,27 @@ public String toString() * For multi-column keys: "k1 = 1 AND k2 = 2" * * @param metadata the table metadata + * @param redact whether to redact the key value, as in "k1 = ? AND k2 = ?". */ - public String toCQLString(TableMetadata metadata) + public String toCQLString(TableMetadata metadata, boolean redact) { List columns = metadata.partitionKeyColumns(); if (columns.size() == 1) - return toCQLString(columns.get(0), getKey()); + return toCQLString(columns.get(0), getKey(), redact); ByteBuffer[] values = ((CompositeType) metadata.partitionKeyType).split(getKey()); StringJoiner joiner = new StringJoiner(" AND "); for (int i = 0; i < columns.size(); i++) - joiner.add(toCQLString(columns.get(i), values[i])); + joiner.add(toCQLString(columns.get(i), values[i], redact)); return joiner.toString(); } - private static String toCQLString(ColumnMetadata metadata, ByteBuffer key) + private static String toCQLString(ColumnMetadata metadata, ByteBuffer key, boolean redact) { - return String.format("%s = %s", metadata.name.toCQLString(), metadata.type.toCQLString(key)); + return String.format("%s = %s", metadata.name.toCQLString(), metadata.type.toCQLString(key, redact)); } public Token getToken() diff --git a/src/java/org/apache/cassandra/db/MultiPartitionReadQuery.java b/src/java/org/apache/cassandra/db/MultiPartitionReadQuery.java index ff4d33c03bd6..7f9570b1fe02 100644 --- a/src/java/org/apache/cassandra/db/MultiPartitionReadQuery.java +++ b/src/java/org/apache/cassandra/db/MultiPartitionReadQuery.java @@ -28,14 +28,14 @@ public interface MultiPartitionReadQuery extends ReadQuery { List ranges(); - default void appendCQLWhereClause(CqlBuilder builder) + default void appendCQLWhereClause(CqlBuilder builder, boolean redact) { // Append the data ranges. TableMetadata metadata = metadata(); - boolean hasRanges = appendRanges(builder); + boolean hasRanges = appendRanges(builder, redact); // Append the clustering index filter and the row filter. - String filter = ranges().get(0).clusteringIndexFilter.toCQLString(metadata, rowFilter()); + String filter = ranges().get(0).clusteringIndexFilter.toCQLString(metadata, rowFilter() , redact); if (!filter.isEmpty()) { if (filter.startsWith("ORDER BY")) @@ -48,7 +48,7 @@ else if (hasRanges) } } - private boolean appendRanges(CqlBuilder builder) + private boolean appendRanges(CqlBuilder builder, boolean redact) { List ranges = ranges(); if (ranges.size() == 1) @@ -57,7 +57,7 @@ private boolean appendRanges(CqlBuilder builder) if (range.isUnrestricted()) return false; - String rangeString = range.toCQLString(metadata()); + String rangeString = range.toCQLString(metadata(), redact); if (!rangeString.isEmpty()) { builder.append(" WHERE ").append(rangeString); @@ -71,7 +71,7 @@ private boolean appendRanges(CqlBuilder builder) { if (i > 0) builder.append(" OR "); - builder.append(ranges.get(i).toCQLString(metadata())); + builder.append(ranges.get(i).toCQLString(metadata(), redact)); } builder.append(')'); return true; diff --git a/src/java/org/apache/cassandra/db/MultiRangeReadCommand.java b/src/java/org/apache/cassandra/db/MultiRangeReadCommand.java index 0ef61e12cb33..4f79f212d378 100644 --- a/src/java/org/apache/cassandra/db/MultiRangeReadCommand.java +++ b/src/java/org/apache/cassandra/db/MultiRangeReadCommand.java @@ -321,9 +321,9 @@ public Verb verb() } @Override - public void appendCQLWhereClause(CqlBuilder builder) + public void appendCQLWhereClause(CqlBuilder builder, boolean redact) { - MultiPartitionReadQuery.super.appendCQLWhereClause(builder); + MultiPartitionReadQuery.super.appendCQLWhereClause(builder, redact); } @Override diff --git a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java index 4dbb43cb7856..cb23b91da4d2 100644 --- a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java +++ b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java @@ -394,9 +394,9 @@ public Verb verb() } @Override - public void appendCQLWhereClause(CqlBuilder builder) + public void appendCQLWhereClause(CqlBuilder builder, boolean redact) { - PartitionRangeReadQuery.super.appendCQLWhereClause(builder); + PartitionRangeReadQuery.super.appendCQLWhereClause(builder, redact); } @Override diff --git a/src/java/org/apache/cassandra/db/ReadCommand.java b/src/java/org/apache/cassandra/db/ReadCommand.java index 1d42f0ae5053..e8a19ae5ae21 100644 --- a/src/java/org/apache/cassandra/db/ReadCommand.java +++ b/src/java/org/apache/cassandra/db/ReadCommand.java @@ -607,7 +607,7 @@ private Threshold.GuardedCounter createTombstoneCounter() Threshold guardrail = shouldRespectTombstoneThresholds() ? Guardrails.scannedTombstones : DefaultGuardrail.DefaultThreshold.NEVER_TRIGGERED; - return guardrail.newCounter(ReadCommand.this::toCQLString, true, null); + return guardrail.newCounter(ReadCommand.this::toRedactedCQLString, false, null); } private MetricRecording() @@ -671,7 +671,7 @@ private void countTombstone(ClusteringPrefix clustering) { metric.tombstoneFailures.inc(); throw new TombstoneOverwhelmingException(tombstones.get(), - ReadCommand.this.toCQLString(), + ReadCommand.this.toRedactedCQLString(), ReadCommand.this.metadata(), currentKey, clustering); diff --git a/src/java/org/apache/cassandra/db/ReadExecutionController.java b/src/java/org/apache/cassandra/db/ReadExecutionController.java index 9968942da038..1ca52dc481b8 100644 --- a/src/java/org/apache/cassandra/db/ReadExecutionController.java +++ b/src/java/org/apache/cassandra/db/ReadExecutionController.java @@ -112,7 +112,7 @@ int oldestUnrepairedTombstone() { return oldestUnrepairedTombstone; } - + void updateMinOldestUnrepairedTombstone(int candidate) { oldestUnrepairedTombstone = Math.min(oldestUnrepairedTombstone, candidate); @@ -254,7 +254,7 @@ public boolean isRepairedDataDigestConclusive() { return repairedDataInfo.isConclusive(); } - + public RepairedDataInfo getRepairedDataInfo() { return repairedDataInfo; @@ -262,7 +262,7 @@ public RepairedDataInfo getRepairedDataInfo() private void addSample() { - String cql = command.toCQLString(); + String cql = command.toRedactedCQLString(); int timeMicros = (int) Math.min(TimeUnit.NANOSECONDS.toMicros(clock.now() - createdAtNanos), Integer.MAX_VALUE); ColumnFamilyStore cfs = ColumnFamilyStore.getIfExists(baseMetadata.id); if (cfs != null) diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java index ddcb912dc63a..de6700d312ae 100644 --- a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java +++ b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java @@ -1152,9 +1152,9 @@ public Verb verb() } @Override - public void appendCQLWhereClause(CqlBuilder builder) + public void appendCQLWhereClause(CqlBuilder builder, boolean redact) { - SinglePartitionReadQuery.super.appendCQLWhereClause(builder); + SinglePartitionReadQuery.super.appendCQLWhereClause(builder, redact); } protected void serializeSelection(DataOutputPlus out, int version) throws IOException diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java b/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java index 1f55368b4db4..e31546954937 100644 --- a/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java +++ b/src/java/org/apache/cassandra/db/SinglePartitionReadQuery.java @@ -159,16 +159,16 @@ default boolean selectsClustering(DecoratedKey key, Clustering clustering) return rowFilter().clusteringKeyRestrictionsAreSatisfiedBy(clustering); } - default void appendCQLWhereClause(CqlBuilder builder) + default void appendCQLWhereClause(CqlBuilder builder, boolean redact) { builder.append(" WHERE "); // Append the partition key restrictions. TableMetadata metadata = metadata(); - builder.append(partitionKey().toCQLString(metadata)); + builder.append(partitionKey().toCQLString(metadata, redact)); // Append the clustering index filter and the row filter. - String filter = clusteringIndexFilter().toCQLString(metadata(), rowFilter()); + String filter = clusteringIndexFilter().toCQLString(metadata(), rowFilter(), redact); if (!filter.isEmpty()) { builder.append(filter.startsWith("ORDER BY") ? " " : " AND ") diff --git a/src/java/org/apache/cassandra/db/Slices.java b/src/java/org/apache/cassandra/db/Slices.java index 75ed9b10a374..a9fcd44c06cb 100644 --- a/src/java/org/apache/cassandra/db/Slices.java +++ b/src/java/org/apache/cassandra/db/Slices.java @@ -153,9 +153,10 @@ public ClusteringBound end() * * @param metadata the table metadata * @param rowFilter a row filter + * @param redact whether to redact the slice column values * @return a CQL string representing this slice and the specified {@link RowFilter} */ - public abstract String toCQLString(TableMetadata metadata, RowFilter rowFilter); + public abstract String toCQLString(TableMetadata metadata, RowFilter rowFilter, boolean redact); /** * Checks if this Slices is empty. @@ -565,7 +566,7 @@ public String toString() } @Override - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, boolean redact) { StringBuilder sb = new StringBuilder(); @@ -617,7 +618,7 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) if (values.size() == 1) { - sb.append(" = ").append(column.type.toCQLString(first.startValue)); + sb.append(" = ").append(column.type.toCQLString(first.startValue, redact)); } else { @@ -625,7 +626,7 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) int j = 0; for (ByteBuffer value : values) { - sb.append(j++ == 0 ? "" : ", ").append(column.type.toCQLString(value)); + sb.append(j++ == 0 ? "" : ", ").append(column.type.toCQLString(value, redact)); } sb.append(')'); } @@ -648,7 +649,7 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) else operator = first.startInclusive ? Operator.GTE : Operator.GT; sb.append(' ').append(operator).append(' ') - .append(column.type.toCQLString(first.startValue)); + .append(column.type.toCQLString(first.startValue, redact)); } if (first.endValue != null) { @@ -661,7 +662,7 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) else operator = first.endInclusive ? Operator.LTE : Operator.LT; sb.append(' ').append(operator).append(' ') - .append(column.type.toCQLString(first.endValue)); + .append(column.type.toCQLString(first.endValue, redact)); } } @@ -676,7 +677,7 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) // Append the row filter. if (!rowFilter.isEmpty()) { - String filter = rowFilter.toCQLString(); + String filter = rowFilter.toCQLString(redact); sb.append(filter.startsWith("ORDER BY") ? " " : " AND "); sb.append(filter); } @@ -803,9 +804,9 @@ public String toString() } @Override - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, boolean redact) { - return rowFilter.toCQLString(); + return rowFilter.toCQLString(redact); } } @@ -885,7 +886,7 @@ public String toString() } @Override - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, boolean redact) { return ""; } diff --git a/src/java/org/apache/cassandra/db/VirtualTablePartitionRangeReadQuery.java b/src/java/org/apache/cassandra/db/VirtualTablePartitionRangeReadQuery.java index 9e73bfb0907e..1a0ccac58fe1 100644 --- a/src/java/org/apache/cassandra/db/VirtualTablePartitionRangeReadQuery.java +++ b/src/java/org/apache/cassandra/db/VirtualTablePartitionRangeReadQuery.java @@ -95,8 +95,8 @@ protected UnfilteredPartitionIterator queryVirtualTable() } @Override - public void appendCQLWhereClause(CqlBuilder builder) + public void appendCQLWhereClause(CqlBuilder builder, boolean redact) { - PartitionRangeReadQuery.super.appendCQLWhereClause(builder); + PartitionRangeReadQuery.super.appendCQLWhereClause(builder, redact); } } diff --git a/src/java/org/apache/cassandra/db/VirtualTableSinglePartitionReadQuery.java b/src/java/org/apache/cassandra/db/VirtualTableSinglePartitionReadQuery.java index 30b5697accd2..03e4134c3ffd 100644 --- a/src/java/org/apache/cassandra/db/VirtualTableSinglePartitionReadQuery.java +++ b/src/java/org/apache/cassandra/db/VirtualTableSinglePartitionReadQuery.java @@ -75,9 +75,9 @@ private VirtualTableSinglePartitionReadQuery(TableMetadata metadata, } @Override - public void appendCQLWhereClause(CqlBuilder builder) + public void appendCQLWhereClause(CqlBuilder builder, boolean redact) { - SinglePartitionReadQuery.super.appendCQLWhereClause(builder); + SinglePartitionReadQuery.super.appendCQLWhereClause(builder, redact); } @Override diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java index 17f6615c43d4..0c6b2017276c 100644 --- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java +++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java @@ -159,9 +159,10 @@ static interface InternalDeserializer * * @param metadata the table metadata * @param rowFilter a row filter + * @param redact whether to redact the clustering column value * @return a CQL string representing this clustering index filter and the specified {@link RowFilter} */ - String toCQLString(TableMetadata metadata, RowFilter rowFilter); + String toCQLString(TableMetadata metadata, RowFilter rowFilter, boolean redact); public interface Serializer { diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java index 347e88e1c7ff..830094514a54 100644 --- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java +++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java @@ -163,10 +163,10 @@ public String toString(TableMetadata metadata) } @Override - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, boolean redact) { if (metadata.clusteringColumns().isEmpty() || clusterings.isEmpty()) - return rowFilter.toCQLString(); + return rowFilter.toCQLString(redact); boolean isSingleColumn = metadata.clusteringColumns().size() == 1; boolean isSingleClustering = clusterings.size() == 1; @@ -183,7 +183,7 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) { sb.append(i++ == 0 ? "" : ", ") .append(isSingleColumn ? "" : '(') - .append(clustering.toCQLString(metadata)) + .append(clustering.toCQLString(metadata, redact)) .append(isSingleColumn ? "" : ')'); maxClusteringSize = Math.max(maxClusteringSize, clustering.size()); @@ -200,7 +200,7 @@ public String toCQLString(TableMetadata metadata, RowFilter rowFilter) if (!rowFilter.isEmpty()) { - String filter = rowFilter.toCQLString(); + String filter = rowFilter.toCQLString(redact); sb.append(filter.startsWith("ORDER BY") ? " " : " AND "); sb.append(filter); } diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java index 85820771a6d8..4cc78fc1715b 100644 --- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java +++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java @@ -134,10 +134,10 @@ public String toString(TableMetadata metadata) } @Override - public String toCQLString(TableMetadata metadata, RowFilter rowFilter) + public String toCQLString(TableMetadata metadata, RowFilter rowFilter, boolean redact) { StringBuilder sb = new StringBuilder(); - sb.append(slices.toCQLString(metadata, rowFilter)); + sb.append(slices.toCQLString(metadata, rowFilter, redact)); appendOrderByToCQLString(metadata, sb); return sb.toString(); } diff --git a/src/java/org/apache/cassandra/db/filter/ColumnFilter.java b/src/java/org/apache/cassandra/db/filter/ColumnFilter.java index dbb11ad26250..7fe468593b03 100644 --- a/src/java/org/apache/cassandra/db/filter/ColumnFilter.java +++ b/src/java/org/apache/cassandra/db/filter/ColumnFilter.java @@ -385,9 +385,10 @@ public boolean isWildcard() /** * Returns the CQL string corresponding to this {@code ColumnFilter}. * + * @param redact whether to redact the queried column names, in case they contain sensitive data. * @return the CQL string corresponding to this {@code ColumnFilter}. */ - public abstract String toCQLString(); + public abstract String toCQLString(boolean redact); /** * Returns the sub-selections or {@code null} if there are none. @@ -722,7 +723,8 @@ public String toString() return "*/*"; } - public String toCQLString() + @Override + public String toCQLString(boolean redact) { return "*"; } @@ -924,20 +926,22 @@ public String toString() { prefix = queried.statics.isEmpty() ? "/" - : String.format("+%s/", toString(queried.statics.selectOrderIterator(), false)); + : String.format("+%s/", toString(queried.statics.selectOrderIterator(), false, false)); } - return prefix + toString(queried.selectOrderIterator(), false); + return prefix + toString(queried.selectOrderIterator(), false, false); } @Override - public String toCQLString() + public String toCQLString(boolean redact) { - return queried.isEmpty() ? "*" : toString(queried.selectOrderIterator(), true); + return queried.isEmpty() ? "*" : toString(queried.selectOrderIterator(), true, redact); } - private String toString(Iterator columns, boolean cql) + private String toString(Iterator columns, boolean cql, boolean redact) { + assert cql || !redact : "Cannot redact non-CQL representation"; + StringJoiner joiner = cql ? new StringJoiner(", ") : new StringJoiner(", ", "[", "]"); while (columns.hasNext()) @@ -952,7 +956,7 @@ private String toString(Iterator columns, boolean cql) if (s.isEmpty()) joiner.add(columnName); else - s.forEach(subSel -> joiner.add(String.format("%s%s", columnName, subSel.toString(cql)))); + s.forEach(subSel -> joiner.add(String.format("%s%s", columnName, subSel.toString(cql, redact)))); } return joiner.toString(); } diff --git a/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java b/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java index 1438663a9178..74672deb5476 100644 --- a/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java +++ b/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java @@ -91,10 +91,17 @@ public int compareTo(ColumnSubselection other) @Override public String toString() { - return toString(false); + return toString(false, false); } - protected abstract String toString(boolean cql); + /** + * Returns a string representation of this subselection. + * + * @param cql if true, the string representation will be in CQL format + * @param redact if true, the string representation will redact sensitive data + * @return a string representation of this subselection + */ + protected abstract String toString(boolean cql, boolean redact); private static class Slice extends ColumnSubselection { @@ -130,13 +137,13 @@ else if (cmp.compare(to, path) < 0) } @Override - protected String toString(boolean cql) + protected String toString(boolean cql, boolean redact) { // This asserts we're dealing with a collection since that's the only thing it's used for so far. AbstractType type = ((CollectionType)column().type).nameComparator(); return String.format("[%s:%s]", - from == CellPath.BOTTOM ? "" : (cql ? type.toCQLString(from.get(0)) : type.getString(from.get(0))), - to == CellPath.TOP ? "" : (cql ? type.toCQLString(to.get(0)) : type.getString(to.get(0)))); + from == CellPath.BOTTOM ? "" : (cql ? type.toCQLString(from.get(0), redact) : type.getString(from.get(0))), + to == CellPath.TOP ? "" : (cql ? type.toCQLString(to.get(0), redact) : type.getString(to.get(0)))); } } @@ -166,11 +173,11 @@ public int compareInclusionOf(CellPath path) } @Override - protected String toString(boolean cql) + protected String toString(boolean cql, boolean redact) { - // This assert we're dealing with a collection since that's the only thing it's used for so far. + // This asserts we're dealing with a collection since that's the only thing it's used for so far. AbstractType type = ((CollectionType)column().type).nameComparator(); - return String.format("[%s]", cql ? type.toCQLString(element.get(0)) : type.getString(element.get(0))); + return String.format("[%s]", cql ? type.toCQLString(element.get(0), redact) : type.getString(element.get(0))); } } diff --git a/src/java/org/apache/cassandra/db/filter/RowFilter.java b/src/java/org/apache/cassandra/db/filter/RowFilter.java index f4446901f8fb..b8a7aa65f428 100644 --- a/src/java/org/apache/cassandra/db/filter/RowFilter.java +++ b/src/java/org/apache/cassandra/db/filter/RowFilter.java @@ -335,9 +335,9 @@ public String toString() return root.toString(); } - public String toCQLString() + public String toCQLString(boolean redact) { - return root.toCQLString(); + return root.toCQLString(redact); } public static Builder builder() @@ -739,10 +739,10 @@ private int numFilteredValues() @Override public String toString() { - return toCQLString(); + return toCQLString(false); } - public String toCQLString() + public String toCQLString(boolean redact) { StringBuilder sb = new StringBuilder(); for (Expression expression : expressions) @@ -751,14 +751,14 @@ public String toCQLString() continue; if (sb.length() > 0) sb.append(isDisjunction ? " OR " : " AND "); - sb.append(expression.toCQLString()); + sb.append(expression.toCQLString(redact)); } for (FilterElement child : children) { if (sb.length() > 0) sb.append(isDisjunction ? " OR " : " AND "); sb.append('('); - sb.append(child.toCQLString()); + sb.append(child.toCQLString(redact)); sb.append(')'); } for (Expression expression : expressions) @@ -767,7 +767,7 @@ public String toCQLString() continue; if (sb.length() > 0) sb.append(' '); - sb.append(expression.toCQLString()); + sb.append(expression.toCQLString(redact)); } return sb.toString(); } @@ -1053,10 +1053,10 @@ public int hashCode() @Override public String toString() { - return toCQLString(); + return toCQLString(false); } - public String toCQLString() + public String toCQLString(boolean redact) { return ""; } @@ -1366,7 +1366,7 @@ private boolean containsKey(TableMetadata metadata, DecoratedKey partitionKey, R } @Override - public String toCQLString() + public String toCQLString(boolean redact) { AbstractType type = column.type; switch (operator) @@ -1391,19 +1391,33 @@ public String toCQLString() // These don't have a value, so we return here to prevent an error calling type.getString(value) return String.format("ORDER BY %s %s", column.name.toCQLString(), operator); case ANN: - return String.format("ORDER BY %s ANN OF %s", column.name.toCQLString(), valueAsCQLString(type, value)); + return String.format("ORDER BY %s ANN OF %s", column.name.toCQLString(), truncateValue(type.toCQLString(value, redact))); + case LIKE_PREFIX: + return likeToCQLString("'%s%%'", type, redact); + case LIKE_SUFFIX: + return likeToCQLString("'%%%s'", type, redact); + case LIKE_CONTAINS: + return likeToCQLString("'%%%s%%'", type, redact); + case LIKE_MATCHES: + return likeToCQLString("'%s'", type, redact); default: break; } - return String.format("%s %s %s", column.name.toCQLString(), operator, valueAsCQLString(type, value)); + return String.format("%s %s %s", column.name.toCQLString(), operator, truncateValue(type.toCQLString(value, redact))); + } + + private String likeToCQLString(String pattern, AbstractType type, boolean redact) + { + if (redact) + return String.format("%s LIKE ?", column.name.toCQLString()); + + String stringValue = String.format(pattern, type.getString(value)); + return String.format("%s LIKE %s", column.name.toCQLString(), truncateValue(stringValue)); } - private static String valueAsCQLString(AbstractType type, ByteBuffer value) + private static String truncateValue(String value) { - var valueString = type.toCQLString(value); - if (valueString.length() > 9) - valueString = valueString.substring(0, 6) + "..."; - return valueString; + return value.length() > 9 ? value.substring(0, 6) + "..." : value; } @Override @@ -1515,14 +1529,14 @@ private boolean isSatisfiedByEq(TableMetadata metadata, DecoratedKey partitionKe } @Override - public String toCQLString() + public String toCQLString(boolean redact) { MapType mt = (MapType) column.type; return String.format("%s[%s] %s %s", column.name.toCQLString(), - mt.nameComparator().toCQLString(key), + mt.nameComparator().toCQLString(key, redact), operator, - mt.valueComparator().toCQLString(value)); + mt.valueComparator().toCQLString(value, redact)); } @Override @@ -1689,13 +1703,13 @@ public boolean isSatisfiedBy(TableMetadata metadata, DecoratedKey partitionKey, } @Override - public String toCQLString() + public String toCQLString(boolean redact) { return String.format("GEO_DISTANCE(%s, %s) %s %s", column.name.toCQLString(), - column.type.toCQLString(value), + column.type.toCQLString(value, redact), distanceOperator, - FloatType.instance.toCQLString(distance)); + FloatType.instance.toCQLString(distance, redact)); } @Override @@ -1769,7 +1783,7 @@ public ByteBuffer getValue() } @Override - public String toCQLString() + public String toCQLString(boolean redact) { return String.format("expr(%s, %s)", targetIndex.name, diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java index db415465c853..d82de63eea66 100644 --- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java +++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java @@ -183,13 +183,20 @@ public ByteBuffer decompose(T value) } /** - * Generates a CQL literal representing the specified binary value. + * Returns a CQL literal representing the specified binary value, or "?" if redaction is requested. * - * @param bytes the value to convert to a CQL literal. + * @param bytes the value to convert to a CQL literal + * @param redact whether to mask the value with '?' (for redaction purposes) */ - public String toCQLString(ByteBuffer bytes) + public String toCQLString(ByteBuffer bytes, boolean redact) { - return bytes == null ? "null" : asCQL3Type().toCQLLiteral(bytes, ProtocolVersion.CURRENT); + if (redact) + return "?"; + + if (bytes == null) + return "null"; + + return asCQL3Type().toCQLLiteral(bytes, ProtocolVersion.CURRENT); } /** get a string representation of the bytes used for various identifier (NOT just for log messages) */ diff --git a/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java b/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java index 0f8555f17aa3..44f2c32cfb8c 100644 --- a/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java +++ b/src/java/org/apache/cassandra/db/monitoring/MonitoringTask.java @@ -47,7 +47,7 @@ * We also log timed out operations, see CASSANDRA-7392. * Since CASSANDRA-12403 we also log queries that were slow. */ -class MonitoringTask +public class MonitoringTask { private static final String LINE_SEPARATOR = getProperty("line.separator"); private static final Logger logger = LoggerFactory.getLogger(MonitoringTask.class); @@ -65,7 +65,7 @@ class MonitoringTask private static final int MAX_OPERATIONS = Integer.parseInt(System.getProperty(Config.PROPERTY_PREFIX + "monitoring_max_operations", "50")); @VisibleForTesting - static MonitoringTask instance = make(REPORT_INTERVAL_MS, MAX_OPERATIONS); + public static MonitoringTask instance = make(REPORT_INTERVAL_MS, MAX_OPERATIONS); private final ScheduledFuture reportingTask; private final OperationsQueue failedOperationsQueue; @@ -133,7 +133,7 @@ private List getLogMessages(AggregatedOperations operations) } @VisibleForTesting - private void logOperations(long approxCurrentTimeNanos) + public void logOperations(long approxCurrentTimeNanos) { logSlowOperations(approxCurrentTimeNanos); logFailedOperations(approxCurrentTimeNanos); diff --git a/src/java/org/apache/cassandra/db/rows/AbstractRow.java b/src/java/org/apache/cassandra/db/rows/AbstractRow.java index 74f111a4ed23..2990e6449c94 100644 --- a/src/java/org/apache/cassandra/db/rows/AbstractRow.java +++ b/src/java/org/apache/cassandra/db/rows/AbstractRow.java @@ -140,10 +140,10 @@ public String toString(TableMetadata metadata, boolean includeClusterKeys, boole sb.append(" ]"); } sb.append(": "); - if(includeClusterKeys) + if (includeClusterKeys) sb.append(clustering().toString(metadata)); else - sb.append(clustering().toCQLString(metadata)); + sb.append(clustering().toCQLString(metadata, false)); sb.append(" | "); boolean isFirst = true; for (ColumnData cd : this) diff --git a/src/java/org/apache/cassandra/index/sai/plan/Plan.java b/src/java/org/apache/cassandra/index/sai/plan/Plan.java index 668d1d4cf22a..9623bfe5efd8 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/Plan.java +++ b/src/java/org/apache/cassandra/index/sai/plan/Plan.java @@ -283,21 +283,51 @@ enum ControlFlow { Continue, Break } */ protected abstract double estimateSelectivity(); + /** + * Formats the whole plan as a pretty tree, redacting the queried column values. + */ + public final String toRedactedStringRecursive() + { + return toStringRecursive(true); + } + + /** + * Formats the whole plan as a pretty tree, not redacting the queried column values. + */ + public final String toUnredactedStringRecursive() + { + return toStringRecursive(false); + } + /** * Formats the whole plan as a pretty tree + * + * @param redact whether to redact the queried column values. */ - public final String toStringRecursive() + private final String toStringRecursive(boolean redact) { - TreeFormatter formatter = new TreeFormatter<>(Plan::toString, Plan::subplans); + TreeFormatter formatter = new TreeFormatter<>(plan -> plan.toString(redact), Plan::subplans); return formatter.format(this); } /** - * Returns the string representation of this node only + * Returns the string representation of this node only, without redacting the queried column values. + * @see #toString(boolean) */ + @Override public final String toString() { - String title = title(); + return toString(false); + } + + /** + * Returns the string representation of this node only + * + * @param redact whether to redact the queried column values. + */ + public final String toString(boolean redact) + { + String title = title(redact); String description = description(); return (title.isEmpty()) ? String.format("%s (%s)\n%s", getClass().getSimpleName(), cost(), description).stripTrailing() @@ -306,17 +336,17 @@ public final String toString() /** * Returns additional information specific to the node displayed in the first line. - * The information is included in the output of {@link #toString()} and {@link #toStringRecursive()}. + * The information is included in the output of {@link #toString()} and {@link #toRedactedStringRecursive()}. * It is up to subclasses to implement it. */ - protected String title() + protected String title(boolean redact) { return ""; } /** * Returns additional information specific to the node, displayed below the title. - * The information is included in the output of {@link #toString()} and {@link #toStringRecursive()}. + * The information is included in the output of {@link #toString()} and {@link #toRedactedStringRecursive()}. * It is up to subclasses to implement it. */ protected String description() @@ -345,7 +375,7 @@ protected String description() public final Plan optimize() { if (logger.isTraceEnabled()) - logger.trace("Optimizing plan:\n{}", this.toStringRecursive()); + logger.trace("Optimizing plan:\n{}", toRedactedStringRecursive()); Plan bestPlanSoFar = this; List leaves = nodesOfType(Leaf.class); @@ -360,14 +390,14 @@ public final Plan optimize() Plan candidate = bestPlanSoFar.removeRestriction(leaf.id); if (logger.isTraceEnabled()) - logger.trace("Candidate query plan:\n{}", candidate.toStringRecursive()); + logger.trace("Candidate query plan:\n{}", candidate.toRedactedStringRecursive()); if (candidate.fullCost() <= bestPlanSoFar.fullCost()) bestPlanSoFar = candidate; } if (logger.isTraceEnabled()) - logger.trace("Optimized plan:\n{}", bestPlanSoFar.toStringRecursive()); + logger.trace("Optimized plan:\n{}", bestPlanSoFar.toRedactedStringRecursive()); return bestPlanSoFar; } @@ -796,7 +826,7 @@ public IndexScan(Factory factory, int id, Expression predicate, long matchingKey } @Override - protected final String title() + protected final String title(boolean redact) { return String.format("of %s (sel: %.9f, step: %.1f)", getIndexName(), selectivity(), access.meanDistance()); @@ -1689,9 +1719,9 @@ protected Filter withAccess(Access access) } @Override - protected String title() + protected String title(boolean redact) { - return String.format("%s (sel: %.9f)", filter, selectivity() / source.get().selectivity()); + return String.format("%s (sel: %.9f)", filter.toCQLString(redact), selectivity() / source.get().selectivity()); } } @@ -1757,7 +1787,7 @@ protected RowsIteration withAccess(Access access) } @Override - protected String title() + protected String title(boolean redact) { return "" + limit; } diff --git a/src/java/org/apache/cassandra/index/sai/plan/QueryController.java b/src/java/org/apache/cassandra/index/sai/plan/QueryController.java index e8eb24cb1007..df7f8c3dcffd 100644 --- a/src/java/org/apache/cassandra/index/sai/plan/QueryController.java +++ b/src/java/org/apache/cassandra/index/sai/plan/QueryController.java @@ -415,11 +415,11 @@ Plan buildPlan() updateIndexMetricsQueriesCount(plan); if (logger.isTraceEnabled()) - logger.trace("Query execution plan:\n" + plan.toStringRecursive()); + logger.trace("Query execution plan:\n" + plan.toRedactedStringRecursive()); if (Tracing.isTracing()) { - Tracing.trace("Query execution plan:\n" + plan.toStringRecursive()); + Tracing.trace("Query execution plan:\n" + plan.toUnredactedStringRecursive()); List origIndexScans = keysIterationPlan.nodesOfType(Plan.IndexScan.class); List selectedIndexScans = plan.nodesOfType(Plan.IndexScan.class); Tracing.trace("Selecting {} {} of {} out of {} indexes", diff --git a/src/java/org/apache/cassandra/service/context/DefaultOperationContext.java b/src/java/org/apache/cassandra/service/context/DefaultOperationContext.java index f22d8e9dd6fc..f5916ff66a8c 100644 --- a/src/java/org/apache/cassandra/service/context/DefaultOperationContext.java +++ b/src/java/org/apache/cassandra/service/context/DefaultOperationContext.java @@ -58,7 +58,7 @@ static class Factory implements OperationContext.Factory @Override public OperationContext forRead(ReadCommand command, ColumnFamilyStore cfs) { - return new DefaultOperationContext(command::toCQLString); + return new DefaultOperationContext(command::toUnredactedCQLString); } } } diff --git a/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java b/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java index 56c53d7b7a85..32ccccc119f7 100644 --- a/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java +++ b/src/java/org/apache/cassandra/service/reads/ReplicaFilteringProtection.java @@ -196,17 +196,25 @@ public void close() // of cached rows we have had during the query. if (!hitFailureThreshold && maxRowsCached > cachedRowsWarnThreshold) { - String message = String.format("Replica filtering protection has cached up to %d rows during query %s, " + - "which is over the warning threshold of %d rows defined by " + - "'cached_replica_rows_warn_threshold' in cassandra.yaml.", - maxRowsCached, command.toCQLString(), cachedRowsWarnThreshold); - - ClientWarn.instance.warn(message); - oneMinuteLogger.warn(message); - Tracing.trace(message); + String clearMessage = cachedRowsWarnMessage(false); + String redactedMessage = cachedRowsWarnMessage(true); + + ClientWarn.instance.warn(clearMessage); + oneMinuteLogger.warn(redactedMessage); + Tracing.trace(clearMessage); } } + private String cachedRowsWarnMessage(boolean redact) + { + return String.format("Replica filtering protection has cached up to %d rows during query %s, " + + "which is over the warning threshold of %d rows defined by " + + "'cached_replica_rows_warn_threshold' in cassandra.yaml.", + maxRowsCached, + redact ? command.toRedactedCQLString() : command.toUnredactedCQLString(), + cachedRowsWarnThreshold); + } + @Override public UnfilteredRowIterators.MergeListener getRowMergeListener(DecoratedKey partitionKey, List versions) { @@ -286,17 +294,25 @@ private void incrementCachedRows() if (currentRowsCached == cachedRowsFailThreshold + 1) { hitFailureThreshold = true; - String message = String.format("Replica filtering protection has cached %d rows during query %s, " + - "which is over the failure threshold of %d rows defined by " + - "'cached_replica_rows_fail_threshold' in cassandra.yaml.", - currentRowsCached, command.toCQLString(), cachedRowsFailThreshold); - - logger.error(message); - Tracing.trace(message); - throw new OverloadedException(message); + String clearMessage = cachedRowsFailMessage(false); + String redactedMessage = cachedRowsFailMessage(true); + + logger.error(redactedMessage); + Tracing.trace(clearMessage); + throw new OverloadedException(redactedMessage); } } + private String cachedRowsFailMessage(boolean redact) + { + return String.format("Replica filtering protection has cached %d rows during query %s, " + + "which is over the failure threshold of %d rows defined by " + + "'cached_replica_rows_fail_threshold' in cassandra.yaml.", + currentRowsCached, + redact ? command.toRedactedCQLString() : command.toUnredactedCQLString(), + cachedRowsFailThreshold); + } + private void releaseCachedRows(int count) { maxRowsCached = Math.max(currentRowsCached, maxRowsCached); diff --git a/src/java/org/apache/cassandra/service/reads/repair/ReadRepairEvent.java b/src/java/org/apache/cassandra/service/reads/repair/ReadRepairEvent.java index 5cec8025ea14..b98ee56ebea1 100644 --- a/src/java/org/apache/cassandra/service/reads/repair/ReadRepairEvent.java +++ b/src/java/org/apache/cassandra/service/reads/repair/ReadRepairEvent.java @@ -22,7 +22,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -66,7 +65,7 @@ enum ReadRepairEventType { this.keyspace = readRepair.cfs.keyspace; this.tableName = readRepair.cfs.getTableName(); - this.cqlCommand = readRepair.command.toCQLString(); + this.cqlCommand = readRepair.command.toRedactedCQLString(); this.consistency = readRepair.replicaPlan().consistencyLevel(); this.speculativeRetry = readRepair.cfs.metadata().params.speculativeRetry.kind(); this.destinations = destinations; diff --git a/test/distributed/org/apache/cassandra/distributed/test/SlowQueryLoggerTest.java b/test/distributed/org/apache/cassandra/distributed/test/SlowQueryLoggerTest.java new file mode 100644 index 000000000000..6dc8889837c0 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/SlowQueryLoggerTest.java @@ -0,0 +1,138 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed 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 org.apache.cassandra.distributed.test; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.function.Consumer; + +import org.junit.Test; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.SuperCall; +import org.apache.cassandra.db.ReadCommand; +import org.apache.cassandra.db.ReadExecutionController; +import org.apache.cassandra.db.monitoring.MonitoringTask; +import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.ICoordinator; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.utils.Throwables; +import org.assertj.core.api.AbstractIterableAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ListAssert; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.apache.cassandra.distributed.api.ConsistencyLevel.ALL; +import static org.apache.cassandra.utils.MonotonicClock.approxTime; + +public class SlowQueryLoggerTest extends TestBaseImpl +{ + private static final String TABLE = "t"; + private static final int SLOW_QUERY_LOG_TIMEOUT_MS = 100; + + /** + * Test that the slow query logger does not log sensitive data. + */ + @Test + public void testDoesNotLogSensitiveData() throws Throwable + { + // effectively disable the scheduled monitoring task so we control it manually for better test stability + System.setProperty("cassandra.slow_query_log_interval_in_ms", "3600000"); + + try (Cluster cluster = init(Cluster.build(2) + .withInstanceInitializer(SlowQueryLoggerTest.BBHelper::install) + .withConfig(config -> config.set("slow_query_log_timeout_in_ms", SLOW_QUERY_LOG_TIMEOUT_MS)) + .start())) + { + ICoordinator coordinator = cluster.coordinator(1); + IInvokableInstance node = cluster.get(2); + + cluster.schemaChange(format("CREATE TABLE %s.%s (k text, c text, v text, PRIMARY KEY (k, c))")); + coordinator.execute(format("INSERT INTO %s.%s (k, c, v) VALUES ('secret_k', 'secret_c', 'secret_v')"), ALL); + + long mark = node.logs().mark(); + coordinator.execute(format("SELECT * FROM %s.%s WHERE k = 'secret_k' AND c = 'secret_c' AND v = 'secret_v' ALLOW FILTERING"), ALL); + node.runOnInstance(() -> MonitoringTask.instance.logOperations(approxTime.now())); + + assertLogsContain(mark, node, "Some operations were slow", format("