summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorErin Zhu <erin.zhu@mongodb.com>2024-02-16 17:39:46 +0000
committerMongoDB Bot <mongo-bot@mongodb.com>2024-02-16 18:58:24 +0000
commitf8a2c84946db93762b46bd9946d8c8d87db5d93f (patch)
tree9ca02fde324bb16e3f4d33df6beac897c9f56b61
parent71b8d3570f69d3c9bc05793b2421ff98de04e990 (diff)
SERVER-86652: Query Stats Backport to 7.0 Batch #6r7.0.7-rc0
Includes changes from SERVER-79071: Add new privilege action type for non-tokenized $queryStats invocation SERVER-79568: Allow Non-Tokenized $queryStats Invocation without Test Commands Enabled SERVER-79453: Add featureFlagQueryStatsFindCommand for Milestone 0 SERVER-78985 specify PartitionedCache edge cases SERVER-78943 Test LRUCache eviction behavior for large queryStats entries SERVER-79828: Only compute query shape for mongos find command when necessary SERVER-79984: Remove query stats passthroughs from amazon-linux2-arm64-fixed-concurrent-transactions variant SERVER-77001 Improve implementation of RateLimiting SERVER-80151 Move query stats files to a new directory SERVER-78083: Add ability to log $queryStats output on each invocation SERVER-80201 split query_stats into multiple files SERVER-76328: Scrutinize expression parser for representative query round-tripping SERVER-78049: Track query stats on changeStream and virtual collections SERVER-80360 Fix query stats in list serialization perf regression SERVER-80304: Make $queryStats parameter hmacKey required for hmac-sha-256 SERVER-80411 Move 'hint' out of query shape and into query stats store SERVER-80934 Add google benchmark for shapification SERVER-80821 Use original request namespace in query shape SERVER-79794 silently catch DBException when $queryStats shapification process bloats the query's shape past the max BSON size limit SERVER-80941 Speed up shapifying path expressions SERVER-80878 Add QueryStatsStore insertion/eviction listener to track size SERVER-78025 Remove skip tag and TODO from working test and Enterprise SERVER-80151 Move query stats files to a new directory GitOrigin-RevId: af54ee776360f35dd25de76279e21785c7cdb681
-rw-r--r--buildscripts/resmokeconfig/suites/benchmarks.yml1
-rw-r--r--buildscripts/resmokeconfig/suites/benchmarks_query.yml2
-rw-r--r--etc/evergreen.yml3
-rw-r--r--jstests/auth/lib/commands_lib.js26
-rw-r--r--jstests/core/views/invalid_system_views.js2
-rw-r--r--jstests/libs/query_stats_utils.js63
-rw-r--r--jstests/noPassthrough/queryStats/application_name_find.js4
-rw-r--r--jstests/noPassthrough/queryStats/clear_query_stats_store.js14
-rw-r--r--jstests/noPassthrough/queryStats/documentSourceQueryStats_redaction_parameters.js12
-rw-r--r--jstests/noPassthrough/queryStats/feature_flag_off_sampling_rate_on.js8
-rw-r--r--jstests/noPassthrough/queryStats/find_cmd_one_way_tokenization.js31
-rw-r--r--jstests/noPassthrough/queryStats/geometry_without_coordinates.js2
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_agg_cmd_collect_on_mongos.js152
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_changeStreams.js61
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_collect_on_mongos.js307
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_collectionType.js36
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_disable_after_initial_request.js5
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_expressions.js76
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_feature_flag.js6
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_feature_flags_small_off_big_on.js63
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_feature_flags_small_on_big_off.js66
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_find_cmd_collect_on_mongos.js130
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_key.js14
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_logging.js99
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_metrics_across_getMore_calls.js2
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_regex.js2
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_sampling_rate.js25
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_server_status_metrics.js49
-rw-r--r--jstests/noPassthrough/queryStats/query_stats_upgrade.js11
-rw-r--r--jstests/noPassthrough/queryStats/redact_queries_with_nonobject_fields.js4
-rw-r--r--jstests/noPassthrough/queryStats/redact_sensitive_fields_in_log.js5
-rw-r--r--jstests/noPassthrough/queryStats/redact_sensitive_fields_in_profile.js2
-rw-r--r--jstests/noPassthrough/queryStats/repl_set_query_stats_key.js31
-rw-r--r--jstests/noPassthrough/queryStats/single_batch_on_mongod.js95
-rw-r--r--jstests/noPassthrough/queryStats/single_batch_on_mongod_agg_cmd.js61
-rw-r--r--jstests/noPassthrough/queryStats/single_batch_on_mongod_find_cmd.js59
-rw-r--r--jstests/noPassthroughWithMongod/query_stats_configuration.js15
-rw-r--r--src/mongo/db/SConscript43
-rw-r--r--src/mongo/db/auth/SConscript2
-rw-r--r--src/mongo/db/auth/action_type.idl3
-rw-r--r--src/mongo/db/auth/builtin_roles.yml1
-rw-r--r--src/mongo/db/catalog/SConscript18
-rw-r--r--src/mongo/db/clientcursor.cpp2
-rw-r--r--src/mongo/db/collection_type.h44
-rw-r--r--src/mongo/db/commands/SConscript8
-rw-r--r--src/mongo/db/commands/find_cmd.cpp27
-rw-r--r--src/mongo/db/commands/run_aggregate.cpp23
-rw-r--r--src/mongo/db/concurrency/SConscript4
-rw-r--r--src/mongo/db/curop.h2
-rw-r--r--src/mongo/db/db_raii.cpp8
-rw-r--r--src/mongo/db/index/SConscript2
-rw-r--r--src/mongo/db/matcher/expression_path.h7
-rw-r--r--src/mongo/db/ops/SConscript4
-rw-r--r--src/mongo/db/pipeline/SConscript6
-rw-r--r--src/mongo/db/pipeline/aggregate_command.idl4
-rw-r--r--src/mongo/db/pipeline/document_source_query_stats.cpp55
-rw-r--r--src/mongo/db/pipeline/document_source_query_stats.h12
-rw-r--r--src/mongo/db/pipeline/document_source_query_stats.idl2
-rw-r--r--src/mongo/db/pipeline/document_source_query_stats_test.cpp10
-rw-r--r--src/mongo/db/pipeline/document_source_query_stats_validators.cpp2
-rw-r--r--src/mongo/db/pipeline/document_source_query_stats_validators.h2
-rw-r--r--src/mongo/db/query/SConscript56
-rw-r--r--src/mongo/db/query/lru_key_value.h37
-rw-r--r--src/mongo/db/query/lru_key_value_test.cpp63
-rw-r--r--src/mongo/db/query/partitioned_cache.h40
-rw-r--r--src/mongo/db/query/plan_cache.h2
-rw-r--r--src/mongo/db/query/query_feature_flags.idl7
-rw-r--r--src/mongo/db/query/query_knobs.idl2
-rw-r--r--src/mongo/db/query/query_shape.cpp91
-rw-r--r--src/mongo/db/query/query_shape.h6
-rw-r--r--src/mongo/db/query/query_shape_test.cpp6
-rw-r--r--src/mongo/db/query/query_stats/README.md28
-rw-r--r--src/mongo/db/query/query_stats/SConscript102
-rw-r--r--src/mongo/db/query/query_stats/aggregate_key_generator.cpp (renamed from src/mongo/db/query/query_stats_aggregate_key_generator.cpp)5
-rw-r--r--src/mongo/db/query/query_stats/aggregate_key_generator.h (renamed from src/mongo/db/query/query_stats_aggregate_key_generator.h)11
-rw-r--r--src/mongo/db/query/query_stats/aggregated_metric.h78
-rw-r--r--src/mongo/db/query/query_stats/find_key_generator.cpp (renamed from src/mongo/db/query/query_stats_find_key_generator.cpp)4
-rw-r--r--src/mongo/db/query/query_stats/find_key_generator.h (renamed from src/mongo/db/query/query_stats_find_key_generator.h)9
-rw-r--r--src/mongo/db/query/query_stats/key_generator.h (renamed from src/mongo/db/query/query_stats_key_generator.h)25
-rw-r--r--src/mongo/db/query/query_stats/query_stats.cpp (renamed from src/mongo/db/query/query_stats.cpp)160
-rw-r--r--src/mongo/db/query/query_stats/query_stats.h (renamed from src/mongo/db/query/query_stats.h)207
-rw-r--r--src/mongo/db/query/query_stats/query_stats_entry.cpp76
-rw-r--r--src/mongo/db/query/query_stats/query_stats_entry.h117
-rw-r--r--src/mongo/db/query/query_stats/query_stats_store_test.cpp (renamed from src/mongo/db/query/query_stats_store_test.cpp)291
-rw-r--r--src/mongo/db/query/query_stats/rate_limiting.cpp (renamed from src/mongo/db/query/rate_limiting.cpp)9
-rw-r--r--src/mongo/db/query/query_stats/rate_limiting.h (renamed from src/mongo/db/query/rate_limiting.h)7
-rw-r--r--src/mongo/db/query/query_stats/rate_limiting_bm.cpp144
-rw-r--r--src/mongo/db/query/query_stats/rate_limiting_test.cpp (renamed from src/mongo/db/query/rate_limiting_test.cpp)2
-rw-r--r--src/mongo/db/query/query_stats/shapifying_bm.cpp143
-rw-r--r--src/mongo/db/query/query_stats/transform_algorithm.idl (renamed from src/mongo/db/query/query_stats_transform_algorithm.idl)0
-rw-r--r--src/mongo/db/query/query_stats/util.cpp (renamed from src/mongo/db/query/query_stats_util.cpp)2
-rw-r--r--src/mongo/db/query/query_stats/util.h (renamed from src/mongo/db/query/query_stats_util.h)0
-rw-r--r--src/mongo/db/query/serialization_options.cpp132
-rw-r--r--src/mongo/db/query/serialization_options.h1
-rw-r--r--src/mongo/db/query/shape_helpers.cpp112
-rw-r--r--src/mongo/db/query/shape_helpers.h49
-rw-r--r--src/mongo/db/query/stats/SConscript2
-rw-r--r--src/mongo/db/repl/SConscript4
-rw-r--r--src/mongo/db/s/SConscript2
-rw-r--r--src/mongo/db/service_context.cpp5
-rw-r--r--src/mongo/db/stats/SConscript2
-rw-r--r--src/mongo/db/storage/kv/SConscript2
-rw-r--r--src/mongo/db/storage/wiredtiger/SConscript2
-rw-r--r--src/mongo/db/timeseries/SConscript4
-rw-r--r--src/mongo/logv2/log_component.h1
-rw-r--r--src/mongo/s/SConscript6
-rw-r--r--src/mongo/s/commands/cluster_find_cmd.h26
-rw-r--r--src/mongo/s/query/SConscript4
-rw-r--r--src/mongo/s/query/cluster_aggregate.cpp5
-rw-r--r--src/mongo/s/query/cluster_client_cursor_impl.cpp2
-rw-r--r--src/mongo/s/query/cluster_client_cursor_mock.h2
-rw-r--r--src/mongo/s/query/cluster_cursor_manager.cpp2
-rw-r--r--src/mongo/s/query/cluster_find.cpp2
113 files changed, 2796 insertions, 1136 deletions
diff --git a/buildscripts/resmokeconfig/suites/benchmarks.yml b/buildscripts/resmokeconfig/suites/benchmarks.yml
index 49ed4e9dcb1..fe1349e1a83 100644
--- a/buildscripts/resmokeconfig/suites/benchmarks.yml
+++ b/buildscripts/resmokeconfig/suites/benchmarks.yml
@@ -21,6 +21,7 @@ selector:
# These benchmarks are being run as part of the benchmarks_query.yml
- build/install/bin/percentile_algo_bm*
- build/install/bin/window_function_percentile_bm*
+ - build/install/bin/rate_limiting_bm*
# These benchmarks are being run as part of the benchmarks_expression*.yml
- build/install/bin/expression_bm*
- build/install/bin/sbe_expression_bm*
diff --git a/buildscripts/resmokeconfig/suites/benchmarks_query.yml b/buildscripts/resmokeconfig/suites/benchmarks_query.yml
index d8e1fd3f93f..cf324ce8f69 100644
--- a/buildscripts/resmokeconfig/suites/benchmarks_query.yml
+++ b/buildscripts/resmokeconfig/suites/benchmarks_query.yml
@@ -7,6 +7,8 @@ selector:
# The trailing asterisk is for handling the .exe extension on Windows.
- build/install/bin/percentile_algo_bm*
- build/install/bin/window_function_percentile_bm*
+ - build/install/bin/rate_limiting_bm*
+ - build/install/bin/shapifying_bm*
executor:
config: {}
diff --git a/etc/evergreen.yml b/etc/evergreen.yml
index 757423a91c9..275ee570ff8 100644
--- a/etc/evergreen.yml
+++ b/etc/evergreen.yml
@@ -2941,9 +2941,6 @@ buildvariants:
- name: vector_search
- name: vector_search_auth
- name: vector_search_ssl
- - name: query_stats_passthrough
- - name: query_stats_passthrough_writeonly
- - name: query_stats_mongos_passthrough
- <<: *enterprise-rhel-80-64-bit-dynamic-classic-engine
name: &enterprise-rhel-80-64-bit-dynamic-classic-engine-query-patch-only enterprise-rhel-80-64-bit-dynamic-classic-engine-query-patch-only
diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js
index bb35e07db8d..dd349450ea4 100644
--- a/jstests/auth/lib/commands_lib.js
+++ b/jstests/auth/lib/commands_lib.js
@@ -6515,14 +6515,24 @@ export const authCommandsLib = {
]
},
{
- // Test that only clusterManager has permission to run $queryStats
- testname: "testTelemetryReadPrivilege",
- command: {aggregate: 1, pipeline: [{$queryStats: {}}], cursor: {}},
- skipSharded: false,
- skipTest: (conn) => {
- return !TestData.setParameters.featureFlagQueryStats;
- },
- testcases: [{runOnDb: adminDbName, roles: roles_clusterManager}]
+ // Test that only clusterManager has permission to run $queryStats without transformation
+ testname: "testQueryStatsReadPrivilege",
+ command: {aggregate: 1, pipeline: [{$queryStats: {}}], cursor: {}},
+ skipSharded: false,
+ skipTest: (conn) => {
+ return !TestData.setParameters.featureFlagQueryStats && !TestData.setParameters.featureFlagQueryStatsFindCommand;
+ },
+ testcases: [{runOnDb: adminDbName, roles: roles_clusterManager}]
+ },
+ {
+ // Test that only clusterManager has permission to run $queryStats with transformation
+ testname: "testQueryStatsReadTransformedPrivilege",
+ command: {aggregate: 1, pipeline: [{$queryStats: {transformIdentifiers: {algorithm: "hmac-sha-256", hmacKey: BinData(8, "MjM0NTY3ODkxMDExMTIxMzE0MTUxNjE3MTgxOTIwMjE=")}}}], cursor: {}},
+ skipSharded: false,
+ skipTest: (conn) => {
+ return !TestData.setParameters.featureFlagQueryStats && !TestData.setParameters.featureFlagQueryStatsFindCommand;
+ },
+ testcases: [{runOnDb: adminDbName, roles: roles_clusterManager}]
},
{
testname: "top",
diff --git a/jstests/core/views/invalid_system_views.js b/jstests/core/views/invalid_system_views.js
index 109ac9e1a94..859138ef67e 100644
--- a/jstests/core/views/invalid_system_views.js
+++ b/jstests/core/views/invalid_system_views.js
@@ -17,8 +17,6 @@
* tenant_migration_incompatible,
* uses_compact,
* references_foreign_collection,
- * # TODO SERVER-78025 reenable query stats coverage on this test
- * skip_for_query_stats
* ]
*/
diff --git a/jstests/libs/query_stats_utils.js b/jstests/libs/query_stats_utils.js
index 62154ddb916..9cc0c306245 100644
--- a/jstests/libs/query_stats_utils.js
+++ b/jstests/libs/query_stats_utils.js
@@ -199,6 +199,61 @@ function confirmAllExpectedFieldsPresent(expectedKey, resultingKey) {
assert.eq(fieldsCounter, Object.keys(expectedKey).length, resultingKey);
}
+function assertExpectedResults(results,
+ expectedQueryStatsKey,
+ expectedExecCount,
+ expectedDocsReturnedSum,
+ expectedDocsReturnedMax,
+ expectedDocsReturnedMin,
+ expectedDocsReturnedSumOfSq,
+ getMores) {
+ const {key, metrics} = results;
+ confirmAllExpectedFieldsPresent(expectedQueryStatsKey, key);
+ assert.eq(expectedExecCount, metrics.execCount);
+ assert.docEq({
+ sum: NumberLong(expectedDocsReturnedSum),
+ max: NumberLong(expectedDocsReturnedMax),
+ min: NumberLong(expectedDocsReturnedMin),
+ sumOfSquares: NumberLong(expectedDocsReturnedSumOfSq)
+ },
+ metrics.docsReturned);
+
+ const {
+ firstSeenTimestamp,
+ latestSeenTimestamp,
+ lastExecutionMicros,
+ totalExecMicros,
+ firstResponseExecMicros
+ } = metrics;
+
+ // The tests can't predict exact timings, so just assert these three fields have been set (are
+ // non-zero).
+ assert.neq(lastExecutionMicros, NumberLong(0));
+ assert.neq(firstSeenTimestamp.getTime(), 0);
+ assert.neq(latestSeenTimestamp.getTime(), 0);
+
+ const distributionFields = ['sum', 'max', 'min', 'sumOfSquares'];
+ for (const field of distributionFields) {
+ assert.neq(totalExecMicros[field], NumberLong(0));
+ assert.neq(firstResponseExecMicros[field], NumberLong(0));
+ if (getMores) {
+ // If there are getMore calls, totalExecMicros fields should be greater than or equal to
+ // firstResponseExecMicros.
+ if (field == 'min' || field == 'max') {
+ // In the case that we've executed multiple queries with the same shape, it is
+ // possible for the min or max to be equal.
+ assert.gte(totalExecMicros[field], firstResponseExecMicros[field]);
+ } else {
+ assert.gt(totalExecMicros[field], firstResponseExecMicros[field]);
+ }
+ } else {
+ // If there are no getMore calls, totalExecMicros fields should be equal to
+ // firstResponseExecMicros.
+ assert.eq(totalExecMicros[field], firstResponseExecMicros[field]);
+ }
+ }
+}
+
function asFieldPath(str) {
return "$" + str;
}
@@ -206,3 +261,11 @@ function asFieldPath(str) {
function asVarRef(str) {
return "$$" + str;
}
+
+function resetQueryStatsStore(conn, queryStatsStoreSize) {
+ // Set the cache size to 0MB to clear the queryStats store, and then reset to
+ // queryStatsStoreSize.
+ assert.commandWorked(conn.adminCommand({setParameter: 1, internalQueryStatsCacheSize: "0MB"}));
+ assert.commandWorked(
+ conn.adminCommand({setParameter: 1, internalQueryStatsCacheSize: queryStatsStoreSize}));
+}
diff --git a/jstests/noPassthrough/queryStats/application_name_find.js b/jstests/noPassthrough/queryStats/application_name_find.js
index 29e887391a4..c45b6b97e9c 100644
--- a/jstests/noPassthrough/queryStats/application_name_find.js
+++ b/jstests/noPassthrough/queryStats/application_name_find.js
@@ -1,14 +1,12 @@
/**
* Test that applicationName and namespace appear in queryStats for the find command.
- * @tags: [featureFlagQueryStats]
+ * @tags: [featureFlagQueryStatsFindCommand]
*/
load("jstests/libs/query_stats_utils.js");
(function() {
"use strict";
const kApplicationName = "MongoDB Shell";
-const kHashedCollName = "w6Ax20mVkbJu4wQWAMjL8Sl+DfXAr2Zqdc3kJRB7Oo0=";
-const kHashedFieldName = "lU7Z0mLRPRUL+RfAD5jhYPRRpXBsZBxS/20EzDwfOG4=";
// Turn on the collecting of queryStats metrics.
let options = {
diff --git a/jstests/noPassthrough/queryStats/clear_query_stats_store.js b/jstests/noPassthrough/queryStats/clear_query_stats_store.js
index 4cdf67ebd99..a21d11c9ace 100644
--- a/jstests/noPassthrough/queryStats/clear_query_stats_store.js
+++ b/jstests/noPassthrough/queryStats/clear_query_stats_store.js
@@ -1,13 +1,13 @@
/**
- * Test that the telemetry store can be cleared when the cache size is reset to 0.
+ * Test that the query stats store can be cleared when the cache size is reset to 0.
* @tags: [featureFlagQueryStats]
*/
-load("jstests/libs/query_stats_utils.js"); // For verifyMetrics.
+load("jstests/libs/query_stats_utils.js"); // For verifyMetrics and getQueryStats.
(function() {
"use strict";
-// Turn on the collecting of telemetry metrics.
+// Turn on the collecting of query stats metrics.
let options = {
setParameter: {internalQueryStatsRateLimit: -1, internalQueryStatsCacheSize: "10MB"},
};
@@ -26,9 +26,10 @@ for (var j = 0; j < 10; ++j) {
}
// Confirm number of entries in the store and that none have been evicted.
-let telemetryResults = testDB.getSiblingDB("admin").aggregate([{$queryStats: {}}]).toArray();
-assert.eq(telemetryResults.length, 10, telemetryResults);
+let res = getQueryStats(conn);
+assert.eq(res.length, 10, res);
assert.eq(testDB.serverStatus().metrics.queryStats.numEvicted, 0);
+assert.gt(testDB.serverStatus().metrics.queryStats.queryStatsStoreSizeEstimateBytes, 0);
// Command to clear the cache.
assert.commandWorked(testDB.adminCommand({setParameter: 1, internalQueryStatsCacheSize: "0MB"}));
@@ -36,8 +37,9 @@ assert.commandWorked(testDB.adminCommand({setParameter: 1, internalQueryStatsCac
// 10 regular queries plus the $queryStats query, means 11 entries evicted when the cache is
// cleared.
assert.eq(testDB.serverStatus().metrics.queryStats.numEvicted, 11);
+assert.eq(testDB.serverStatus().metrics.queryStats.queryStatsStoreSizeEstimateBytes, 0);
-// Calling $queryStats should fail when the telemetry store size is 0 bytes.
+// Calling $queryStats should fail when the query stats store size is 0 bytes.
assert.throwsWithCode(() => testDB.getSiblingDB("admin").aggregate([{$queryStats: {}}]), 6579000);
MongoRunner.stopMongod(conn);
}());
diff --git a/jstests/noPassthrough/queryStats/documentSourceQueryStats_redaction_parameters.js b/jstests/noPassthrough/queryStats/documentSourceQueryStats_redaction_parameters.js
index becc4566ae7..05ffaa49433 100644
--- a/jstests/noPassthrough/queryStats/documentSourceQueryStats_redaction_parameters.js
+++ b/jstests/noPassthrough/queryStats/documentSourceQueryStats_redaction_parameters.js
@@ -1,10 +1,10 @@
/**
* Test the $queryStats hmac properties.
- * @tags: [featureFlagQueryStats]
+ * @tags: [featureFlagQueryStatsFindCommand]
*/
load("jstests/aggregation/extras/utils.js"); // For assertAdminDBErrCodeAndErrMsgContains.
-load("jstests/libs/query_stats_utils.js");
+load("jstests/libs/query_stats_utils.js"); // For getQueryStatsFindCmd
(function() {
"use strict";
@@ -84,6 +84,14 @@ function runTest(conn) {
40414,
"BSON field '$queryStats.transformIdentifiers.algorithm' is missing but a required field");
+ // TransformIdentifiers with algorithm but missing hmacKey throws error.
+ pipeline = [{$queryStats: {transformIdentifiers: {algorithm: "hmac-sha-256"}}}];
+ assertAdminDBErrCodeAndErrMsgContains(
+ coll,
+ pipeline,
+ ErrorCodes.FailedToParse,
+ "The 'hmacKey' parameter of the $queryStats stage must be specified when applying the hmac-sha-256 algorithm");
+
// Parameter object with unrecognized key throws error.
pipeline =
[{$queryStats: {transformIdentifiers: {algorithm: "hmac-sha-256", hmacStrategy: "on"}}}];
diff --git a/jstests/noPassthrough/queryStats/feature_flag_off_sampling_rate_on.js b/jstests/noPassthrough/queryStats/feature_flag_off_sampling_rate_on.js
index 26f4dd5e6a1..68e1b4447e2 100644
--- a/jstests/noPassthrough/queryStats/feature_flag_off_sampling_rate_on.js
+++ b/jstests/noPassthrough/queryStats/feature_flag_off_sampling_rate_on.js
@@ -1,5 +1,5 @@
/**
- * Test that calls to read from telemetry store fail when feature flag is turned off and sampling
+ * Test that calls to read from query stats store fail when feature flag is turned off and sampling
* rate > 0.
*/
load('jstests/libs/analyze_plan.js');
@@ -17,8 +17,10 @@ const testdb = conn.getDB('test');
// This test specifically tests error handling when the feature flag is not on.
// TODO SERVER-65800 This test can be deleted when the feature is on by default.
-if (!conn || FeatureFlagUtil.isEnabled(testdb, "QueryStats")) {
- jsTestLog(`Skipping test since feature flag is disabled. conn: ${conn}`);
+// TODO SERVER-79494 remove reference to featureFlagQueryStatsFindCommand.
+if (!conn || FeatureFlagUtil.isEnabled(testdb, "QueryStats") ||
+ FeatureFlagUtil.isEnabled(testdb, "QueryStatsFindCommand")) {
+ jsTestLog(`Skipping test since feature flag is enabled. conn: ${conn}`);
if (conn) {
MongoRunner.stopMongod(conn);
}
diff --git a/jstests/noPassthrough/queryStats/find_cmd_one_way_tokenization.js b/jstests/noPassthrough/queryStats/find_cmd_one_way_tokenization.js
index c933a889203..62d07cba355 100644
--- a/jstests/noPassthrough/queryStats/find_cmd_one_way_tokenization.js
+++ b/jstests/noPassthrough/queryStats/find_cmd_one_way_tokenization.js
@@ -5,7 +5,6 @@ load("jstests/libs/query_stats_utils.js");
(function() {
"use strict";
-const kHashedCollName = "w6Ax20mVkbJu4wQWAMjL8Sl+DfXAr2Zqdc3kJRB7Oo0=";
const kHashedFieldName = "lU7Z0mLRPRUL+RfAD5jhYPRRpXBsZBxS/20EzDwfOG4=";
function runTest(conn) {
@@ -42,7 +41,7 @@ function runTest(conn) {
queryStats[1].key.queryShape.filter);
}
-const conn = MongoRunner.runMongod({
+let conn = MongoRunner.runMongod({
setParameter: {
internalQueryStatsRateLimit: -1,
featureFlagQueryStats: true,
@@ -51,7 +50,17 @@ const conn = MongoRunner.runMongod({
runTest(conn);
MongoRunner.stopMongod(conn);
-const st = new ShardingTest({
+// TODO SERVER-79494 remove second run with find-command-only feature flag
+conn = MongoRunner.runMongod({
+ setParameter: {
+ internalQueryStatsRateLimit: -1,
+ featureFlagQueryStatsFindCommand: true,
+ }
+});
+runTest(conn);
+MongoRunner.stopMongod(conn);
+
+let st = new ShardingTest({
mongos: 1,
shards: 1,
config: 1,
@@ -65,4 +74,20 @@ const st = new ShardingTest({
});
runTest(st.s);
st.stop();
+
+// TODO SERVER-79494 remove second run with find-command-only feature flag
+st = new ShardingTest({
+ mongos: 1,
+ shards: 1,
+ config: 1,
+ rs: {nodes: 1},
+ mongosOptions: {
+ setParameter: {
+ internalQueryStatsRateLimit: -1,
+ featureFlagQueryStatsFindCommand: true,
+ }
+ },
+});
+runTest(st.s);
+st.stop();
}());
diff --git a/jstests/noPassthrough/queryStats/geometry_without_coordinates.js b/jstests/noPassthrough/queryStats/geometry_without_coordinates.js
index 98ba6ec40aa..a11d6b1f38f 100644
--- a/jstests/noPassthrough/queryStats/geometry_without_coordinates.js
+++ b/jstests/noPassthrough/queryStats/geometry_without_coordinates.js
@@ -1,6 +1,6 @@
// This test was designed to reproduce SERVER-77430. There was a mistaken assertion in a parser that
// we are interested in proving will not fail here.
-// @tags: [featureFlagQueryStats]
+// @tags: [featureFlagQueryStatsFindCommand]
(function() {
"use strict";
diff --git a/jstests/noPassthrough/queryStats/query_stats_agg_cmd_collect_on_mongos.js b/jstests/noPassthrough/queryStats/query_stats_agg_cmd_collect_on_mongos.js
new file mode 100644
index 00000000000..21c24a85c56
--- /dev/null
+++ b/jstests/noPassthrough/queryStats/query_stats_agg_cmd_collect_on_mongos.js
@@ -0,0 +1,152 @@
+/**
+ * Test that mongos is collecting query stats metrics for agg queries.
+ * @tags: [featureFlagQueryStats]
+ */
+
+load('jstests/libs/query_stats_utils.js');
+
+(function() {
+"use strict";
+
+const setup = () => {
+ const st = new ShardingTest({
+ mongos: 1,
+ shards: 1,
+ config: 1,
+ rs: {nodes: 1},
+ mongosOptions: {
+ setParameter: {
+ internalQueryStatsRateLimit: -1,
+ }
+ },
+ });
+ const mongos = st.s;
+ const db = mongos.getDB("test");
+ const coll = db.coll;
+ coll.insert({v: 1});
+ coll.insert({v: 4});
+ return st;
+};
+
+// Assert that, for agg queries, no query stats results are written until a cursor has reached
+// exhaustion; ensure accurate results once they're written.
+{
+ const st = setup();
+ const db = st.s.getDB("test");
+ const coll = db.coll;
+
+ const queryStatsKey = {
+ queryShape: {
+ cmdNs: {db: "test", coll: "coll"},
+ command: "aggregate",
+ pipeline: [
+ {$match: {$and: [{v: {$gt: "?number"}}, {v: {$lt: "?number"}}]}},
+ {$project: {_id: true, hello: true}}
+ ]
+
+ },
+ cursor: {batchSize: "?number"},
+ applicationName: "MongoDB Shell",
+ };
+
+ const cursor = coll.aggregate(
+ [
+ {$match: {v: {$gt: 0, $lt: 5}}},
+ {$project: {hello: true}},
+ ],
+ {cursor: {batchSize: 1}}); // returns 1 doc
+
+ // Since the cursor hasn't been exhausted yet, ensure no query stats results have been written
+ // yet.
+ let queryStats = getQueryStats(db);
+ assert.eq(0, queryStats.length, queryStats);
+
+ // Run a getMore to exhaust the cursor, then ensure query stats results have been written
+ // accurately. batchSize must be 2 so the cursor recognizes exhaustion.
+ assert.commandWorked(db.runCommand({
+ getMore: cursor.getId(),
+ collection: coll.getName(),
+ batchSize: 2
+ })); // returns 1 doc, exhausts the cursor
+ queryStats = getQueryStatsAggCmd(db);
+ assert.eq(1, queryStats.length, queryStats);
+ assertExpectedResults(queryStats[0],
+ queryStatsKey,
+ /* expectedExecCount */ 1,
+ /* expectedDocsReturnedSum */ 2,
+ /* expectedDocsReturnedMax */ 2,
+ /* expectedDocsReturnedMin */ 2,
+ /* expectedDocsReturnedSumOfSq */ 4,
+ /* getMores */ true);
+
+ // Run more queries (to exhaustion) with the same query shape, and ensure query stats results
+ // are accurate.
+ coll.aggregate([
+ {$match: {v: {$gt: 0, $lt: 5}}},
+ {$project: {hello: true}},
+ ]); // returns 2 docs
+ coll.aggregate([
+ {$match: {v: {$gt: 2, $lt: 3}}},
+ {$project: {hello: true}},
+ ]); // returns 0 docs
+ coll.aggregate([
+ {$match: {v: {$gt: 0, $lt: 2}}},
+ {$project: {hello: true}},
+ ]); // returns 1 doc
+ queryStats = getQueryStatsAggCmd(db);
+ assert.eq(1, queryStats.length, queryStats);
+ assertExpectedResults(queryStats[0],
+ queryStatsKey,
+ /* expectedExecCount */ 4,
+ /* expectedDocsReturnedSum */ 5,
+ /* expectedDocsReturnedMax */ 2,
+ /* expectedDocsReturnedMin */ 0,
+ /* expectedDocsReturnedSumOfSq */ 9,
+ /* getMores */ true);
+
+ st.stop();
+}
+
+// Assert on batchSize-limited agg queries that killCursors will write metrics with partial results
+// to the query stats store.
+{
+ const st = setup();
+ const db = st.s.getDB("test");
+ const coll = db.coll;
+
+ const queryStatsKey = {
+ queryShape: {
+ cmdNs: {db: "test", coll: "coll"},
+ command: "aggregate",
+ pipeline: [{$match: {$and: [{v: {$gt: "?number"}}, {v: {$lt: "?number"}}]}}]
+ },
+ cursor: {batchSize: "?number"},
+ applicationName: "MongoDB Shell",
+ };
+
+ const cursor1 = coll.aggregate(
+ [
+ {$match: {v: {$gt: 0, $lt: 5}}},
+ ],
+ {cursor: {batchSize: 1}}); // returns 1 doc
+ const cursor2 = coll.aggregate(
+ [
+ {$match: {v: {$gt: 0, $lt: 2}}},
+ ],
+ {cursor: {batchSize: 1}}); // returns 1 doc
+
+ assert.commandWorked(
+ db.runCommand({killCursors: coll.getName(), cursors: [cursor1.getId(), cursor2.getId()]}));
+ const queryStats = getQueryStats(db);
+ assert.eq(1, queryStats.length);
+ assertExpectedResults(queryStats[0],
+ queryStatsKey,
+ /* expectedExecCount */ 2,
+ /* expectedDocsReturnedSum */ 2,
+ /* expectedDocsReturnedMax */ 1,
+ /* expectedDocsReturnedMin */ 1,
+ /* expectedDocsReturnedSumOfSq */ 2,
+ /* getMores */ false);
+ st.stop();
+}
+}());
diff --git a/jstests/noPassthrough/queryStats/query_stats_changeStreams.js b/jstests/noPassthrough/queryStats/query_stats_changeStreams.js
new file mode 100644
index 00000000000..a279396629d
--- /dev/null
+++ b/jstests/noPassthrough/queryStats/query_stats_changeStreams.js
@@ -0,0 +1,61 @@
+// Tests the collection of query stats for a change stream query.
+// @tags: [
+// uses_change_streams,
+// requires_replication,
+// requires_sharding,
+// featureFlagQueryStats
+// ]
+load("jstests/libs/change_stream_util.js"); // For ChangeStreamTest.
+load("jstests/libs/fixture_helpers.js"); // For FixtureHelpers
+load("jstests/libs/query_stats_utils.js"); // For getQueryStatsAggCmd.
+(function() {
+"use strict";
+function runTest(conn) {
+ const db = conn.getDB("test");
+ const coll = db.coll;
+ coll.drop();
+
+ // Create a changeStream collection
+ let cst = new ChangeStreamTest(db);
+ cst.startWatchingChanges({
+ pipeline: [{$changeStream: {}}],
+ collection: coll,
+ });
+ cst.cleanUp();
+
+ const queryStats = getQueryStatsAggCmd(db);
+ assert.eq(1, queryStats.length);
+ assert.eq(coll.getName(), queryStats[0].key.queryShape.cmdNs.coll);
+
+ // TODO SERVER-76263 Support reporting 'collectionType' on a sharded cluster.
+ if (!FixtureHelpers.isMongos(db)) {
+ assert.eq("changeStream", queryStats[0].key.collectionType);
+ }
+}
+
+{
+ // Test the non-sharded case.
+ const rst = new ReplSetTest({nodes: 2});
+ rst.startSet({setParameter: {internalQueryStatsRateLimit: -1}});
+ rst.initiate();
+ runTest(rst.getPrimary());
+ rst.stopSet();
+}
+
+{
+ // Test on a sharded cluster.
+ const st = new ShardingTest({
+ mongos: 1,
+ shards: 1,
+ config: 1,
+ rs: {nodes: 1},
+ mongosOptions: {
+ setParameter: {
+ internalQueryStatsRateLimit: -1,
+ }
+ },
+ });
+ runTest(st.s);
+ st.stop();
+}
+}());
diff --git a/jstests/noPassthrough/queryStats/query_stats_collect_on_mongos.js b/jstests/noPassthrough/queryStats/query_stats_collect_on_mongos.js
deleted file mode 100644
index 4a44fa2afc1..00000000000
--- a/jstests/noPassthrough/queryStats/query_stats_collect_on_mongos.js
+++ /dev/null
@@ -1,307 +0,0 @@
-/**
- * Test that mongos is collecting query stats metrics.
- * @tags: [featureFlagQueryStats]
- */
-
-load('jstests/libs/query_stats_utils.js');
-
-(function() {
-"use strict";
-
-const setup = () => {
- const st = new ShardingTest({
- mongos: 1,
- shards: 1,
- config: 1,
- rs: {nodes: 1},
- mongosOptions: {
- setParameter: {
- internalQueryStatsRateLimit: -1,
- }
- },
- });
- const mongos = st.s;
- const db = mongos.getDB("test");
- const coll = db.coll;
- coll.insert({v: 1});
- coll.insert({v: 4});
- return st;
-};
-
-const assertExpectedResults = (results,
- expectedQueryStatsKey,
- expectedExecCount,
- expectedDocsReturnedSum,
- expectedDocsReturnedMax,
- expectedDocsReturnedMin,
- expectedDocsReturnedSumOfSq,
- getMores) => {
- const {key, metrics} = results;
- confirmAllExpectedFieldsPresent(expectedQueryStatsKey, key);
- assert.eq(expectedExecCount, metrics.execCount);
- assert.docEq({
- sum: NumberLong(expectedDocsReturnedSum),
- max: NumberLong(expectedDocsReturnedMax),
- min: NumberLong(expectedDocsReturnedMin),
- sumOfSquares: NumberLong(expectedDocsReturnedSumOfSq)
- },
- metrics.docsReturned);
-
- const {
- firstSeenTimestamp,
- latestSeenTimestamp,
- lastExecutionMicros,
- totalExecMicros,
- firstResponseExecMicros
- } = metrics;
-
- // This test can't predict exact timings, so just assert these three fields have been set (are
- // non-zero).
- assert.neq(lastExecutionMicros, NumberLong(0));
- assert.neq(firstSeenTimestamp.getTime(), 0);
- assert.neq(latestSeenTimestamp.getTime(), 0);
-
- const distributionFields = ['sum', 'max', 'min', 'sumOfSquares'];
- for (const field of distributionFields) {
- assert.neq(totalExecMicros[field], NumberLong(0));
- assert.neq(firstResponseExecMicros[field], NumberLong(0));
- if (getMores) {
- // If there are getMore calls, totalExecMicros fields should be greater than or equal to
- // firstResponseExecMicros.
- if (field == 'min' || field == 'max') {
- // In the case that we've executed multiple queries with the same shape, it is
- // possible for the min or max to be equal.
- assert.gte(totalExecMicros[field], firstResponseExecMicros[field]);
- } else {
- assert.gt(totalExecMicros[field], firstResponseExecMicros[field]);
- }
- } else {
- // If there are no getMore calls, totalExecMicros fields should be equal to
- // firstResponseExecMicros.
- assert.eq(totalExecMicros[field], firstResponseExecMicros[field]);
- }
- }
-};
-
-// Assert that, for find queries, no query stats results are written until a cursor has reached
-// exhaustion; ensure accurate results once they're written.
-{
- const st = setup();
- const db = st.s.getDB("test");
- const collName = "coll";
- const coll = db[collName];
-
- const queryStatsKey = {
- queryShape: {
- cmdNs: {db: "test", coll: "coll"},
- command: "find",
- filter: {$and: [{v: {$gt: "?number"}}, {v: {$lt: "?number"}}]},
- },
- readConcern: {level: "local", provenance: "implicitDefault"},
- batchSize: "?number",
- client: {application: {name: "MongoDB Shell"}}
- };
-
- const cursor = coll.find({v: {$gt: 0, $lt: 5}}).batchSize(1); // returns 1 doc
-
- // Since the cursor hasn't been exhausted yet, ensure no query stats results have been written
- // yet.
- let queryStats = getQueryStats(db);
- assert.eq(0, queryStats.length, queryStats);
-
- // Run a getMore to exhaust the cursor, then ensure query stats results have been written
- // accurately. batchSize must be 2 so the cursor recognizes exhaustion.
- assert.commandWorked(db.runCommand({
- getMore: cursor.getId(),
- collection: coll.getName(),
- batchSize: 2
- })); // returns 1 doc, exhausts the cursor
- queryStats = getQueryStatsFindCmd(db);
- assert.eq(1, queryStats.length, queryStats);
- assertExpectedResults(queryStats[0],
- queryStatsKey,
- /* expectedExecCount */ 1,
- /* expectedDocsReturnedSum */ 2,
- /* expectedDocsReturnedMax */ 2,
- /* expectedDocsReturnedMin */ 2,
- /* expectedDocsReturnedSumOfSq */ 4,
- /* getMores */ true);
-
- // Run more queries (to exhaustion) with the same query shape, and ensure query stats results
- // are accurate.
- coll.find({v: {$gt: 2, $lt: 3}}).batchSize(10).toArray(); // returns 0 docs
- coll.find({v: {$gt: 0, $lt: 1}}).batchSize(10).toArray(); // returns 0 docs
- coll.find({v: {$gt: 0, $lt: 2}}).batchSize(10).toArray(); // return 1 doc
- queryStats = getQueryStatsFindCmd(db);
- assert.eq(1, queryStats.length, queryStats);
- assertExpectedResults(queryStats[0],
- queryStatsKey,
- /* expectedExecCount */ 4,
- /* expectedDocsReturnedSum */ 3,
- /* expectedDocsReturnedMax */ 2,
- /* expectedDocsReturnedMin */ 0,
- /* expectedDocsReturnedSumOfSq */ 5,
- /* getMores */ true);
-
- st.stop();
-}
-
-// Assert that, for agg queries, no query stats results are written until a cursor has reached
-// exhaustion; ensure accurate results once they're written.
-{
- const st = setup();
- const db = st.s.getDB("test");
- const coll = db.coll;
-
- const queryStatsKey = {
- queryShape: {
- cmdNs: {db: "test", coll: "coll"},
- command: "aggregate",
- pipeline: [
- {$match: {$and: [{v: {$gt: "?number"}}, {v: {$lt: "?number"}}]}},
- {$project: {_id: true, hello: true}}
- ]
-
- },
- cursor: {batchSize: "?number"},
- applicationName: "MongoDB Shell",
- };
-
- const cursor = coll.aggregate(
- [
- {$match: {v: {$gt: 0, $lt: 5}}},
- {$project: {hello: true}},
- ],
- {cursor: {batchSize: 1}}); // returns 1 doc
-
- // Since the cursor hasn't been exhausted yet, ensure no query stats results have been written
- // yet.
- let queryStats = getQueryStats(db);
- assert.eq(0, queryStats.length, queryStats);
-
- // Run a getMore to exhaust the cursor, then ensure query stats results have been written
- // accurately. batchSize must be 2 so the cursor recognizes exhaustion.
- assert.commandWorked(db.runCommand({
- getMore: cursor.getId(),
- collection: coll.getName(),
- batchSize: 2
- })); // returns 1 doc, exhausts the cursor
- queryStats = getQueryStatsAggCmd(db);
- assert.eq(1, queryStats.length, queryStats);
- assertExpectedResults(queryStats[0],
- queryStatsKey,
- /* expectedExecCount */ 1,
- /* expectedDocsReturnedSum */ 2,
- /* expectedDocsReturnedMax */ 2,
- /* expectedDocsReturnedMin */ 2,
- /* expectedDocsReturnedSumOfSq */ 4,
- /* getMores */ true);
-
- // Run more queries (to exhaustion) with the same query shape, and ensure query stats results
- // are accurate.
- coll.aggregate([
- {$match: {v: {$gt: 0, $lt: 5}}},
- {$project: {hello: true}},
- ]); // returns 2 docs
- coll.aggregate([
- {$match: {v: {$gt: 2, $lt: 3}}},
- {$project: {hello: true}},
- ]); // returns 0 docs
- coll.aggregate([
- {$match: {v: {$gt: 0, $lt: 2}}},
- {$project: {hello: true}},
- ]); // returns 1 doc
- queryStats = getQueryStatsAggCmd(db);
- assert.eq(1, queryStats.length, queryStats);
- assertExpectedResults(queryStats[0],
- queryStatsKey,
- /* expectedExecCount */ 4,
- /* expectedDocsReturnedSum */ 5,
- /* expectedDocsReturnedMax */ 2,
- /* expectedDocsReturnedMin */ 0,
- /* expectedDocsReturnedSumOfSq */ 9,
- /* getMores */ true);
-
- st.stop();
-}
-
-// Assert on batchSize-limited find queries that killCursors will write metrics with partial results
-// to the query stats store.
-{
- const st = setup();
- const db = st.s.getDB("test");
- const collName = "coll";
- const coll = db[collName];
-
- const queryStatsKey = {
- queryShape: {
- cmdNs: {db: "test", coll: "coll"},
- command: "find",
- filter: {$and: [{v: {$gt: "?number"}}, {v: {$lt: "?number"}}]},
- },
- readConcern: {level: "local", provenance: "implicitDefault"},
- batchSize: "?number",
- client: {application: {name: "MongoDB Shell"}}
- };
-
- const cursor1 = coll.find({v: {$gt: 0, $lt: 5}}).batchSize(1); // returns 1 doc
- const cursor2 = coll.find({v: {$gt: 0, $lt: 2}}).batchSize(1); // returns 1 doc
-
- assert.commandWorked(
- db.runCommand({killCursors: coll.getName(), cursors: [cursor1.getId(), cursor2.getId()]}));
- const queryStats = getQueryStats(db);
- assert.eq(1, queryStats.length);
- assertExpectedResults(queryStats[0],
- queryStatsKey,
- /* expectedExecCount */ 2,
- /* expectedDocsReturnedSum */ 2,
- /* expectedDocsReturnedMax */ 1,
- /* expectedDocsReturnedMin */ 1,
- /* expectedDocsReturnedSumOfSq */ 2,
- /* getMores */ false);
- st.stop();
-}
-
-// Assert on batchSize-limited agg queries that killCursors will write metrics with partial results
-// to the query stats store.
-{
- const st = setup();
- const db = st.s.getDB("test");
- const coll = db.coll;
-
- const queryStatsKey = {
- queryShape: {
- cmdNs: {db: "test", coll: "coll"},
- command: "aggregate",
- pipeline: [{$match: {$and: [{v: {$gt: "?number"}}, {v: {$lt: "?number"}}]}}]
- },
- cursor: {batchSize: "?number"},
- applicationName: "MongoDB Shell",
- };
-
- const cursor1 = coll.aggregate(
- [
- {$match: {v: {$gt: 0, $lt: 5}}},
- ],
- {cursor: {batchSize: 1}}); // returns 1 doc
- const cursor2 = coll.aggregate(
- [
- {$match: {v: {$gt: 0, $lt: 2}}},
- ],
- {cursor: {batchSize: 1}}); // returns 1 doc
-
- assert.commandWorked(
- db.runCommand({killCursors: coll.getName(), cursors: [cursor1.getId(), cursor2.getId()]}));
- const queryStats = getQueryStats(db);
- assert.eq(1, queryStats.length);
- assertExpectedResults(queryStats[0],
- queryStatsKey,
- /* expectedExecCount */ 2,
- /* expectedDocsReturnedSum */ 2,
- /* expectedDocsReturnedMax */ 1,
- /* expectedDocsReturnedMin */ 1,
- /* expectedDocsReturnedSumOfSq */ 2,
- /* getMores */ false);
- st.stop();
-}
-}());
diff --git a/jstests/noPassthrough/queryStats/query_stats_collectionType.js b/jstests/noPassthrough/queryStats/query_stats_collectionType.js
index f86e6d11022..39e2ee944b3 100644
--- a/jstests/noPassthrough/queryStats/query_stats_collectionType.js
+++ b/jstests/noPassthrough/queryStats/query_stats_collectionType.js
@@ -1,5 +1,7 @@
/**
- * Test that collectionType is returned properly in $queryStats.
+ * Test that collectionType is returned properly in $queryStats. Checks for collection types
+ * "collection", "view", "timeseries", "nonExistent", and "virtual". Type "changeStream" is covered
+ * in query_stats_changeStreams.js.
* @tags: [featureFlagQueryStats]
*/
load("jstests/libs/query_stats_utils.js");
@@ -38,15 +40,16 @@ function runTest(conn) {
coll.insert({v: 3, time: ISODate("2021-05-18T02:00:00.000Z")});
coll.find({v: 6}).toArray();
coll.aggregate().toArray();
+
// QueryStats should still be collected for queries run on nonexistent collections.
assert.commandWorked(testDB.runCommand({find: jsTestName() + "_nonExistent", filter: {v: 6}}));
assert.commandWorked(
testDB.runCommand({aggregate: jsTestName() + "_nonExistent", pipeline: [], cursor: {}}));
- // Verify that we have two telemetry entries for the collection type. This assumes we have
+ // Verify that we have two query stats entries for the collection type. This assumes we have
// executed one find and one agg query for the given collection type.
- function verifyTelemetryForCollectionType(collectionType) {
- const telemetry = getQueryStats(conn, {
+ function verifyQueryStatsForCollectionType(collectionType) {
+ const queryStats = getQueryStats(conn, {
extraMatch: {
"key.collectionType": collectionType,
"key.queryShape.cmdNs.coll": jsTestName() + "_" + collectionType
@@ -57,13 +60,27 @@ function runTest(conn) {
// that find() queries over views are rewritten to
// aggregate(). Ie, the query shapes are different because the
// queries are different.
- assert.eq(2, telemetry.length, "Expected result for collection type " + collectionType);
+ assert.eq(2, queryStats.length, "Expected result for collection type " + collectionType);
}
- verifyTelemetryForCollectionType("collection");
- verifyTelemetryForCollectionType("view");
- verifyTelemetryForCollectionType("timeseries");
- verifyTelemetryForCollectionType("nonExistent");
+ verifyQueryStatsForCollectionType("collection");
+ verifyQueryStatsForCollectionType("view");
+ verifyQueryStatsForCollectionType("timeseries");
+ verifyQueryStatsForCollectionType("nonExistent");
+
+ // Run commands that should be tracked as "virtual" collection type.
+ assert.commandWorked(
+ testDB.adminCommand({aggregate: 1, pipeline: [{$currentOp: {}}], cursor: {}}));
+ assert.commandWorked(testDB.adminCommand(
+ {aggregate: 1, pipeline: [{$documents: [{a: 1}, {a: 4}]}], cursor: {}}));
+ assert.commandWorked(
+ testDB.adminCommand({aggregate: 1, pipeline: [{$listLocalSessions: {}}], cursor: {}}));
+
+ // Verify the queries on "virtual" collection types were tracked appropriately. This includes
+ // the 3 queries directly run above, in addition to 1 entry for the $queryStats aggregations
+ // run for the test.
+ let queryStats = getQueryStats(conn, {extraMatch: {"key.collectionType": "virtual"}});
+ assert.eq(4, queryStats.length);
// Verify that, for views, we capture the original query before it's rewritten. The view would
// include a $gt predicate on 'v'.
@@ -101,7 +118,6 @@ if (false) {
setParameter: {
internalQueryStatsSamplingRate: -1,
featureFlagQueryStats: true,
- 'failpoint.skipClusterParameterRefresh': "{'mode':'alwaysOn'}"
}
},
});
diff --git a/jstests/noPassthrough/queryStats/query_stats_disable_after_initial_request.js b/jstests/noPassthrough/queryStats/query_stats_disable_after_initial_request.js
index 08e87be016e..68580dda93a 100644
--- a/jstests/noPassthrough/queryStats/query_stats_disable_after_initial_request.js
+++ b/jstests/noPassthrough/queryStats/query_stats_disable_after_initial_request.js
@@ -4,6 +4,8 @@
* @tags: [featureFlagQueryStats]
*/
load("jstests/libs/query_stats_utils.js"); // For getQueryStatsFindCmd.
+(function() {
+"use strict";
// Test that no QueryStats entry is written when (1) dispatching an initial find query, (2)
// disabling QueryStats, then (3) completing the command. Below, we run variations of this test
@@ -66,4 +68,5 @@ testStatsAreNotCollectedWhenDisabledBeforeCommandCompletion({
enableQueryStatsFn: () => setQueryStatsCacheSize("10MB")
});
-MongoRunner.stopMongod(conn); \ No newline at end of file
+MongoRunner.stopMongod(conn);
+}()); \ No newline at end of file
diff --git a/jstests/noPassthrough/queryStats/query_stats_expressions.js b/jstests/noPassthrough/queryStats/query_stats_expressions.js
new file mode 100644
index 00000000000..fe7f9bc4ebb
--- /dev/null
+++ b/jstests/noPassthrough/queryStats/query_stats_expressions.js
@@ -0,0 +1,76 @@
+/**
+ * Test that queryStats works properly for a find command that uses agg expressions and produces the
+ * proper query shape without issues during re-parsing.
+ * @tags: [featureFlagQueryStatsFindCommand]
+ */
+load("jstests/libs/query_stats_utils.js"); // For getQueryStats and resetQueryStatsStore.
+
+(function() {
+"use strict";
+
+// Turn on the collecting of telemetry metrics.
+let options = {
+ setParameter: {internalQueryStatsRateLimit: -1},
+};
+
+const conn = MongoRunner.runMongod(options);
+const testDB = conn.getDB('test');
+var coll = testDB[jsTestName()];
+coll.drop();
+
+const bulk = coll.initializeUnorderedBulkOp();
+const numDocs = 100;
+for (let i = 0; i < numDocs / 2; ++i) {
+ bulk.insert({foo: i, bar: i, applyDiscount: true, word: "asdf"});
+ bulk.insert({foo: i, bar: i, applyDiscount: false, word: "ghjk"});
+}
+assert.commandWorked(bulk.execute());
+coll.createIndex({foo: 1});
+
+// Tests that $meta is re-parsed correctly by ensuring the metaDataKeyword is not serialized as
+// string literal.
+{
+ resetQueryStatsStore(conn, "1MB");
+ coll.find({$expr: {idxKey: {$meta: "indexKey"}}}).itcount();
+ let queryStats = getQueryStats(testDB);
+ assert.eq({"$expr": {"idxKey": {"$meta": "indexKey"}}}, queryStats[0].key.queryShape.filter);
+}
+
+// Tests that $regexMatch is re-parsed correctly. The parser does not check that regex is a valid
+// regex pattern, so regular serialization is okay.
+{
+ resetQueryStatsStore(conn, "1MB");
+ coll.find({$expr: {result: {$regexMatch: {input: "$word", regex: "^a"}}}}).itcount();
+ let queryStats = getQueryStats(testDB);
+ assert.eq({"$expr": {"result": {"$regexMatch": {"input": "$word", "regex": "?string"}}}},
+ queryStats[0].key.queryShape.filter);
+}
+
+// Tests that $let is re-parsed correctly, by ensuring the variables are not serialized as string
+// literals.
+{
+ resetQueryStatsStore(conn, "1MB");
+ coll.find({$expr: {$let: {
+ vars: {
+ total: { $add: [ '$foo', '$bar' ] },
+ discounted: { $cond: { if: '$applyDiscount', then: 0.9, else: 1 } }
+ },
+ in: { $multiply: [ "$$total", "$$discounted" ] }
+ }}}).itcount();
+ let queryStats = getQueryStats(testDB);
+ assert.eq({
+ "$expr": {
+ "$let": {
+ "vars": {
+ "total": {"$add": ["$foo", "$bar"]},
+ "discounted": {"$cond": ["$applyDiscount", "?number", "?number"]}
+ },
+ "in": {"$multiply": ["$$total", "$$discounted"]}
+ }
+ }
+ },
+ queryStats[0].key.queryShape.filter);
+}
+
+MongoRunner.stopMongod(conn);
+}());
diff --git a/jstests/noPassthrough/queryStats/query_stats_feature_flag.js b/jstests/noPassthrough/queryStats/query_stats_feature_flag.js
index bcce489d8da..c71ee1bdb93 100644
--- a/jstests/noPassthrough/queryStats/query_stats_feature_flag.js
+++ b/jstests/noPassthrough/queryStats/query_stats_feature_flag.js
@@ -1,5 +1,5 @@
/**
- * Test that calls to read from telemetry store fail when feature flag is turned off.
+ * Test that calls to read from query stats store fail when feature flag is turned off.
*/
load('jstests/libs/analyze_plan.js');
load("jstests/libs/feature_flag_util.js");
@@ -9,9 +9,11 @@ load("jstests/libs/feature_flag_util.js");
// This test specifically tests error handling when the feature flag is not on.
// TODO SERVER-65800 this test can be removed when the feature flag is removed.
+// TODO SERVER-79494 remove reference to featureFlagQueryStatsFindCommand.
const conn = MongoRunner.runMongod();
const testDB = conn.getDB('test');
-if (FeatureFlagUtil.isEnabled(testDB, "QueryStats")) {
+if (FeatureFlagUtil.isEnabled(testDB, "QueryStats") ||
+ FeatureFlagUtil.isEnabled(testDB, "QueryStatsFindCommand")) {
jsTestLog("Skipping test since query stats are enabled.");
MongoRunner.stopMongod(conn);
return;
diff --git a/jstests/noPassthrough/queryStats/query_stats_feature_flags_small_off_big_on.js b/jstests/noPassthrough/queryStats/query_stats_feature_flags_small_off_big_on.js
new file mode 100644
index 00000000000..d5947581b48
--- /dev/null
+++ b/jstests/noPassthrough/queryStats/query_stats_feature_flags_small_off_big_on.js
@@ -0,0 +1,63 @@
+/**
+ * Test that query stats are collected properly when featureFlagQueryStatsFindCommand is disabled
+ * but featureFlagQueryStats is enabled. Metrics should be collected for find and agg queries.
+ * TODO SERVER-79494 remove this test once featureFlagQueryStatsFindCommand is removed.
+ */
+load("jstests/libs/query_stats_utils.js");
+load("jstests/libs/feature_flag_util.js"); // For 'FeatureFlagUtil'
+
+(function() {
+"use strict";
+
+function runTest(conn) {
+ const testDB = conn.getDB('test');
+ let coll = testDB[jsTestName()];
+ coll.drop();
+
+ coll.insert({x: 0});
+ coll.insert({x: 1});
+ coll.insert({x: 2});
+
+ let res = coll.aggregate([{$match: {x: 2}}]).toArray();
+ assert.eq(1, res.length, res);
+
+ let queryStats = getQueryStats(conn);
+ // Ensure metrics were collected for the agg query.
+ assert.eq(1, queryStats.length, queryStats);
+
+ res = coll.find({x: 2}).toArray();
+ assert.eq(1, res.length, res);
+ queryStats = getQueryStats(conn);
+ // Ensure we collected metrics for the find query, agg query, and additional agg query from
+ // calling $queryStats.
+ assert.eq(3, queryStats.length, queryStats);
+ verifyMetrics(queryStats);
+}
+
+const conn = MongoRunner.runMongod({
+ setParameter: {internalQueryStatsRateLimit: -1, featureFlagQueryStats: true},
+});
+const testDB = conn.getDB('test');
+if (FeatureFlagUtil.isEnabled(testDB, "QueryStatsFindCommand")) {
+ jsTestLog("Skipping test since featureFlagQueryStatsFindCommand is on.");
+ MongoRunner.stopMongod(conn);
+ quit();
+}
+runTest(conn);
+MongoRunner.stopMongod(conn);
+
+const st = new ShardingTest({
+ mongos: 1,
+ shards: 1,
+ config: 1,
+ rs: {nodes: 1},
+ mongosOptions: {
+ setParameter: {
+ internalQueryStatsRateLimit: -1,
+ featureFlagQueryStats: true,
+ }
+ }
+});
+runTest(st.s);
+st.stop();
+}()); \ No newline at end of file
diff --git a/jstests/noPassthrough/queryStats/query_stats_feature_flags_small_on_big_off.js b/jstests/noPassthrough/queryStats/query_stats_feature_flags_small_on_big_off.js
new file mode 100644
index 00000000000..a0eaa6a3a94
--- /dev/null
+++ b/jstests/noPassthrough/queryStats/query_stats_feature_flags_small_on_big_off.js
@@ -0,0 +1,66 @@
+/**
+ * Test that query stats are collected properly when featureFlagQueryStatsFindCommand is enabled but
+ * featureFlagQueryStats is disabled. Metrics for find commands should be collected, but metrics for
+ * aggregate commmands should not be collected.
+ * TODO SERVER-79494 remove this test once featureFlagQueryStatsFindCommand is removed.
+ */
+load("jstests/libs/query_stats_utils.js");
+load("jstests/libs/feature_flag_util.js"); // For 'FeatureFlagUtil'
+
+(function() {
+"use strict";
+
+function runTest(conn) {
+ const testDB = conn.getDB('test');
+ let coll = testDB[jsTestName()];
+ coll.drop();
+
+ coll.insert({x: 0});
+ coll.insert({x: 1});
+ coll.insert({x: 2});
+
+ let res = coll.aggregate([{$match: {x: 2}}]).toArray();
+ assert.eq(1, res.length, res);
+
+ let queryStats = getQueryStats(conn);
+ // Ensure no metrics have been colleted so far.
+ assert.eq(0, queryStats.length, queryStats);
+
+ res = coll.find({x: 2}).toArray();
+ assert.eq(1, res.length, res);
+ queryStats = getQueryStats(conn);
+ // Ensure we collected metrics for the find command.
+ assert.eq(1, queryStats.length, queryStats);
+ const entry = queryStats[0];
+ assert.eq("find", entry.key.queryShape.command);
+ assert.eq({"x": {$eq: "?number"}}, entry.key.queryShape.filter);
+ verifyMetrics(queryStats);
+}
+
+const conn = MongoRunner.runMongod({
+ setParameter: {internalQueryStatsRateLimit: -1, featureFlagQueryStatsFindCommand: true},
+});
+const testDB = conn.getDB('test');
+if (FeatureFlagUtil.isEnabled(testDB, "QueryStats")) {
+ jsTestLog("Skipping test since full query stats are enabled.");
+ MongoRunner.stopMongod(conn);
+ quit();
+}
+runTest(conn);
+MongoRunner.stopMongod(conn);
+
+const st = new ShardingTest({
+ mongos: 1,
+ shards: 1,
+ config: 1,
+ rs: {nodes: 1},
+ mongosOptions: {
+ setParameter: {
+ internalQueryStatsRateLimit: -1,
+ featureFlagQueryStatsFindCommand: true,
+ }
+ }
+});
+runTest(st.s);
+st.stop();
+}()); \ No newline at end of file
diff --git a/jstests/noPassthrough/queryStats/query_stats_find_cmd_collect_on_mongos.js b/jstests/noPassthrough/queryStats/query_stats_find_cmd_collect_on_mongos.js
new file mode 100644
index 00000000000..e3afdda143b
--- /dev/null
+++ b/jstests/noPassthrough/queryStats/query_stats_find_cmd_collect_on_mongos.js
@@ -0,0 +1,130 @@
+/**
+ * Test that mongos is collecting query stats metrics for find queries.
+ * @tags: [featureFlagQueryStatsFindCommand]
+ */
+
+load('jstests/libs/query_stats_utils.js');
+
+(function() {
+"use strict";
+
+const setup = () => {
+ const st = new ShardingTest({
+ mongos: 1,
+ shards: 1,
+ config: 1,
+ rs: {nodes: 1},
+ mongosOptions: {
+ setParameter: {
+ internalQueryStatsRateLimit: -1,
+ }
+ },
+ });
+ const mongos = st.s;
+ const db = mongos.getDB("test");
+ const coll = db.coll;
+ coll.insert({v: 1});
+ coll.insert({v: 4});
+ return st;
+};
+
+// Assert that, for find queries, no query stats results are written until a cursor has reached
+// exhaustion; ensure accurate results once they're written.
+{
+ const st = setup();
+ const db = st.s.getDB("test");
+ const collName = "coll";
+ const coll = db[collName];
+
+ const queryStatsKey = {
+ queryShape: {
+ cmdNs: {db: "test", coll: "coll"},
+ command: "find",
+ filter: {$and: [{v: {$gt: "?number"}}, {v: {$lt: "?number"}}]},
+ },
+ readConcern: {level: "local", provenance: "implicitDefault"},
+ batchSize: "?number",
+ client: {application: {name: "MongoDB Shell"}}
+ };
+
+ const cursor = coll.find({v: {$gt: 0, $lt: 5}}).batchSize(1); // returns 1 doc
+
+ // Since the cursor hasn't been exhausted yet, ensure no query stats results have been written
+ // yet.
+ let queryStats = getQueryStats(db);
+ assert.eq(0, queryStats.length, queryStats);
+
+ // Run a getMore to exhaust the cursor, then ensure query stats results have been written
+ // accurately. batchSize must be 2 so the cursor recognizes exhaustion.
+ assert.commandWorked(db.runCommand({
+ getMore: cursor.getId(),
+ collection: coll.getName(),
+ batchSize: 2
+ })); // returns 1 doc, exhausts the cursor
+ queryStats = getQueryStatsFindCmd(db);
+ assert.eq(1, queryStats.length, queryStats);
+ assertExpectedResults(queryStats[0],
+ queryStatsKey,
+ /* expectedExecCount */ 1,
+ /* expectedDocsReturnedSum */ 2,
+ /* expectedDocsReturnedMax */ 2,
+ /* expectedDocsReturnedMin */ 2,
+ /* expectedDocsReturnedSumOfSq */ 4,
+ /* getMores */ true);
+
+ // Run more queries (to exhaustion) with the same query shape, and ensure query stats results
+ // are accurate.
+ coll.find({v: {$gt: 2, $lt: 3}}).batchSize(10).toArray(); // returns 0 docs
+ coll.find({v: {$gt: 0, $lt: 1}}).batchSize(10).toArray(); // returns 0 docs
+ coll.find({v: {$gt: 0, $lt: 2}}).batchSize(10).toArray(); // return 1 doc
+ queryStats = getQueryStatsFindCmd(db);
+ assert.eq(1, queryStats.length, queryStats);
+ assertExpectedResults(queryStats[0],
+ queryStatsKey,
+ /* expectedExecCount */ 4,
+ /* expectedDocsReturnedSum */ 3,
+ /* expectedDocsReturnedMax */ 2,
+ /* expectedDocsReturnedMin */ 0,
+ /* expectedDocsReturnedSumOfSq */ 5,
+ /* getMores */ true);
+
+ st.stop();
+}
+
+// Assert on batchSize-limited find queries that killCursors will write metrics with partial results
+// to the query stats store.
+{
+ const st = setup();
+ const db = st.s.getDB("test");
+ const collName = "coll";
+ const coll = db[collName];
+
+ const queryStatsKey = {
+ queryShape: {
+ cmdNs: {db: "test", coll: "coll"},
+ command: "find",
+ filter: {$and: [{v: {$gt: "?number"}}, {v: {$lt: "?number"}}]},
+ },
+ readConcern: {level: "local", provenance: "implicitDefault"},
+ batchSize: "?number",
+ client: {application: {name: "MongoDB Shell"}}
+ };
+
+ const cursor1 = coll.find({v: {$gt: 0, $lt: 5}}).batchSize(1); // returns 1 doc
+ const cursor2 = coll.find({v: {$gt: 0, $lt: 2}}).batchSize(1); // returns 1 doc
+
+ assert.commandWorked(
+ db.runCommand({killCursors: coll.getName(), cursors: [cursor1.getId(), cursor2.getId()]}));
+ const queryStats = getQueryStats(db);
+ assert.eq(1, queryStats.length);
+ assertExpectedResults(queryStats[0],
+ queryStatsKey,
+ /* expectedExecCount */ 2,
+ /* expectedDocsReturnedSum */ 2,
+ /* expectedDocsReturnedMax */ 1,
+ /* expectedDocsReturnedMin */ 1,
+ /* expectedDocsReturnedSumOfSq */ 2,
+ /* getMores */ false);
+ st.stop();
+}
+}()); \ No newline at end of file
diff --git a/jstests/noPassthrough/queryStats/query_stats_key.js b/jstests/noPassthrough/queryStats/query_stats_key.js
index f4905e00e0b..69264b6194f 100644
--- a/jstests/noPassthrough/queryStats/query_stats_key.js
+++ b/jstests/noPassthrough/queryStats/query_stats_key.js
@@ -1,6 +1,6 @@
/**
- * This test confirms that telemetry store key fields are properly nested and none are missing.
- * @tags: [featureFlagQueryStats]
+ * This test confirms that query stats store key fields are properly nested and none are missing.
+ * @tags: [featureFlagQueryStatsFindCommand]
*/
load("jstests/libs/query_stats_utils.js");
(function() {
@@ -32,7 +32,6 @@ function confirmAllFieldsPresent(queryStatsEntries) {
"filter",
"sort",
"projection",
- "hint",
"skip",
"limit",
"singleBatch",
@@ -61,7 +60,8 @@ function confirmAllFieldsPresent(queryStatsEntries) {
"apiVersion",
"apiStrict",
"collectionType",
- "client"
+ "client",
+ "hint",
];
for (const entry of queryStatsEntries) {
@@ -128,9 +128,9 @@ let commandObj = {
};
assert.commandWorked(testDB.runCommand(commandObj));
-let telemetry = getQueryStats(conn);
-assert.eq(1, telemetry.length);
-confirmAllFieldsPresent(telemetry);
+let stats = getQueryStats(conn);
+assert.eq(1, stats.length);
+confirmAllFieldsPresent(stats);
// $hint can only be string(index name) or object (index spec).
assert.throwsWithCode(() => {
diff --git a/jstests/noPassthrough/queryStats/query_stats_logging.js b/jstests/noPassthrough/queryStats/query_stats_logging.js
new file mode 100644
index 00000000000..cbc0e161142
--- /dev/null
+++ b/jstests/noPassthrough/queryStats/query_stats_logging.js
@@ -0,0 +1,99 @@
+/**
+ * Test logging of $queryStats.
+ * @tags: [featureFlagQueryStats]
+ */
+
+(function() {
+"use strict";
+let options = {
+ setParameter: {internalQueryStatsRateLimit: -1},
+};
+
+function countMatching(arr, func) {
+ let count = 0;
+ let i = arr.length;
+ while (i--) {
+ if (func(arr[i])) {
+ count += 1;
+ }
+ }
+ return count;
+}
+
+function runQueryStatsAndVerifyLogs({pipeline, transformed, logLevel}) {
+ const conn = MongoRunner.runMongod(options);
+ const db = conn.getDB('test');
+ var coll = db[jsTestName()];
+ coll.drop();
+
+ // Set the logLevel.
+ assert.commandWorked(db.setLogLevel(logLevel, "queryStats"));
+
+ // Insert a document and run two find command with different shapes so that we have two entries
+ // in the queryStats store.
+ assert.commandWorked(coll.insert({foo: 1, bar: "hello"}));
+ assert.eq(coll.find({foo: 1}).itcount(), 1);
+ assert.eq(coll.find({bar: "hello"}).itcount(), 1);
+
+ // Run $queryStats with the specified pipeline.
+ let result = conn.adminCommand({aggregate: 1, pipeline: [pipeline], cursor: {}});
+ assert.commandWorked(result);
+
+ if (logLevel >= 1) {
+ // Checking that we log the invocation of $queryStats.
+ let spec = transformed
+ ? {"transformIdentifiers": {"algorithm": "hmac-sha-256", "hmacKey": "###"}}
+ : {};
+ assert(checkLog.checkContainsWithCountJson(
+ conn, 7808300, {"commandSpec": spec}, 1, null, true),
+ "failed to find log with id " + 7808300);
+ }
+
+ // We only log the output of $queryStats when invoked with transformation, and log level at
+ // least 3.
+ if (logLevel >= 3 && transformed) {
+ // Checking that we are logging both entries in the query stats store.
+ assert(checkLog.checkContainsWithCountJson(conn, 7808301, {}, 2, null, true),
+ "failed to find log with id " + 7808301);
+
+ // Checking that the output is what we expect.
+ const query = assert.commandWorked(db.adminCommand({getLog: "global"}));
+ assert(query.hasOwnProperty("log"), "no log field");
+ assert.eq(countMatching(query.log, function(v) {
+ const has = (s) => v.indexOf(s) !== -1;
+ return has("thisOutput") && has("key:") && has("queryShape") &&
+ (has("?number") || has("?string"));
+ }), 2);
+
+ // Checking that we log when we are done outputting the results of $queryStats.
+ assert(checkLog.checkContainsWithCountJson(conn, 7808302, {}, 1, null, true),
+ "failed to find log with id " + 7808302);
+ }
+
+ // At a log level of 0 we should not be seeing any of these logs.
+ if (logLevel == 0) {
+ assert(checkLog.checkContainsWithCountJson(conn, 7808300, {}, 0, null, true));
+ assert(checkLog.checkContainsWithCountJson(conn, 7808301, {}, 0, null, true));
+ assert(checkLog.checkContainsWithCountJson(conn, 7808302, {}, 0, null, true));
+ }
+
+ MongoRunner.stopMongod(conn);
+}
+
+// Testing logging with transformation.
+const hmacKey = "MjM0NTY3ODkxMDExMTIxMzE0MTUxNjE3MTgxOTIwMjE=";
+let pipeline = {
+ $queryStats: {transformIdentifiers: {algorithm: "hmac-sha-256", hmacKey: BinData(8, hmacKey)}}
+};
+runQueryStatsAndVerifyLogs({pipeline: pipeline, transformed: true, logLevel: 3});
+runQueryStatsAndVerifyLogs({pipeline: pipeline, transformed: true, logLevel: 1});
+runQueryStatsAndVerifyLogs({pipeline: pipeline, transformed: true, logLevel: 0});
+
+// Testing logging without transformation.
+pipeline = {
+ $queryStats: {}
+};
+runQueryStatsAndVerifyLogs({pipeline: pipeline, transformed: false, logLevel: 3});
+runQueryStatsAndVerifyLogs({pipeline: pipeline, transformed: false, logLevel: 1});
+runQueryStatsAndVerifyLogs({pipeline: pipeline, transformed: false, logLevel: 0});
+}());
diff --git a/jstests/noPassthrough/queryStats/query_stats_metrics_across_getMore_calls.js b/jstests/noPassthrough/queryStats/query_stats_metrics_across_getMore_calls.js
index 659d065a6c1..6f0dfe33373 100644
--- a/jstests/noPassthrough/queryStats/query_stats_metrics_across_getMore_calls.js
+++ b/jstests/noPassthrough/queryStats/query_stats_metrics_across_getMore_calls.js
@@ -1,6 +1,6 @@
/**
* Test that the queryStats metrics are aggregated properly by distinct query shape over getMore
- * calls.
+ * calls, for agg commands.
* @tags: [featureFlagQueryStats]
*/
load("jstests/libs/query_stats_utils.js"); // For verifyMetrics and getQueryStatsAggCmd.
diff --git a/jstests/noPassthrough/queryStats/query_stats_regex.js b/jstests/noPassthrough/queryStats/query_stats_regex.js
index b910df94f4e..2a96b3e898e 100644
--- a/jstests/noPassthrough/queryStats/query_stats_regex.js
+++ b/jstests/noPassthrough/queryStats/query_stats_regex.js
@@ -1,6 +1,6 @@
/**
* Test that telemetry works properly for a find command that uses regex.
- * @tags: [featureFlagQueryStats]
+ * @tags: [featureFlagQueryStatsFindCommand]
*/
(function() {
"use strict";
diff --git a/jstests/noPassthrough/queryStats/query_stats_sampling_rate.js b/jstests/noPassthrough/queryStats/query_stats_sampling_rate.js
index 74f2cff3055..35cfe209567 100644
--- a/jstests/noPassthrough/queryStats/query_stats_sampling_rate.js
+++ b/jstests/noPassthrough/queryStats/query_stats_sampling_rate.js
@@ -1,9 +1,10 @@
/**
- * Test that calls to read from telemetry store fail when sampling rate is not greater than 0 even
+ * Test that calls to read from query stats store fail when sampling rate is not greater than 0 even
* if feature flag is on.
- * @tags: [featureFlagQueryStats]
+ * @tags: [featureFlagQueryStatsFindCommand]
*/
load('jstests/libs/analyze_plan.js');
+load("jstests/libs/query_stats_utils.js");
(function() {
"use strict";
@@ -20,19 +21,17 @@ for (var i = 0; i < 20; i++) {
coll.insert({foo: 0, bar: Math.floor(Math.random() * 3)});
}
-coll.aggregate([{$match: {foo: 1}}], {cursor: {batchSize: 2}});
+coll.find({foo: 1}).batchSize(2).toArray();
-// Reading telemetry store with a sampling rate of 0 should return 0 documents.
-let telStore = testdb.adminCommand({aggregate: 1, pipeline: [{$queryStats: {}}], cursor: {}});
-assert.eq(telStore.cursor.firstBatch.length, 0);
+// Reading query stats store with a sampling rate of 0 should return 0 documents.
+let stats = getQueryStats(testdb);
+assert.eq(stats.length, 0);
-// Reading telemetry store should work now with a sampling rate of greater than 0.
-assert.commandWorked(
- testdb.adminCommand({setParameter: 1, internalQueryStatsRateLimit: 2147483647}));
-coll.aggregate([{$match: {foo: 1}}], {cursor: {batchSize: 2}});
-telStore = assert.commandWorked(
- testdb.adminCommand({aggregate: 1, pipeline: [{$queryStats: {}}], cursor: {}}));
-assert.eq(telStore.cursor.firstBatch.length, 1);
+// Reading query stats store should work now with a sampling rate of greater than 0.
+assert.commandWorked(testdb.adminCommand({setParameter: 1, internalQueryStatsRateLimit: -1}));
+coll.find({foo: 1}).batchSize(2).toArray();
+stats = getQueryStats(testdb);
+assert.eq(stats.length, 1);
MongoRunner.stopMongod(conn);
}());
diff --git a/jstests/noPassthrough/queryStats/query_stats_server_status_metrics.js b/jstests/noPassthrough/queryStats/query_stats_server_status_metrics.js
index ff470589b5f..c1070cf6ec5 100644
--- a/jstests/noPassthrough/queryStats/query_stats_server_status_metrics.js
+++ b/jstests/noPassthrough/queryStats/query_stats_server_status_metrics.js
@@ -161,6 +161,47 @@ function telemetryStoreWriteErrorsTest(conn, testDB, coll, testOptions) {
}
/**
+ * Test that running the $queryStats aggregation stage correctly does or does not impact
+ * serverStatus counters (whichever is applicable).
+ */
+function queryStatsAggregationStageTest(conn, testDB, coll) {
+ // First, ensure that the query stats store is empty.
+ assert.eq(testDB.serverStatus().metrics.queryStats.queryStatsStoreSizeEstimateBytes, 0);
+ assert.eq(testDB.serverStatus().metrics.queryStats.numEvicted, 0);
+
+ // Insert some query stats data and capture "before" serverStatus metrics.
+ for (let i = 100; i < 200; i++) {
+ coll.aggregate([{$match: {["foo" + i]: "bar"}}]).itcount();
+ }
+
+ let sizeBefore = testDB.serverStatus().metrics.queryStats.queryStatsStoreSizeEstimateBytes;
+ const evictedBefore = testDB.serverStatus().metrics.queryStats.numEvicted;
+
+ // Run a $queryStats pipeline. We should insert a new entry for this query.
+ assert.commandWorked(
+ testDB.adminCommand({aggregate: 1, pipeline: [{$queryStats: {}}], cursor: {}}));
+
+ assert.eq(testDB.serverStatus().metrics.queryStats.numEvicted,
+ evictedBefore,
+ "$queryStats should not have triggered evictions");
+ assert.gt(testDB.serverStatus().metrics.queryStats.queryStatsStoreSizeEstimateBytes,
+ sizeBefore,
+ "$queryStats pipeline should have been added to the query stats store");
+ sizeBefore = testDB.serverStatus().metrics.queryStats.queryStatsStoreSizeEstimateBytes;
+
+ // Now run $queryStats again. The command should be fully read-only.
+ assert.commandWorked(
+ testDB.adminCommand({aggregate: 1, pipeline: [{$queryStats: {}}], cursor: {}}));
+
+ assert.eq(testDB.serverStatus().metrics.queryStats.numEvicted,
+ evictedBefore,
+ "$queryStats should not have triggered evictions");
+ assert.eq(testDB.serverStatus().metrics.queryStats.queryStatsStoreSizeEstimateBytes,
+ sizeBefore,
+ "$queryStats pipeline should not have impacted query stats store size");
+}
+
+/**
* In this configuration, we insert enough entries into the telemetry store to trigger LRU
* eviction.
*/
@@ -217,4 +258,12 @@ runTestWithMongodOptions({
setParameter: {internalQueryStatsCacheSize: "0.00001MB", internalQueryStatsRateLimit: -1},
},
telemetryStoreWriteErrorsTest);
+
+/**
+ * Tests that $queryStats has expected effects (or no effect) on counters.
+ */
+runTestWithMongodOptions({
+ setParameter: {internalQueryStatsCacheSize: "2MB", internalQueryStatsRateLimit: -1},
+},
+ queryStatsAggregationStageTest);
}());
diff --git a/jstests/noPassthrough/queryStats/query_stats_upgrade.js b/jstests/noPassthrough/queryStats/query_stats_upgrade.js
index 35a05c52b43..86598b548c0 100644
--- a/jstests/noPassthrough/queryStats/query_stats_upgrade.js
+++ b/jstests/noPassthrough/queryStats/query_stats_upgrade.js
@@ -1,6 +1,6 @@
/**
- * Test that telemetry doesn't work on a lower FCV version but works after an FCV upgrade.
- * @tags: [featureFlagQueryStats]
+ * Test that query stats doesn't work on a lower FCV version but works after an FCV upgrade.
+ * @tags: [featureFlagQueryStatsFindCommand]
*/
load('jstests/libs/analyze_plan.js');
load("jstests/libs/feature_flag_util.js");
@@ -11,8 +11,6 @@ load("jstests/libs/feature_flag_util.js");
const dbpath = MongoRunner.dataPath + jsTestName();
let conn = MongoRunner.runMongod({dbpath: dbpath});
let testDB = conn.getDB(jsTestName());
-// This test should only be run with the flag enabled.
-assert(FeatureFlagUtil.isEnabled(testDB, "QueryStats"));
function testLower(restart = false) {
let adminDB = conn.getDB("admin");
@@ -26,13 +24,14 @@ function testLower(restart = false) {
}
assert.commandFailedWithCode(
- testDB.adminCommand({aggregate: 1, pipeline: [{$queryStats: {}}], cursor: {}}), 6579000);
+ testDB.adminCommand({aggregate: 1, pipeline: [{$queryStats: {}}], cursor: {}}),
+ [6579000, ErrorCodes.QueryFeatureNotAllowed]);
// Upgrade FCV.
assert.commandWorked(adminDB.runCommand(
{setFeatureCompatibilityVersion: binVersionToFCV("latest"), confirm: true}));
- // We should be able to run a telemetry pipeline now that the FCV is correct.
+ // We should be able to run a query stats pipeline now that the FCV is correct.
assert.commandWorked(
testDB.adminCommand({aggregate: 1, pipeline: [{$queryStats: {}}], cursor: {}}),
);
diff --git a/jstests/noPassthrough/queryStats/redact_queries_with_nonobject_fields.js b/jstests/noPassthrough/queryStats/redact_queries_with_nonobject_fields.js
index 502a3ab14a9..4543183f61c 100644
--- a/jstests/noPassthrough/queryStats/redact_queries_with_nonobject_fields.js
+++ b/jstests/noPassthrough/queryStats/redact_queries_with_nonobject_fields.js
@@ -1,5 +1,5 @@
/**
- * Test that telemetry key generation works for queries with non-object fields.
+ * Test that query stats key generation works for queries with non-object fields.
* @tags: [featureFlagQueryStats]
*/
load('jstests/libs/analyze_plan.js');
@@ -7,7 +7,7 @@ load('jstests/libs/analyze_plan.js');
(function() {
"use strict";
-// Turn on the collecting of telemetry metrics.
+// Turn on the collecting of query stats metrics.
let options = {
setParameter: {internalQueryStatsRateLimit: -1},
};
diff --git a/jstests/noPassthrough/queryStats/redact_sensitive_fields_in_log.js b/jstests/noPassthrough/queryStats/redact_sensitive_fields_in_log.js
index 7e7599b3cd6..60651aa1ce1 100644
--- a/jstests/noPassthrough/queryStats/redact_sensitive_fields_in_log.js
+++ b/jstests/noPassthrough/queryStats/redact_sensitive_fields_in_log.js
@@ -1,9 +1,6 @@
/**
* Test that the queryStats HMAC key is not logged.
- * @tags: [
- * featureFlagQueryStats,
- * requires_sharding,
- * ]
+ * @tags: [featureFlagQueryStatsFindCommand]
*/
(function() {
diff --git a/jstests/noPassthrough/queryStats/redact_sensitive_fields_in_profile.js b/jstests/noPassthrough/queryStats/redact_sensitive_fields_in_profile.js
index b836ed74be3..2b63ced6fd0 100644
--- a/jstests/noPassthrough/queryStats/redact_sensitive_fields_in_profile.js
+++ b/jstests/noPassthrough/queryStats/redact_sensitive_fields_in_profile.js
@@ -1,6 +1,6 @@
/**
* Test that the queryStats HMAC key is not leaked during profiling.
- * @tags: [featureFlagQueryStats]
+ * @tags: [featureFlagQueryStatsFindCommand]
*/
(function() {
"use strict";
diff --git a/jstests/noPassthrough/queryStats/repl_set_query_stats_key.js b/jstests/noPassthrough/queryStats/repl_set_query_stats_key.js
index 98c86f65cb1..5adb9ecfd4f 100644
--- a/jstests/noPassthrough/queryStats/repl_set_query_stats_key.js
+++ b/jstests/noPassthrough/queryStats/repl_set_query_stats_key.js
@@ -2,7 +2,7 @@
* This test confirms that queryStats store key fields specific to replica sets (readConcern and
* readPreference) are included and correctly shapified. General command fields related to api
* versioning are included for good measure.
- * @tags: [featureFlagQueryStats]
+ * @tags: [featureFlagQueryStatsFindCommand]
*/
load("jstests/libs/query_stats_utils.js");
(function() {
@@ -10,19 +10,16 @@ load("jstests/libs/query_stats_utils.js");
const replTest = new ReplSetTest({name: 'reindexTest', nodes: 2});
-// Turn on the collecting of telemetry metrics.
+// Turn on the collecting of query stats metrics.
replTest.startSet({setParameter: {internalQueryStatsRateLimit: -1}});
replTest.initiate();
const primary = replTest.getPrimary();
-const secondary = replTest.getSecondary();
const dbName = jsTestName();
const collName = "foobar";
const primaryDB = primary.getDB(dbName);
const primaryColl = primaryDB.getCollection(collName);
-const secondaryDB = secondary.getDB(dbName);
-const secondaryColl = secondaryDB.getCollection(collName);
primaryColl.drop();
@@ -53,11 +50,11 @@ let commandObj = {
};
const replSetConn = new Mongo(replTest.getURL());
assert.commandWorked(replSetConn.getDB(dbName).runCommand(commandObj));
-let telemetry = getQueryStats(replSetConn, {collName: collName});
-delete telemetry[0].key["collectionType"];
-confirmCommandFieldsPresent(telemetry[0].key, commandObj);
+let stats = getQueryStats(replSetConn, {collName: collName});
+delete stats[0].key["collectionType"];
+confirmCommandFieldsPresent(stats[0].key, commandObj);
// check that readConcern afterClusterTime is normalized.
-assert.eq(telemetry[0].key.readConcern.afterClusterTime, "?timestamp");
+assert.eq(stats[0].key.readConcern.afterClusterTime, "?timestamp");
// check that readPreference not populated and readConcern just has an afterClusterTime field.
commandObj["readConcern"] = {
@@ -65,11 +62,11 @@ commandObj["readConcern"] = {
};
delete commandObj["$readPreference"];
assert.commandWorked(replSetConn.getDB(dbName).runCommand(commandObj));
-telemetry = getQueryStats(replSetConn, {collName});
+stats = getQueryStats(replSetConn, {collName});
// We're not concerned with this field here.
-delete telemetry[0].key["collectionType"];
-confirmCommandFieldsPresent(telemetry[0].key, commandObj);
-assert.eq(telemetry[0].key["readConcern"], {"afterClusterTime": "?timestamp"});
+delete stats[0].key["collectionType"];
+confirmCommandFieldsPresent(stats[0].key, commandObj);
+assert.eq(stats[0].key["readConcern"], {"afterClusterTime": "?timestamp"});
// check that readConcern has no afterClusterTime and fields related to api usage are not present.
commandObj["readConcern"] = {
@@ -79,11 +76,11 @@ delete commandObj["apiDeprecationErrors"];
delete commandObj["apiVersion"];
delete commandObj["apiStrict"];
assert.commandWorked(replSetConn.getDB(dbName).runCommand(commandObj));
-telemetry = getQueryStats(replSetConn, {collName: collName});
-assert.eq(telemetry[1].key["readConcern"], {level: "local"});
+stats = getQueryStats(replSetConn, {collName: collName});
+assert.eq(stats[1].key["readConcern"], {level: "local"});
// We're not concerned with this field here.
-delete telemetry[1].key["collectionType"];
-confirmCommandFieldsPresent(telemetry[1].key, commandObj);
+delete stats[1].key["collectionType"];
+confirmCommandFieldsPresent(stats[1].key, commandObj);
replTest.stopSet();
})();
diff --git a/jstests/noPassthrough/queryStats/single_batch_on_mongod.js b/jstests/noPassthrough/queryStats/single_batch_on_mongod.js
deleted file mode 100644
index 9852aaec586..00000000000
--- a/jstests/noPassthrough/queryStats/single_batch_on_mongod.js
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * Test that the queryStats metrics are aggregated properly for queries run on a mongod where the
- * results fit into a single batch (and thus don't require a cursor).
- * @tags: [featureFlagQueryStats]
- */
-load("jstests/libs/query_stats_utils.js"); // For verifyMetrics and getQueryStatsAggCmd.
-
-(function() {
-"use strict";
-
-// Turn on the collecting of queryStats metrics.
-let options = {
- setParameter: {internalQueryStatsRateLimit: -1},
-};
-
-const conn = MongoRunner.runMongod(options);
-const testDB = conn.getDB('test');
-var coll = testDB[jsTestName()];
-coll.drop();
-
-// Bulk insert documents to reduces roundtrips and make timeout on a slow machine less likely.
-const bulk = coll.initializeUnorderedBulkOp();
-const numDocs = 100;
-for (let i = 0; i < numDocs / 2; ++i) {
- bulk.insert({foo: 0, bar: Math.floor(Math.random() * 3)});
- bulk.insert({foo: 1, bar: Math.floor(Math.random() * -2)});
-}
-assert.commandWorked(bulk.execute());
-
-// Assert that two find queries with identical structures are represented by the same key.
-{
- // Note that toArray is necessary to guarantee the query finishes executing on the server (at
- // which point an entry is finally written to the queryStats store).
- coll.find({foo: {$eq: 0}}).toArray();
- coll.find({foo: {$eq: 1}}).toArray();
-
- // This command will return all queryStats store entires.
- const queryStatsResults = getQueryStatsFindCmd(testDB);
- // Assert there is only one entry.
- assert.eq(queryStatsResults.length, 1, queryStatsResults);
- const queryStatsEntry = queryStatsResults[0];
- assert.eq(queryStatsEntry.key.queryShape.cmdNs.db, "test");
- assert.eq(queryStatsEntry.key.queryShape.cmdNs.coll, jsTestName());
- assert.eq(queryStatsEntry.key.client.application.name, "MongoDB Shell");
-
- // Assert we update execution count for identically shaped queries.
- assert.eq(queryStatsEntry.metrics.execCount, 2);
-
- // Assert queryStats values are accurate for the two above queries.
- assert.eq(queryStatsEntry.metrics.docsReturned.sum, numDocs);
- assert.eq(queryStatsEntry.metrics.docsReturned.min, numDocs / 2);
- assert.eq(queryStatsEntry.metrics.docsReturned.max, numDocs / 2);
-
- // The total size of documents in the collection should ensure that the queries in this test can
- // be executed without requiring multiple batches, but we verify that by looking at the
- // timestamps.
- assert.eq(queryStatsEntry.metrics.firstResponseExecMicros,
- queryStatsEntry.metrics.totalExecMicros);
- verifyMetrics(queryStatsResults);
-}
-
-// Assert that two agg queries with identical structures are represented by the same key.
-{
- // Note that toArray is necessary to guarantee the query finishes executing on the server (at
- // which point an entry is finally written to the queryStats store).
- coll.aggregate([{$match: {foo: 1}}]).toArray();
- coll.aggregate([{$match: {foo: 0}}]).toArray();
-
- // This command will return all queryStats store entires.
- const queryStatsResults = getQueryStatsAggCmd(testDB);
- // Assert there is only one entry.
- assert.eq(queryStatsResults.length, 1, queryStatsResults);
- const queryStatsEntry = queryStatsResults[0];
- assert.eq(queryStatsEntry.key.queryShape.cmdNs.db, "test");
- assert.eq(queryStatsEntry.key.queryShape.cmdNs.coll, jsTestName());
- assert.eq(queryStatsEntry.key.client.application.name, "MongoDB Shell");
-
- // Assert we update execution count for identically shaped queries.
- assert.eq(queryStatsEntry.metrics.execCount, 2);
-
- // Assert queryStats values are accurate for the two above queries.
- assert.eq(queryStatsEntry.metrics.docsReturned.sum, numDocs);
- assert.eq(queryStatsEntry.metrics.docsReturned.min, numDocs / 2);
- assert.eq(queryStatsEntry.metrics.docsReturned.max, numDocs / 2);
-
- // The total size of documents in the collection should ensure that the queries in this test can
- // be executed without requiring multiple batches, but we verify that by looking at the
- // timestamps.
- assert.eq(queryStatsEntry.metrics.firstResponseExecMicros,
- queryStatsEntry.metrics.totalExecMicros);
-
- verifyMetrics(queryStatsResults);
-}
-MongoRunner.stopMongod(conn);
-}());
diff --git a/jstests/noPassthrough/queryStats/single_batch_on_mongod_agg_cmd.js b/jstests/noPassthrough/queryStats/single_batch_on_mongod_agg_cmd.js
new file mode 100644
index 00000000000..9048053e89b
--- /dev/null
+++ b/jstests/noPassthrough/queryStats/single_batch_on_mongod_agg_cmd.js
@@ -0,0 +1,61 @@
+/**
+ * Test that the queryStats metrics are aggregated properly for queries run on a mongod where the
+ * results fit into a single batch (and thus don't require a cursor), for agg commands.
+ * @tags: [featureFlagQueryStats]
+ */
+load("jstests/libs/query_stats_utils.js"); // For verifyMetrics and getQueryStatsAggCmd.
+
+(function() {
+"use strict";
+
+// Turn on the collecting of queryStats metrics.
+let options = {
+ setParameter: {internalQueryStatsRateLimit: -1},
+};
+
+const conn = MongoRunner.runMongod(options);
+const testDB = conn.getDB('test');
+var coll = testDB[jsTestName()];
+coll.drop();
+
+// Bulk insert documents to reduces roundtrips and make timeout on a slow machine less likely.
+const bulk = coll.initializeUnorderedBulkOp();
+const numDocs = 100;
+for (let i = 0; i < numDocs / 2; ++i) {
+ bulk.insert({foo: 0, bar: Math.floor(Math.random() * 3)});
+ bulk.insert({foo: 1, bar: Math.floor(Math.random() * -2)});
+}
+assert.commandWorked(bulk.execute());
+
+// Assert that two agg queries with identical structures are represented by the same key.
+
+// Note that toArray is necessary to guarantee the query finishes executing on the server (at
+// which point an entry is finally written to the queryStats store).
+coll.aggregate([{$match: {foo: 1}}]).toArray();
+coll.aggregate([{$match: {foo: 0}}]).toArray();
+
+// This command will return all queryStats store entires.
+const queryStatsResults = getQueryStatsAggCmd(testDB);
+// Assert there is only one entry.
+assert.eq(queryStatsResults.length, 1, queryStatsResults);
+const queryStatsEntry = queryStatsResults[0];
+assert.eq(queryStatsEntry.key.queryShape.cmdNs.db, "test");
+assert.eq(queryStatsEntry.key.queryShape.cmdNs.coll, jsTestName());
+assert.eq(queryStatsEntry.key.client.application.name, "MongoDB Shell");
+
+// Assert we update execution count for identically shaped queries.
+assert.eq(queryStatsEntry.metrics.execCount, 2);
+
+// Assert queryStats values are accurate for the two above queries.
+assert.eq(queryStatsEntry.metrics.docsReturned.sum, numDocs);
+assert.eq(queryStatsEntry.metrics.docsReturned.min, numDocs / 2);
+assert.eq(queryStatsEntry.metrics.docsReturned.max, numDocs / 2);
+
+// The total size of documents in the collection should ensure that the queries in this test can
+// be executed without requiring multiple batches, but we verify that by looking at the
+// timestamps.
+assert.eq(queryStatsEntry.metrics.firstResponseExecMicros, queryStatsEntry.metrics.totalExecMicros);
+
+verifyMetrics(queryStatsResults);
+MongoRunner.stopMongod(conn);
+}());
diff --git a/jstests/noPassthrough/queryStats/single_batch_on_mongod_find_cmd.js b/jstests/noPassthrough/queryStats/single_batch_on_mongod_find_cmd.js
new file mode 100644
index 00000000000..fc8a4ab1faf
--- /dev/null
+++ b/jstests/noPassthrough/queryStats/single_batch_on_mongod_find_cmd.js
@@ -0,0 +1,59 @@
+/**
+ * Test that the queryStats metrics are aggregated properly for queries run on a mongod where the
+ * results fit into a single batch (and thus don't require a cursor), for find commands.
+ * @tags: [featureFlagQueryStatsFindCommand]
+ */
+load("jstests/libs/query_stats_utils.js"); // For verifyMetrics and getQueryStatsAggCmd.
+
+(function() {
+"use strict";
+
+// Turn on the collecting of queryStats metrics.
+let options = {
+ setParameter: {internalQueryStatsRateLimit: -1},
+};
+
+const conn = MongoRunner.runMongod(options);
+const testDB = conn.getDB('test');
+var coll = testDB[jsTestName()];
+coll.drop();
+
+// Bulk insert documents to reduces roundtrips and make timeout on a slow machine less likely.
+const bulk = coll.initializeUnorderedBulkOp();
+const numDocs = 100;
+for (let i = 0; i < numDocs / 2; ++i) {
+ bulk.insert({foo: 0, bar: Math.floor(Math.random() * 3)});
+ bulk.insert({foo: 1, bar: Math.floor(Math.random() * -2)});
+}
+assert.commandWorked(bulk.execute());
+
+// Note that toArray is necessary to guarantee the query finishes executing on the server (at
+// which point an entry is finally written to the queryStats store).
+coll.find({foo: {$eq: 0}}).toArray();
+coll.find({foo: {$eq: 1}}).toArray();
+
+// This command will return all queryStats store entires.
+const queryStatsResults = getQueryStatsFindCmd(testDB);
+// Assert there is only one entry.
+assert.eq(queryStatsResults.length, 1, queryStatsResults);
+const queryStatsEntry = queryStatsResults[0];
+assert.eq(queryStatsEntry.key.queryShape.cmdNs.db, "test");
+assert.eq(queryStatsEntry.key.queryShape.cmdNs.coll, jsTestName());
+assert.eq(queryStatsEntry.key.client.application.name, "MongoDB Shell");
+
+// Assert we update execution count for identically shaped queries.
+assert.eq(queryStatsEntry.metrics.execCount, 2);
+
+// Assert queryStats values are accurate for the two above queries.
+assert.eq(queryStatsEntry.metrics.docsReturned.sum, numDocs);
+assert.eq(queryStatsEntry.metrics.docsReturned.min, numDocs / 2);
+assert.eq(queryStatsEntry.metrics.docsReturned.max, numDocs / 2);
+
+// The total size of documents in the collection should ensure that the queries in this test can
+// be executed without requiring multiple batches, but we verify that by looking at the
+// timestamps.
+assert.eq(queryStatsEntry.metrics.firstResponseExecMicros, queryStatsEntry.metrics.totalExecMicros);
+verifyMetrics(queryStatsResults);
+
+MongoRunner.stopMongod(conn);
+}());
diff --git a/jstests/noPassthroughWithMongod/query_stats_configuration.js b/jstests/noPassthroughWithMongod/query_stats_configuration.js
index d7cc2acba53..b9a649aaca7 100644
--- a/jstests/noPassthroughWithMongod/query_stats_configuration.js
+++ b/jstests/noPassthroughWithMongod/query_stats_configuration.js
@@ -1,14 +1,15 @@
/**
- * Tests that the telemetry store can be resized if it is configured, and cannot be resized if it is
- * disabled.
+ * Tests that the query stats store can be resized if it is configured, and cannot be resized if it
+ * is disabled.
*/
(function() {
"use strict";
load("jstests/libs/feature_flag_util.js");
-if (FeatureFlagUtil.isEnabled(db, "QueryStats")) {
- function testTelemetrySetting(paramName, paramValue) {
+if (FeatureFlagUtil.isEnabled(db, "QueryStats") ||
+ FeatureFlagUtil.isEnabled(db, "QueryStatsFindCommand")) {
+ function testQueryStatsSetting(paramName, paramValue) {
// The feature flag is enabled - make sure the telemetry store can be configured.
const original = assert.commandWorked(db.adminCommand({getParameter: 1, [paramName]: 1}));
assert(original.hasOwnProperty(paramName), original);
@@ -21,10 +22,10 @@ if (FeatureFlagUtil.isEnabled(db, "QueryStats")) {
db.adminCommand({setParameter: 1, [paramName]: originalValue}));
}
}
- testTelemetrySetting("internalQueryStatsCacheSize", "2MB");
- testTelemetrySetting("internalQueryStatsRateLimit", 2147483647);
+ testQueryStatsSetting("internalQueryStatsCacheSize", "2MB");
+ testQueryStatsSetting("internalQueryStatsRateLimit", 2147483647);
} else {
- // The feature flag is disabled - make sure the telemetry store *cannot* be configured.
+ // The feature flag is disabled - make sure the query stats store *cannot* be configured.
assert.commandFailedWithCode(
db.adminCommand({setParameter: 1, internalQueryStatsCacheSize: '2MB'}), 7373500);
assert.commandFailedWithCode(
diff --git a/src/mongo/db/SConscript b/src/mongo/db/SConscript
index df9dfcc0d02..8a4f29dedfb 100644
--- a/src/mongo/db/SConscript
+++ b/src/mongo/db/SConscript
@@ -119,7 +119,6 @@ env.Library(
'$BUILD_DIR/mongo/db/catalog/clustered_collection_options',
'$BUILD_DIR/mongo/db/catalog/collection_crud',
'$BUILD_DIR/mongo/db/concurrency/exception_util',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/db/session/logical_session_id_helpers',
'$BUILD_DIR/mongo/db/storage/key_string',
'$BUILD_DIR/mongo/db/transaction/transaction',
@@ -196,7 +195,7 @@ env.Library(
'curop_failpoint_helpers.cpp',
],
LIBDEPS=[
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/util/fail_point',
],
)
@@ -561,7 +560,6 @@ env.Library(
'$BUILD_DIR/mongo/db/catalog/clustered_collection_options',
'$BUILD_DIR/mongo/db/catalog/collection_crud',
'$BUILD_DIR/mongo/db/concurrency/exception_util',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/db/repl/storage_interface',
'change_stream_options_manager',
'change_stream_serverless_helpers',
@@ -889,7 +887,7 @@ env.Library(
LIBDEPS_PRIVATE=[
'$BUILD_DIR/mongo/db/catalog/collection_uuid_mismatch_info',
'$BUILD_DIR/mongo/db/catalog/local_oplog_info',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'multitenancy',
'server_base',
'storage/capped_snapshots',
@@ -1004,7 +1002,7 @@ env.Library(
],
LIBDEPS=[
'$BUILD_DIR/mongo/client/clientdriver_minimal',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'not_primary_error_tracker',
'ops/write_ops_parsers',
],
@@ -1052,7 +1050,6 @@ env.Library(
],
LIBDEPS=[
'$BUILD_DIR/mongo/base',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/transport/service_entry_point',
'curop_metrics',
'rw_concern_d',
@@ -1066,6 +1063,7 @@ env.Library(
'commands/fsync_locked',
'concurrency/lock_manager',
'not_primary_error_tracker',
+ 'query/query_stats/query_stats',
'read_concern_d_impl',
's/sharding_runtime_d',
'service_entry_point_common',
@@ -1146,7 +1144,6 @@ env.Library(
'catalog/collection_catalog',
'catalog/index_build_entry_idl',
'index_build_entry_helpers',
- 'query/op_metrics',
'repl/tenant_migration_access_blocker',
'resumable_index_builds_idl',
's/forwardable_operation_metadata',
@@ -1239,7 +1236,7 @@ env.Library(
"curop_metrics.cpp",
],
LIBDEPS_PRIVATE=[
- "$BUILD_DIR/mongo/db/query/op_metrics",
+ "$BUILD_DIR/mongo/db/query/query_stats/query_stats",
"commands/server_status_core",
],
)
@@ -1265,7 +1262,7 @@ env.Library(
'write_concern.cpp',
],
LIBDEPS=[
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'read_write_concern_defaults',
'repl/read_concern_args',
'repl/speculative_majority_read_info',
@@ -1289,7 +1286,7 @@ env.Library(
'read_concern_mongod.idl',
],
LIBDEPS_PRIVATE=[
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/s/grid',
'concurrency/exception_util',
'repl/repl_coordinator_interface',
@@ -1503,7 +1500,7 @@ env.Library(
LIBDEPS=[
'$BUILD_DIR/mongo/base',
'$BUILD_DIR/mongo/db/auth/auth_checks',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/scripting/scripting',
'$BUILD_DIR/mongo/util/background_job',
'$BUILD_DIR/mongo/util/elapsed_tracker',
@@ -1661,7 +1658,7 @@ env.Library(
'query/collation/collator_interface',
'query/datetime/date_time_support',
'query/query_knobs',
- 'serialization_options',
+ 'query_shape_common',
'stats/counters',
'update/pattern_cmp',
],
@@ -1674,17 +1671,17 @@ env.Library(
)
env.Library(
- target='serialization_options',
- source=[
+ target='query_shape_common', source=[
+ 'query/query_shape.idl',
+ 'query/shape_helpers.cpp',
'query/serialization_options.cpp',
- ],
- LIBDEPS=[
+ ], LIBDEPS=[
'$BUILD_DIR/mongo/base',
'$BUILD_DIR/mongo/db/exec/document_value/document_value',
'$BUILD_DIR/mongo/db/pipeline/field_path',
- ],
- LIBDEPS_PRIVATE=[],
-)
+ ], LIBDEPS_PRIVATE=[
+ '$BUILD_DIR/mongo/util/namespace_string_database_name_util',
+ ])
env.Library(
target='startup_recovery',
@@ -1801,7 +1798,7 @@ env.Library(
],
LIBDEPS=[
'$BUILD_DIR/mongo/db/pipeline/lite_parsed_document_source',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/session/logical_session_id',
'commands',
],
@@ -2276,7 +2273,7 @@ env.Library(
LIBDEPS_PRIVATE=[
# NOTE: If you need to add a static or mongo initializer to mongod startup,
# please add that library here, as a private library dependency.
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/executor/async_rpc_error_info',
'$BUILD_DIR/mongo/executor/network_interface_factory',
'$BUILD_DIR/mongo/rpc/rpc',
@@ -2407,7 +2404,6 @@ env.Library(
'$BUILD_DIR/mongo/db/change_stream_options_manager',
'$BUILD_DIR/mongo/db/change_streams_cluster_parameter',
'$BUILD_DIR/mongo/db/pipeline/change_stream_expired_pre_image_remover',
- '$BUILD_DIR/mongo/db/query/stats/query_stats',
'$BUILD_DIR/mongo/db/s/query_analysis_writer',
'$BUILD_DIR/mongo/db/set_change_stream_state_coordinator',
'$BUILD_DIR/mongo/idl/cluster_server_parameter',
@@ -2453,6 +2449,7 @@ env.Library(
'op_observer/user_write_block_mode_op_observer',
'periodic_runner_job_abort_expired_transactions',
'pipeline/process_interface/mongod_process_interface_factory',
+ 'query/stats/stats',
'repl/drop_pending_collection_reaper',
'repl/initial_syncer',
'repl/repl_coordinator_impl',
@@ -2672,7 +2669,6 @@ if wiredtiger:
'$BUILD_DIR/mongo/db/ops/write_ops',
'$BUILD_DIR/mongo/db/pipeline/change_stream_expired_pre_image_remover',
'$BUILD_DIR/mongo/db/query/common_query_enums_and_helpers',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/db/query/query_test_service_context',
'$BUILD_DIR/mongo/db/repl/image_collection_entry',
'$BUILD_DIR/mongo/db/repl/repl_coordinator_interface',
@@ -2715,6 +2711,7 @@ if wiredtiger:
'mirror_maestro',
'multitenancy',
'operation_time_tracker',
+ 'query/query_stats/query_stats',
'query_exec',
'read_write_concern_defaults_mock',
'record_id_helpers',
diff --git a/src/mongo/db/auth/SConscript b/src/mongo/db/auth/SConscript
index f8abf79f273..653f38a192d 100644
--- a/src/mongo/db/auth/SConscript
+++ b/src/mongo/db/auth/SConscript
@@ -202,7 +202,7 @@ env.Library(
'$BUILD_DIR/mongo/db/commands/authentication_commands',
'$BUILD_DIR/mongo/db/common',
'$BUILD_DIR/mongo/db/global_settings',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/util/concurrency/thread_pool',
'$BUILD_DIR/mongo/util/icu',
'$BUILD_DIR/mongo/util/net/ssl_manager',
diff --git a/src/mongo/db/auth/action_type.idl b/src/mongo/db/auth/action_type.idl
index 650dadeee57..a111236ac9e 100644
--- a/src/mongo/db/auth/action_type.idl
+++ b/src/mongo/db/auth/action_type.idl
@@ -149,7 +149,8 @@ enums:
planCacheIndexFilter : "planCacheIndexFilter" # view/update index filters
planCacheRead : "planCacheRead" # view contents of plan cache
planCacheWrite : "planCacheWrite" # clear cache, drop cache entry, pin/unpin/shun plans
- queryStatsRead: "queryStatsRead" # view contents of queryStats store
+ queryStatsRead: "queryStatsRead" # view untransformed contents of queryStats store
+ queryStatsReadTransformed: "queryStatsReadTransformed" # view transformed contents of queryStats store
refineCollectionShardKey : "refineCollectionShardKey"
reIndex : "reIndex"
remove : "remove"
diff --git a/src/mongo/db/auth/builtin_roles.yml b/src/mongo/db/auth/builtin_roles.yml
index 19ee41b876a..626f10935c6 100644
--- a/src/mongo/db/auth/builtin_roles.yml
+++ b/src/mongo/db/auth/builtin_roles.yml
@@ -352,6 +352,7 @@ roles:
- setChangeStreamState
- getChangeStreamState
- queryStatsRead
+ - queryStatsReadTransformed
- checkMetadataConsistency
- transitionFromDedicatedConfigServer
- transitionToDedicatedConfigServer
diff --git a/src/mongo/db/catalog/SConscript b/src/mongo/db/catalog/SConscript
index 1b0adf9370a..9b44fbb8e74 100644
--- a/src/mongo/db/catalog/SConscript
+++ b/src/mongo/db/catalog/SConscript
@@ -125,7 +125,7 @@ env.Library(
LIBDEPS_PRIVATE=[
'$BUILD_DIR/mongo/db/concurrency/exception_util',
'$BUILD_DIR/mongo/db/index/index_access_method',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/shard_role',
'$BUILD_DIR/mongo/db/storage/duplicate_key_error_info',
'$BUILD_DIR/mongo/db/storage/key_string',
@@ -142,8 +142,8 @@ env.Library(
LIBDEPS_PRIVATE=[
'$BUILD_DIR/mongo/base',
'$BUILD_DIR/mongo/db/concurrency/exception_util',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/db/query/query_knobs',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/shard_role',
'$BUILD_DIR/mongo/db/storage/record_store_base',
'$BUILD_DIR/mongo/db/storage/storage_repair_observer',
@@ -232,7 +232,7 @@ env.Library(
'collection_write_path.cpp',
],
LIBDEPS_PRIVATE=[
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/record_id_helpers',
'$BUILD_DIR/mongo/db/repl/repl_coordinator_interface',
'$BUILD_DIR/mongo/db/shard_role_api',
@@ -270,7 +270,7 @@ env.Library(
'$BUILD_DIR/mongo/base',
'$BUILD_DIR/mongo/db/concurrency/exception_util',
'$BUILD_DIR/mongo/db/index/index_access_method',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/query_exec',
'$BUILD_DIR/mongo/db/repl/repl_coordinator_interface',
'$BUILD_DIR/mongo/db/resumable_index_builds_idl',
@@ -307,7 +307,7 @@ env.Library(
'$BUILD_DIR/mongo/db/index/index_access_method',
'$BUILD_DIR/mongo/db/profile_filter',
'$BUILD_DIR/mongo/db/query/collation/collator_factory_interface',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/server_base',
'$BUILD_DIR/mongo/db/service_context',
'$BUILD_DIR/mongo/db/storage/bson_collection_catalog_entry',
@@ -392,7 +392,7 @@ env.Library(
'$BUILD_DIR/mongo/db/index/index_access_method',
'$BUILD_DIR/mongo/db/multitenancy',
'$BUILD_DIR/mongo/db/op_observer/op_observer',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/record_id_helpers',
'$BUILD_DIR/mongo/db/repl/drop_pending_collection_reaper',
'$BUILD_DIR/mongo/db/repl/oplog',
@@ -465,7 +465,7 @@ env.Library(
'$BUILD_DIR/mongo/db/concurrency/exception_util',
'$BUILD_DIR/mongo/db/index/index_access_method',
'$BUILD_DIR/mongo/db/multi_key_path_tracker',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/record_id_helpers',
'$BUILD_DIR/mongo/db/server_base',
'$BUILD_DIR/mongo/db/service_context',
@@ -493,7 +493,7 @@ env.Library(
],
LIBDEPS_PRIVATE=[
'$BUILD_DIR/mongo/db/index/index_access_method',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/util/fail_point',
'validate_idl',
],
@@ -599,8 +599,8 @@ env.Library(
LIBDEPS_PRIVATE=[
'$BUILD_DIR/mongo/db/collection_index_usage_tracker',
'$BUILD_DIR/mongo/db/fts/base_fts',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/db/query/query_planner',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/server_base',
'$BUILD_DIR/mongo/db/service_context',
'$BUILD_DIR/mongo/db/shard_role_api',
diff --git a/src/mongo/db/clientcursor.cpp b/src/mongo/db/clientcursor.cpp
index 23fbaa9e538..8db9f882933 100644
--- a/src/mongo/db/clientcursor.cpp
+++ b/src/mongo/db/clientcursor.cpp
@@ -48,7 +48,7 @@
#include "mongo/db/cursor_server_params.h"
#include "mongo/db/jsobj.h"
#include "mongo/db/query/explain.h"
-#include "mongo/db/query/query_stats.h"
+#include "mongo/db/query/query_stats/query_stats.h"
#include "mongo/db/repl/repl_client_info.h"
#include "mongo/db/repl/replication_coordinator.h"
#include "mongo/util/background.h"
diff --git a/src/mongo/db/collection_type.h b/src/mongo/db/collection_type.h
index e30056c8146..0c58804bf2a 100644
--- a/src/mongo/db/collection_type.h
+++ b/src/mongo/db/collection_type.h
@@ -29,18 +29,42 @@
#pragma once
-#include "mongo/db/query/util/named_enum.h"
+#include "mongo/util/assert_util.h"
namespace mongo::query_shape {
-#define COLLECTION_TYPE(F) \
- F(unknown) \
- F(timeseries) \
- F(view) \
- F(nonExistent) \
- F(collection) \
- F(end)
-QUERY_UTIL_NAMED_ENUM_DEFINE(CollectionType, COLLECTION_TYPE)
-#undef COLLECTION_TYPE
+// This enum is not compatible with the QUERY_UTIL_NAMED_ENUM_DEFINE util since the "virtual" type
+// conflicts with the C++ keyword "virtual". Instead, we manually define the enum and the
+// toStringData function below.
+enum class CollectionType {
+ kUnknown,
+ kCollection,
+ kView,
+ kTimeseries,
+ kChangeStream,
+ kVirtual,
+ kNonExistent,
+};
+
+static StringData toStringData(CollectionType type) {
+ switch (type) {
+ case CollectionType::kUnknown:
+ return "unknown"_sd;
+ case CollectionType::kCollection:
+ return "collection"_sd;
+ case CollectionType::kView:
+ return "view"_sd;
+ case CollectionType::kTimeseries:
+ return "timeseries"_sd;
+ case CollectionType::kChangeStream:
+ return "changeStream"_sd;
+ case CollectionType::kVirtual:
+ return "virtual"_sd;
+ case CollectionType::kNonExistent:
+ return "nonExistent"_sd;
+ default:
+ MONGO_UNREACHABLE_TASSERT(7804900);
+ }
+}
} // namespace mongo::query_shape
diff --git a/src/mongo/db/commands/SConscript b/src/mongo/db/commands/SConscript
index 5c8f2de0365..4d0ce84193b 100644
--- a/src/mongo/db/commands/SConscript
+++ b/src/mongo/db/commands/SConscript
@@ -200,7 +200,7 @@ env.Library(
'$BUILD_DIR/mongo/db/concurrency/exception_util',
'$BUILD_DIR/mongo/db/concurrency/lock_manager',
'$BUILD_DIR/mongo/db/dbdirectclient',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/storage/backup_cursor_hooks',
'fsync_locked',
],
@@ -411,9 +411,9 @@ env.Library(
'$BUILD_DIR/mongo/db/pipeline/process_interface/mongo_process_interface',
'$BUILD_DIR/mongo/db/query/command_request_response',
'$BUILD_DIR/mongo/db/query/cursor_response_idl',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/db/query/query_shape',
- '$BUILD_DIR/mongo/db/query/stats/query_stats',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
+ '$BUILD_DIR/mongo/db/query/stats/stats',
'$BUILD_DIR/mongo/db/query/stats/stats_histograms',
'$BUILD_DIR/mongo/db/query_exec',
'$BUILD_DIR/mongo/db/repl/replica_set_messages',
@@ -721,7 +721,7 @@ env.Library(
'$BUILD_DIR/mongo/db/auth/auth',
'$BUILD_DIR/mongo/db/auth/authprivilege',
'$BUILD_DIR/mongo/db/catalog/collection_catalog',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/query_expressions',
'$BUILD_DIR/mongo/db/server_base',
],
diff --git a/src/mongo/db/commands/find_cmd.cpp b/src/mongo/db/commands/find_cmd.cpp
index c78bbcaefc4..1b7d3973a2d 100644
--- a/src/mongo/db/commands/find_cmd.cpp
+++ b/src/mongo/db/commands/find_cmd.cpp
@@ -55,8 +55,9 @@
#include "mongo/db/query/get_executor.h"
#include "mongo/db/query/query_knobs_gen.h"
#include "mongo/db/query/query_shape.h"
-#include "mongo/db/query/query_stats.h"
-#include "mongo/db/query/query_stats_find_key_generator.h"
+#include "mongo/db/query/query_stats/find_key_generator.h"
+#include "mongo/db/query/query_stats/key_generator.h"
+#include "mongo/db/query/query_stats/query_stats.h"
#include "mongo/db/repl/replication_coordinator.h"
#include "mongo/db/s/query_analysis_writer.h"
#include "mongo/db/service_context.h"
@@ -155,14 +156,20 @@ std::unique_ptr<CanonicalQuery> parseQueryAndBeginOperation(
// It is important to do this before canonicalizing and optimizing the query, each of which
// would alter the query shape.
if (!(collection && collection.get()->getCollectionOptions().encryptedFieldConfig)) {
- query_stats::registerRequest(opCtx, nss, [&]() {
- BSONObj queryShape = query_shape::extractQueryShape(
- *parsedRequest,
- SerializationOptions::kRepresentativeQueryShapeSerializeOptions,
- expCtx);
- return std::make_unique<query_stats::FindKeyGenerator>(
- expCtx, *parsedRequest, std::move(queryShape), ctx.getCollectionType());
- });
+ query_stats::registerRequest(
+ opCtx,
+ nss,
+ [&]() {
+ // This callback is either never invoked or invoked immediately within
+ // registerRequest, so use-after-move of parsedRequest isn't an issue.
+ BSONObj queryShape = query_shape::extractQueryShape(
+ *parsedRequest,
+ SerializationOptions::kRepresentativeQueryShapeSerializeOptions,
+ expCtx);
+ return std::make_unique<query_stats::FindKeyGenerator>(
+ expCtx, *parsedRequest, std::move(queryShape), ctx.getCollectionType());
+ },
+ /*requiresFullQueryStatsFeatureFlag*/ false);
}
return uassertStatusOK(
diff --git a/src/mongo/db/commands/run_aggregate.cpp b/src/mongo/db/commands/run_aggregate.cpp
index 8220d3f7015..996f043c713 100644
--- a/src/mongo/db/commands/run_aggregate.cpp
+++ b/src/mongo/db/commands/run_aggregate.cpp
@@ -73,8 +73,9 @@
#include "mongo/db/query/plan_summary_stats.h"
#include "mongo/db/query/query_knobs_gen.h"
#include "mongo/db/query/query_planner_common.h"
-#include "mongo/db/query/query_stats.h"
-#include "mongo/db/query/query_stats_aggregate_key_generator.h"
+#include "mongo/db/query/query_stats/aggregate_key_generator.h"
+#include "mongo/db/query/query_stats/key_generator.h"
+#include "mongo/db/query/query_stats/query_stats.h"
#include "mongo/db/read_concern.h"
#include "mongo/db/repl/oplog.h"
#include "mongo/db/repl/read_concern_args.h"
@@ -981,6 +982,14 @@ Status _runAggregate(OperationContext* opCtx,
return std::make_pair(expCtx, std::move(pipeline));
};
+ auto collectionType =
+ ctx ? ctx->getCollectionType() : query_shape::CollectionType::kUnknown;
+ if (liteParsedPipeline.hasChangeStream()) {
+ collectionType = query_shape::CollectionType::kChangeStream;
+ } else if (nss.isCollectionlessAggregateNS()) {
+ collectionType = query_shape::CollectionType::kVirtual;
+ }
+
// If this is a view, resolve it by finding the underlying collection and stitching view
// pipelines and this request's pipeline together. We then release our locks before
// recursively calling runAggregate(), which will re-acquire locks on the underlying
@@ -994,7 +1003,7 @@ Status _runAggregate(OperationContext* opCtx,
(!liteParsedPipeline.startsWithCollStats() || ctx->getView()->timeseries())) {
try {
invariant(collatorToUse.has_value());
- query_stats::registerRequest(opCtx, nss, [&]() {
+ query_stats::registerRequest(opCtx, request.getNamespace(), [&]() {
// In this path we haven't yet parsed the pipeline, but we need to do so for
// query shape stats - which should track the queries before views are resolved.
// Inside this callback we know we have already checked that query stats are
@@ -1011,7 +1020,7 @@ Status _runAggregate(OperationContext* opCtx,
expCtx,
pipelineInvolvedNamespaces,
origNss,
- ctx->getCollectionType());
+ collectionType);
});
} catch (const DBException& ex) {
if (ex.code() == 6347902) {
@@ -1053,14 +1062,14 @@ Status _runAggregate(OperationContext* opCtx,
// with encrypted fields. We still collect query stats on collection-less aggregations.
if (!(ctx && ctx->getCollection() &&
ctx->getCollection()->getCollectionOptions().encryptedFieldConfig)) {
- query_stats::registerRequest(opCtx, nss, [&]() {
+ query_stats::registerRequest(opCtx, request.getNamespace(), [&]() {
return std::make_unique<query_stats::AggregateKeyGenerator>(
request,
*pipeline,
expCtx,
pipelineInvolvedNamespaces,
- nss,
- ctx ? ctx->getCollectionType() : query_shape::CollectionType::unknown);
+ request.getNamespace(),
+ collectionType);
});
}
diff --git a/src/mongo/db/concurrency/SConscript b/src/mongo/db/concurrency/SConscript
index 1a94a2cc627..458259a11b7 100644
--- a/src/mongo/db/concurrency/SConscript
+++ b/src/mongo/db/concurrency/SConscript
@@ -27,7 +27,7 @@ env.Library(
],
LIBDEPS_PRIVATE=[
'$BUILD_DIR/mongo/db/commands/server_status_core',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/server_base',
'$BUILD_DIR/mongo/db/server_options_servers',
'$BUILD_DIR/mongo/db/storage/recovery_unit_base',
@@ -104,7 +104,7 @@ env.CppUnitTest(
],
LIBDEPS=[
'$BUILD_DIR/mongo/db/auth/authmocks',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/service_context_d_test_fixture',
'$BUILD_DIR/mongo/transport/transport_layer_common',
'$BUILD_DIR/mongo/transport/transport_layer_mock',
diff --git a/src/mongo/db/curop.h b/src/mongo/db/curop.h
index cae355b5704..3dedf0a84e9 100644
--- a/src/mongo/db/curop.h
+++ b/src/mongo/db/curop.h
@@ -40,7 +40,7 @@
#include "mongo/db/cursor_id.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/profile_filter.h"
-#include "mongo/db/query/query_stats_key_generator.h"
+#include "mongo/db/query/query_stats/key_generator.h"
#include "mongo/db/server_options.h"
#include "mongo/db/stats/resource_consumption_metrics.h"
#include "mongo/db/write_concern_options.h"
diff --git a/src/mongo/db/db_raii.cpp b/src/mongo/db/db_raii.cpp
index b55b118c966..6e2de95fa9e 100644
--- a/src/mongo/db/db_raii.cpp
+++ b/src/mongo/db/db_raii.cpp
@@ -1586,12 +1586,12 @@ const NamespaceString& AutoGetCollectionForReadCommandMaybeLockFree::getNss() co
query_shape::CollectionType AutoGetCollectionForReadCommandMaybeLockFree::getCollectionType()
const {
if (auto&& view = getView()) {
- return view->timeseries() ? query_shape::CollectionType::timeseries
- : query_shape::CollectionType::view;
+ return view->timeseries() ? query_shape::CollectionType::kTimeseries
+ : query_shape::CollectionType::kView;
}
auto&& collection = getCollection();
- return collection ? query_shape::CollectionType::collection
- : query_shape::CollectionType::nonExistent;
+ return collection ? query_shape::CollectionType::kCollection
+ : query_shape::CollectionType::kNonExistent;
}
diff --git a/src/mongo/db/index/SConscript b/src/mongo/db/index/SConscript
index 9f7366cbab5..81562c6abfe 100644
--- a/src/mongo/db/index/SConscript
+++ b/src/mongo/db/index/SConscript
@@ -72,8 +72,8 @@ iamEnv.Library(
'$BUILD_DIR/mongo/db/pipeline/document_path_support',
'$BUILD_DIR/mongo/db/query/collation/collator_factory_interface',
'$BUILD_DIR/mongo/db/query/collation/collator_interface',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/db/query/projection_ast',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/query/sort_pattern',
'$BUILD_DIR/mongo/db/query_expressions',
'$BUILD_DIR/mongo/db/record_id_helpers',
diff --git a/src/mongo/db/matcher/expression_path.h b/src/mongo/db/matcher/expression_path.h
index 296dc2dfbf6..611acbcb3e8 100644
--- a/src/mongo/db/matcher/expression_path.h
+++ b/src/mongo/db/matcher/expression_path.h
@@ -196,11 +196,12 @@ public:
}
void serialize(BSONObjBuilder* out, const SerializationOptions& opts) const override {
- auto&& rhs = getSerializedRightHandSide(opts);
if (opts.includePath) {
- out->append(opts.serializeFieldPathFromString(path()), rhs);
+ BSONObjBuilder subObj(out->subobjStart(opts.serializeFieldPathFromString(path())));
+ appendSerializedRightHandSide(&subObj, opts);
+ subObj.doneFast();
} else {
- out->appendElements(rhs);
+ appendSerializedRightHandSide(out, opts);
}
}
diff --git a/src/mongo/db/ops/SConscript b/src/mongo/db/ops/SConscript
index 7569e7382bf..9118a4854bd 100644
--- a/src/mongo/db/ops/SConscript
+++ b/src/mongo/db/ops/SConscript
@@ -21,7 +21,7 @@ env.Library(
'$BUILD_DIR/mongo/db/matcher/expressions_mongod_only',
'$BUILD_DIR/mongo/db/mongod_options',
'$BUILD_DIR/mongo/db/query/command_request_response',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/query_exec',
'$BUILD_DIR/mongo/db/repl/image_collection_entry',
'$BUILD_DIR/mongo/db/repl/repl_coordinator_interface',
@@ -60,7 +60,7 @@ env.Library(
'$BUILD_DIR/mongo/db/curop_metrics',
'$BUILD_DIR/mongo/db/dbhelpers',
'$BUILD_DIR/mongo/db/introspect',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/record_id_helpers',
'$BUILD_DIR/mongo/db/repl/oplog',
'$BUILD_DIR/mongo/db/repl/repl_coordinator_interface',
diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript
index 8b20e26017a..c849e850110 100644
--- a/src/mongo/db/pipeline/SConscript
+++ b/src/mongo/db/pipeline/SConscript
@@ -136,7 +136,7 @@ env.Library(
LIBDEPS=[
'$BUILD_DIR/mongo/db/exec/document_value/document_value',
'$BUILD_DIR/mongo/db/query/query_knobs',
- '$BUILD_DIR/mongo/db/query/stats/query_stats',
+ '$BUILD_DIR/mongo/db/query/stats/stats',
'$BUILD_DIR/mongo/db/query_expressions',
'$BUILD_DIR/mongo/scripting/scripting_common',
'$BUILD_DIR/mongo/util/summation',
@@ -504,8 +504,8 @@ env.Library(
LIBDEPS=[
'$BUILD_DIR/mongo/base',
'$BUILD_DIR/mongo/db/exec/document_value/document_value',
- '$BUILD_DIR/mongo/db/query/query_stats_parse',
- '$BUILD_DIR/mongo/db/serialization_options',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats_parse',
+ '$BUILD_DIR/mongo/db/query_shape_common',
'$BUILD_DIR/mongo/db/storage/key_string',
'$BUILD_DIR/mongo/db/timeseries/timeseries_options',
'$BUILD_DIR/mongo/idl/idl_parser',
diff --git a/src/mongo/db/pipeline/aggregate_command.idl b/src/mongo/db/pipeline/aggregate_command.idl
index bb6cad08b1b..3aa63c40a76 100644
--- a/src/mongo/db/pipeline/aggregate_command.idl
+++ b/src/mongo/db/pipeline/aggregate_command.idl
@@ -100,6 +100,10 @@ commands:
agg_stage: queryStats
resource_pattern: cluster
action_type: queryStatsRead
+ - privilege: # $queryStats
+ agg_stage: queryStats
+ resource_pattern: cluster
+ action_type: queryStatsReadTransformed
- privilege: # $changeStream
resource_pattern: exact_namespace
action_type: changeStream
diff --git a/src/mongo/db/pipeline/document_source_query_stats.cpp b/src/mongo/db/pipeline/document_source_query_stats.cpp
index 0b53f72448d..01bd1a87d84 100644
--- a/src/mongo/db/pipeline/document_source_query_stats.cpp
+++ b/src/mongo/db/pipeline/document_source_query_stats.cpp
@@ -35,18 +35,18 @@
#include "mongo/util/assert_util.h"
#include "mongo/util/debug_util.h"
-#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery
+#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQueryStats
namespace mongo {
namespace {
CounterMetric queryStatsHmacApplicationErrors("queryStats.numHmacApplicationErrors");
}
-REGISTER_DOCUMENT_SOURCE_WITH_FEATURE_FLAG(queryStats,
- DocumentSourceQueryStats::LiteParsed::parse,
- DocumentSourceQueryStats::createFromBson,
- AllowedWithApiStrict::kNeverInVersion1,
- feature_flags::gFeatureFlagQueryStats);
+// TODO SERVER-79494 Use REGISTER_DOCUMENT_SOURCE_WITH_FEATURE_FLAG
+REGISTER_DOCUMENT_SOURCE(queryStats,
+ DocumentSourceQueryStats::LiteParsed::parse,
+ DocumentSourceQueryStats::createFromBson,
+ AllowedWithApiStrict::kNeverInVersion1);
namespace {
@@ -71,9 +71,12 @@ auto parseSpec(const BSONElement& spec, const Ctor& ctor) {
if (transformIdentifiers) {
algorithm = transformIdentifiers->getAlgorithm();
boost::optional<ConstDataRange> hmacKeyContainer = transformIdentifiers->getHmacKey();
- if (hmacKeyContainer) {
- hmacKey = std::string(hmacKeyContainer->data(), (size_t)hmacKeyContainer->length());
- }
+ uassert(ErrorCodes::FailedToParse,
+ str::stream() << "The 'hmacKey' parameter of the $queryStats stage must be "
+ "specified when applying the hmac-sha-256 algorithm",
+ algorithm != TransformAlgorithmEnum::kHmacSha256 ||
+ hmacKeyContainer != boost::none);
+ hmacKey = std::string(hmacKeyContainer->data(), (size_t)hmacKeyContainer->length());
}
return ctor(algorithm, hmacKey);
}
@@ -97,6 +100,13 @@ std::vector<std::pair<size_t, QueryStatsEntry>> copyPartition(
std::unique_ptr<DocumentSourceQueryStats::LiteParsed> DocumentSourceQueryStats::LiteParsed::parse(
const NamespaceString& nss, const BSONElement& spec) {
+ // TODO SERVER-79494 Remove this manual feature flag check once we're registering doc source
+ // with REGISTER_DOCUMENT_SOURCE_WITH_FEATURE_FLAG
+ uassert(ErrorCodes::QueryFeatureNotAllowed,
+ "$queryStats is not allowed in the current configuration. You may need to enable the "
+ "correponding feature flag",
+ query_stats::isQueryStatsFeatureEnabled(/*requiresFullQueryStatsFeatureFlag*/ false));
+
return parseSpec(spec, [&](TransformAlgorithmEnum algorithm, std::string hmacKey) {
return std::make_unique<DocumentSourceQueryStats::LiteParsed>(
spec.fieldName(), algorithm, hmacKey);
@@ -105,12 +115,25 @@ std::unique_ptr<DocumentSourceQueryStats::LiteParsed> DocumentSourceQueryStats::
boost::intrusive_ptr<DocumentSource> DocumentSourceQueryStats::createFromBson(
BSONElement spec, const boost::intrusive_ptr<ExpressionContext>& pExpCtx) {
+ // TODO SERVER-79494 Remove this manual feature flag check once we're registering doc source
+ // with REGISTER_DOCUMENT_SOURCE_WITH_FEATURE_FLAG
+ uassert(ErrorCodes::QueryFeatureNotAllowed,
+ "$queryStats is not allowed in the current configuration. You may need to enable the "
+ "correponding feature flag",
+ query_stats::isQueryStatsFeatureEnabled(/*requiresFullQueryStatsFeatureFlag*/ false));
+
const NamespaceString& nss = pExpCtx->ns;
uassert(ErrorCodes::InvalidNamespace,
"$queryStats must be run against the 'admin' database with {aggregate: 1}",
nss.db() == DatabaseName::kAdmin.db() && nss.isCollectionlessAggregateNS());
+ LOGV2_DEBUG_OPTIONS(7808300,
+ 1,
+ {logv2::LogTruncation::Disabled},
+ "Logging invocation $queryStats",
+ "commandSpec"_attr =
+ spec.Obj().redact(BSONObj::RedactLevel::sensitiveOnly));
return parseSpec(spec, [&](TransformAlgorithmEnum algorithm, std::string hmacKey) {
return new DocumentSourceQueryStats(pExpCtx, algorithm, hmacKey);
});
@@ -145,12 +168,20 @@ DocumentSource::GetNextResult DocumentSourceQueryStats::doGetNext() {
* The inner iterator iterates over a materialized container of all entries in the partition.
* This is done to reduce the time under which the partition lock is held.
*/
+ bool shouldLog = _algorithm != TransformAlgorithmEnum::kNone;
while (true) {
// First, attempt to exhaust all elements in the materialized partition.
if (!_materializedPartition.empty()) {
// Move out of the container reference.
auto doc = std::move(_materializedPartition.front());
_materializedPartition.pop_front();
+ if (shouldLog) {
+ LOGV2_DEBUG_OPTIONS(7808301,
+ 3,
+ {logv2::LogTruncation::Disabled},
+ "Logging all outputs of $queryStats",
+ "thisOutput"_attr = doc);
+ }
return {std::move(doc)};
}
@@ -159,6 +190,12 @@ DocumentSource::GetNextResult DocumentSourceQueryStats::doGetNext() {
// Materialized partition is exhausted, move to the next.
_currentPartition++;
if (_currentPartition >= _queryStatsStore.numPartitions()) {
+ if (shouldLog) {
+ LOGV2_DEBUG_OPTIONS(7808302,
+ 3,
+ {logv2::LogTruncation::Disabled},
+ "Finished logging outout of $queryStats");
+ }
return DocumentSource::GetNextResult::makeEOF();
}
diff --git a/src/mongo/db/pipeline/document_source_query_stats.h b/src/mongo/db/pipeline/document_source_query_stats.h
index 5118a293cd2..da290d7c3e6 100644
--- a/src/mongo/db/pipeline/document_source_query_stats.h
+++ b/src/mongo/db/pipeline/document_source_query_stats.h
@@ -32,7 +32,7 @@
#include "mongo/db/pipeline/document_source.h"
#include "mongo/db/pipeline/document_source_query_stats_gen.h"
#include "mongo/db/pipeline/lite_parsed_document_source.h"
-#include "mongo/db/query/query_stats.h"
+#include "mongo/db/query/query_stats/query_stats.h"
#include "mongo/util/producer_consumer_queue.h"
namespace mongo {
@@ -59,8 +59,14 @@ public:
PrivilegeVector requiredPrivileges(bool isMongos,
bool bypassDocumentValidation) const override {
- return {Privilege(ResourcePattern::forClusterResource(), ActionType::queryStatsRead)};
- ;
+ return _algorithm == TransformAlgorithmEnum::kNone
+
+ ? PrivilegeVector{Privilege(ResourcePattern::forClusterResource(),
+ ActionType::queryStatsReadTransformed),
+ Privilege(ResourcePattern::forClusterResource(),
+ ActionType::queryStatsRead)}
+ : PrivilegeVector{Privilege(ResourcePattern::forClusterResource(),
+ ActionType::queryStatsReadTransformed)};
}
bool allowedToPassthroughFromMongos() const final {
diff --git a/src/mongo/db/pipeline/document_source_query_stats.idl b/src/mongo/db/pipeline/document_source_query_stats.idl
index 025e3e956d2..e7d18e578ad 100644
--- a/src/mongo/db/pipeline/document_source_query_stats.idl
+++ b/src/mongo/db/pipeline/document_source_query_stats.idl
@@ -31,7 +31,7 @@ global:
- "mongo/db/pipeline/document_source_query_stats_validators.h"
imports:
- "mongo/db/basic_types.idl"
- - "mongo/db/query/query_stats_transform_algorithm.idl"
+ - "mongo/db/query/query_stats/transform_algorithm.idl"
structs:
TransformIdentifiersSpec:
diff --git a/src/mongo/db/pipeline/document_source_query_stats_test.cpp b/src/mongo/db/pipeline/document_source_query_stats_test.cpp
index 94c5dddee35..0fc946f6a1b 100644
--- a/src/mongo/db/pipeline/document_source_query_stats_test.cpp
+++ b/src/mongo/db/pipeline/document_source_query_stats_test.cpp
@@ -34,6 +34,7 @@
#include "mongo/db/exec/document_value/document_value_test_util.h"
#include "mongo/db/pipeline/aggregation_context_fixture.h"
#include "mongo/db/pipeline/document_source_query_stats.h"
+#include "mongo/idl/server_parameter_test_util.h"
#include "mongo/unittest/unittest.h"
#include "mongo/util/str.h"
@@ -54,6 +55,7 @@ public:
};
TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfSpecIsNotObject) {
+ RAIIServerParameterControllerForTest queryStatsFeatureFlag{"featureFlagQueryStats", true};
ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson(
fromjson("{$queryStats: 1}").firstElement(), getExpCtx()),
AssertionException,
@@ -61,6 +63,7 @@ TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfSpecIsNotObject) {
}
TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfNotRunOnAdmin) {
+ RAIIServerParameterControllerForTest queryStatsFeatureFlag{"featureFlagQueryStats", true};
getExpCtx()->ns =
NamespaceString::makeCollectionlessAggregateNSS(DatabaseName(boost::none, "foo"));
ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson(
@@ -70,6 +73,7 @@ TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfNotRunOnAdmin) {
}
TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfNotRunWithAggregateOne) {
+ RAIIServerParameterControllerForTest queryStatsFeatureFlag{"featureFlagQueryStats", true};
getExpCtx()->ns = NamespaceString::createNamespaceString_forTest("admin.foo");
ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson(
fromjson("{$queryStats: {}}").firstElement(), getExpCtx()),
@@ -78,6 +82,7 @@ TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfNotRunWithAggregateOne)
}
TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfUnrecognisedParameterSpecified) {
+ RAIIServerParameterControllerForTest queryStatsFeatureFlag{"featureFlagQueryStats", true};
ASSERT_THROWS_CODE(DocumentSourceQueryStats::createFromBson(
fromjson("{$queryStats: {foo: true}}").firstElement(), getExpCtx()),
AssertionException,
@@ -85,6 +90,7 @@ TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfUnrecognisedParameterSpe
}
TEST_F(DocumentSourceQueryStatsTest, ParseAndSerialize) {
+ RAIIServerParameterControllerForTest queryStatsFeatureFlag{"featureFlagQueryStats", true};
const auto obj = fromjson("{$queryStats: {}}");
const auto doc = DocumentSourceQueryStats::createFromBson(obj.firstElement(), getExpCtx());
const auto queryStatsOp = static_cast<DocumentSourceQueryStats*>(doc.get());
@@ -99,6 +105,7 @@ TEST_F(DocumentSourceQueryStatsTest, ParseAndSerialize) {
}
TEST_F(DocumentSourceQueryStatsTest, ParseAndSerializeShouldIncludeHmacKey) {
+ RAIIServerParameterControllerForTest queryStatsFeatureFlag{"featureFlagQueryStats", true};
const auto obj = fromjson(R"({
$queryStats: {
transformIdentifiers: {
@@ -130,6 +137,7 @@ TEST_F(DocumentSourceQueryStatsTest, ParseAndSerializeShouldIncludeHmacKey) {
}
TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfAlgorithmIsNotSupported) {
+ RAIIServerParameterControllerForTest queryStatsFeatureFlag{"featureFlagQueryStats", true};
auto obj = fromjson(R"({
$queryStats: {
transformIdentifiers: {
@@ -144,6 +152,7 @@ TEST_F(DocumentSourceQueryStatsTest, ShouldFailToParseIfAlgorithmIsNotSupported)
TEST_F(DocumentSourceQueryStatsTest,
ShouldFailToParseIfTransformIdentifiersSpecifiedButEmptyAlgorithm) {
+ RAIIServerParameterControllerForTest queryStatsFeatureFlag{"featureFlagQueryStats", true};
auto obj = fromjson(R"({
$queryStats: {
transformIdentifiers: {
@@ -158,6 +167,7 @@ TEST_F(DocumentSourceQueryStatsTest,
TEST_F(DocumentSourceQueryStatsTest,
ShouldFailToParseIfTransformIdentifiersSpecifiedButNoAlgorithm) {
+ RAIIServerParameterControllerForTest queryStatsFeatureFlag{"featureFlagQueryStats", true};
auto obj = fromjson(R"({
$queryStats: {
transformIdentifiers: {
diff --git a/src/mongo/db/pipeline/document_source_query_stats_validators.cpp b/src/mongo/db/pipeline/document_source_query_stats_validators.cpp
index 8a4980c6297..8a3f234e325 100644
--- a/src/mongo/db/pipeline/document_source_query_stats_validators.cpp
+++ b/src/mongo/db/pipeline/document_source_query_stats_validators.cpp
@@ -30,7 +30,7 @@
#include "mongo/platform/basic.h"
#include "mongo/db/pipeline/document_source_query_stats_validators.h"
-#include "mongo/db/query/query_stats_transform_algorithm_gen.h"
+#include "mongo/db/query/query_stats/transform_algorithm_gen.h"
#include "mongo/util/str.h"
#include <set>
diff --git a/src/mongo/db/pipeline/document_source_query_stats_validators.h b/src/mongo/db/pipeline/document_source_query_stats_validators.h
index 412eff10cee..077552da0b3 100644
--- a/src/mongo/db/pipeline/document_source_query_stats_validators.h
+++ b/src/mongo/db/pipeline/document_source_query_stats_validators.h
@@ -30,7 +30,7 @@
#pragma once
#include "mongo/base/status.h"
-#include "mongo/db/query/query_stats_transform_algorithm_gen.h"
+#include "mongo/db/query/query_stats/transform_algorithm_gen.h"
namespace mongo {
/**
diff --git a/src/mongo/db/query/SConscript b/src/mongo/db/query/SConscript
index a66dcde06a3..920fcb5dd34 100644
--- a/src/mongo/db/query/SConscript
+++ b/src/mongo/db/query/SConscript
@@ -14,6 +14,7 @@ env.SConscript(
'cost_model',
'datetime',
'optimizer',
+ 'query_stats',
'stats',
],
exports=[
@@ -275,8 +276,8 @@ env.Library(
'framework_control.cpp',
'query_feature_flags.idl',
'query_knobs.idl',
+ 'query_stats/util.cpp',
'sbe_plan_cache_on_parameter_change.cpp',
- 'query_stats_util.cpp',
],
LIBDEPS_PRIVATE=[
'$BUILD_DIR/mongo/db/commands/test_commands_enabled',
@@ -364,23 +365,11 @@ env.Library(
)
env.Library(
- target='rate_limiting',
- source=[
- 'rate_limiting.cpp',
- ],
- LIBDEPS=[
- '$BUILD_DIR/mongo/base',
- '$BUILD_DIR/mongo/util/clock_sources',
- ],
-)
-
-env.Library(
target='query_shape',
source=[
'query_shape.cpp',
- 'query_shape.idl',
- 'query_stats_find_key_generator.cpp',
- 'query_stats_aggregate_key_generator.cpp',
+ 'query_stats/find_key_generator.cpp',
+ 'query_stats/aggregate_key_generator.cpp',
],
LIBDEPS=[
'$BUILD_DIR/mongo/base',
@@ -390,44 +379,12 @@ env.Library(
'canonical_query',
],
LIBDEPS_PRIVATE=[
+ '$BUILD_DIR/mongo/db/query_shape_common',
'projection_ast',
'sort_pattern',
],
)
-env.Library(target='query_stats_parse', source=['query_stats_transform_algorithm.idl'], LIBDEPS=[
- '$BUILD_DIR/mongo/base',
- '$BUILD_DIR/mongo/idl/idl_parser',
-])
-
-env.Library(
- target='op_metrics',
- source=['query_stats.cpp'],
- LIBDEPS=[
- '$BUILD_DIR/mongo/base',
- '$BUILD_DIR/mongo/bson/mutable/mutable_bson',
- '$BUILD_DIR/mongo/db/commands',
- '$BUILD_DIR/mongo/db/profile_filter',
- '$BUILD_DIR/mongo/db/server_options',
- '$BUILD_DIR/mongo/db/service_context',
- '$BUILD_DIR/mongo/db/stats/counters',
- '$BUILD_DIR/mongo/db/storage/storage_engine_parameters',
- '$BUILD_DIR/mongo/rpc/client_metadata',
- '$BUILD_DIR/mongo/util/fail_point',
- '$BUILD_DIR/mongo/util/net/network',
- '$BUILD_DIR/mongo/util/processinfo',
- 'command_request_response',
- 'memory_util',
- 'query_knobs',
- 'query_stats_parse',
- 'rate_limiting',
- ],
- LIBDEPS_PRIVATE=[
- '$BUILD_DIR/mongo/db/auth/auth',
- '$BUILD_DIR/mongo/util/namespace_string_database_name_util',
- ],
-)
-
env.CppUnitTest(
target="db_query_test",
source=[
@@ -483,9 +440,7 @@ env.CppUnitTest(
"query_settings_test.cpp",
"query_shape_test.cpp",
"query_shape_test.idl",
- "query_stats_store_test.cpp",
"query_solution_test.cpp",
- "rate_limiting_test.cpp",
"sbe_and_hash_test.cpp",
"sbe_and_sorted_test.cpp",
"sbe_shard_filter_test.cpp",
@@ -529,7 +484,6 @@ env.CppUnitTest(
"query_request",
"query_shape",
"query_test_service_context",
- "rate_limiting",
],
)
diff --git a/src/mongo/db/query/lru_key_value.h b/src/mongo/db/query/lru_key_value.h
index 3bb26e95f20..2786e1c40ab 100644
--- a/src/mongo/db/query/lru_key_value.h
+++ b/src/mongo/db/query/lru_key_value.h
@@ -39,18 +39,42 @@
namespace mongo {
/**
+ * 'InsertionEvictionListener' class to use with 'LRUBudgetTracker' that will always noop.
+ */
+class NoopInsertionEvictionListener {
+public:
+ // Called when a key-value pair is being inserted. Parameters are the key-value pair and its
+ // estimated size.
+ template <class K, class V>
+ void onInsert(const K&, const V&, size_t) {}
+
+ // Called when a key-value pair is being evicted. Parameters are the key-value pair and its
+ // estimated size.
+ template <class K, class V>
+ void onEvict(const K&, const V&, size_t) {}
+
+ // Called when the cache is being cleared. Parameter is the estimated size of the key-value
+ // pairs in the cache before it was cleared.
+ void onClear(size_t) {}
+};
+
+/**
* This class tracks a size of entries in 'LRUKeyValue'.
* The size can be understood as a number of the entries, an amount of memory they occupied,
* or any other value defined by the template parameter 'Estimator'.
* The 'Estimator' must be deterministic and always return the same value for the same entry.
+ * The 'InsertionEvictionListener' will be called on every insertion and eviction as well as when
+ * the cache is cleared.
*/
-template <class K, class V, typename Estimator>
+template <class K, class V, typename Estimator, typename InsertionEvictionListener>
class LRUBudgetTracker {
public:
LRUBudgetTracker(size_t maxBudget) : _max(maxBudget), _current(0) {}
void onAdd(const K& k, const V& v) {
- _current += _estimator(k, v);
+ size_t budget = _estimator(k, v);
+ _current += budget;
+ _listener.onInsert(k, v, budget);
}
void onRemove(const K& k, const V& v) {
@@ -60,9 +84,11 @@ public:
"LRU budget underflow: current={}, budget={} "_format(_current, budget),
_current >= budget);
_current -= budget;
+ _listener.onEvict(k, v, budget);
}
void onClear() {
+ _listener.onClear(_current);
_current = 0;
}
@@ -83,6 +109,7 @@ private:
size_t _max;
size_t _current;
Estimator _estimator;
+ InsertionEvictionListener _listener;
};
/**
@@ -90,6 +117,9 @@ private:
* policy. The size allowed in the kv-store is controlled by 'LRUBudgetTracker'
* set in the constructor.
*
+ * An 'InsertionEvictionListener' may optionally be specified to track the insertion and eviction of
+ * each key-value pair.
+ *
* Caveat:
* This kv-store is NOT thread safe! The client to this utility is responsible
* for protecting concurrent access to the LRU store if used in a threaded
@@ -104,6 +134,7 @@ private:
template <class K,
class V,
class KeyValueBudgetEstimator,
+ class InsertionEvictionListener = NoopInsertionEvictionListener,
class KeyHasher = std::hash<K>,
class Eq = std::equal_to<K>>
class LRUKeyValue {
@@ -273,7 +304,7 @@ private:
return nEvicted;
}
- LRUBudgetTracker<K, V, KeyValueBudgetEstimator> _budgetTracker;
+ LRUBudgetTracker<K, V, KeyValueBudgetEstimator, InsertionEvictionListener> _budgetTracker;
// (K, V) pairs are stored in this std::list. They are sorted in order of use, where the front
// is the most recently used and the back is the least recently used.
diff --git a/src/mongo/db/query/lru_key_value_test.cpp b/src/mongo/db/query/lru_key_value_test.cpp
index 8d767021543..6dcdfc5a4ea 100644
--- a/src/mongo/db/query/lru_key_value_test.cpp
+++ b/src/mongo/db/query/lru_key_value_test.cpp
@@ -95,8 +95,40 @@ struct NonTrivialBudgetEstimator {
using NonTrivialTestSharedPtrValue =
LRUKeyValue<size_t, std::shared_ptr<NonTrivialEntry>, NonTrivialBudgetEstimator>;
-template <typename Key, typename Value, typename Estimator>
-void assertInKVStore(LRUKeyValue<Key, Value, Estimator>& cache, Key key, Value value) {
+class NonTrivialInsertionEvictionListener {
+public:
+ NonTrivialInsertionEvictionListener() {
+ keyTotal = 0;
+ valueTotal = 0;
+ budgetTotal = 0;
+ }
+
+ void onInsert(const int& k, const ValueType& v, size_t budget) {
+ keyTotal += k;
+ valueTotal += v.val;
+ budgetTotal += budget;
+ }
+
+ void onEvict(const int& k, const ValueType& v, size_t budget) {
+ keyTotal -= k;
+ valueTotal -= v.val;
+ budgetTotal -= budget;
+ }
+
+ void onClear(size_t budget) {
+ budgetTotal -= budget;
+ }
+
+ static size_t keyTotal;
+ static size_t valueTotal;
+ static size_t budgetTotal;
+};
+size_t NonTrivialInsertionEvictionListener::keyTotal;
+size_t NonTrivialInsertionEvictionListener::valueTotal;
+size_t NonTrivialInsertionEvictionListener::budgetTotal;
+
+template <typename Key, typename Value, typename Estimator, typename Listener>
+void assertInKVStore(LRUKeyValue<Key, Value, Estimator, Listener>& cache, Key key, Value value) {
ASSERT_TRUE(cache.hasKey(key));
auto s = cache.get(key);
ASSERT(s.isOK());
@@ -105,8 +137,8 @@ void assertInKVStore(LRUKeyValue<Key, Value, Estimator>& cache, Key key, Value v
ASSERT_EQUALS(*(kvItr->second), *value);
}
-template <typename Key, typename Value, typename Estimator>
-void assertNotInKVStore(LRUKeyValue<Key, Value, Estimator>& cache, Key key) {
+template <typename Key, typename Value, typename Estimator, typename Listener>
+void assertNotInKVStore(LRUKeyValue<Key, Value, Estimator, Listener>& cache, Key key) {
ASSERT_FALSE(cache.hasKey(key));
auto s = cache.get(key);
ASSERT(!s.isOK());
@@ -356,7 +388,8 @@ TEST(LRUKeyValueTest, UniquePtrKeyValue) {
assertNotInKVStore(cacheForEviction, 1); // The entry with key '1' has been Evicted.
}
-using TestScalarValue = LRUKeyValue<int, ValueType, TrivialBudgetEstimator>;
+using TestScalarValue =
+ LRUKeyValue<int, ValueType, TrivialBudgetEstimator, NonTrivialInsertionEvictionListener>;
void assertValueInKVStore(TestScalarValue& cache, int key, ValueType value) {
ASSERT_TRUE(cache.hasKey(key));
@@ -373,9 +406,17 @@ TEST(LRUKeyValueTest, ScalarKeyValue) {
assertValueInKVStore(cache, 1, ValueType{2});
assertNotInKVStore(cache, 3);
+ ASSERT_EQUALS(NonTrivialInsertionEvictionListener::keyTotal, 1);
+ ASSERT_EQUALS(NonTrivialInsertionEvictionListener::valueTotal, 2);
+ ASSERT_EQUALS(NonTrivialInsertionEvictionListener::budgetTotal, 1);
+
cache.add(1, ValueType{3});
assertValueInKVStore(cache, 1, ValueType{3});
+ ASSERT_EQUALS(NonTrivialInsertionEvictionListener::keyTotal, 1);
+ ASSERT_EQUALS(NonTrivialInsertionEvictionListener::valueTotal, 3);
+ ASSERT_EQUALS(NonTrivialInsertionEvictionListener::budgetTotal, 1);
+
// Test eviction.
TestScalarValue cacheForEviction{2};
cacheForEviction.add(1, ValueType{1});
@@ -384,6 +425,18 @@ TEST(LRUKeyValueTest, ScalarKeyValue) {
ASSERT_EQUALS(cacheForEviction.size(), static_cast<size_t>(2));
assertNotInKVStore(cacheForEviction, 1); // The entry with key '1' has been Evicted.
+
+ ASSERT_EQUALS(NonTrivialInsertionEvictionListener::keyTotal, 5);
+ ASSERT_EQUALS(NonTrivialInsertionEvictionListener::valueTotal, 5);
+ ASSERT_EQUALS(NonTrivialInsertionEvictionListener::budgetTotal, 2);
+
+ // Clear the remaining values.
+ cacheForEviction.clear();
+
+ assertNotInKVStore(cacheForEviction, 2); // The entry with key '2' has been Evicted.
+ assertNotInKVStore(cacheForEviction, 3); // The entry with key '3' has been Evicted.
+
+ ASSERT_EQUALS(NonTrivialInsertionEvictionListener::budgetTotal, 0);
}
} // namespace
diff --git a/src/mongo/db/query/partitioned_cache.h b/src/mongo/db/query/partitioned_cache.h
index 87f622373d2..ca10f731b71 100644
--- a/src/mongo/db/query/partitioned_cache.h
+++ b/src/mongo/db/query/partitioned_cache.h
@@ -45,6 +45,7 @@ template <class KeyType,
class ValueType,
class KeyBudgetEstimator,
class Partitioner,
+ class InsertionEvictionListener,
class KeyHasher = std::hash<KeyType>,
class Eq = std::equal_to<KeyType>>
class PartitionedCache {
@@ -53,12 +54,49 @@ private:
PartitionedCache& operator=(const PartitionedCache&) = delete;
public:
- using Lru = LRUKeyValue<KeyType, ValueType, KeyBudgetEstimator, KeyHasher, Eq>;
+ using Lru = LRUKeyValue<KeyType,
+ ValueType,
+ KeyBudgetEstimator,
+ InsertionEvictionListener,
+ KeyHasher,
+ Eq>;
using Partition = typename Partitioned<Lru, Partitioner>::OnePartition;
using PartitionId = typename Partitioned<Lru, Partitioner>::PartitionId;
/**
* Initialize plan cache with the total cache size in bytes and number of partitions.
+ *
+ * Important edge cases to consider include:
+ *
+ * 1. Adding an entry that is larger than the max partition size to a non-empty partition.
+ *
+ * This will evict both entries. This is because entries are evicted from the partition in
+ * order of least recently used. Thus, the oldest, small entry will be evicted first but the
+ * partition will still be over budget with the new, too-large entry so it will be evicted as
+ * well.
+ *
+ * 2. Adding a queryStats store entry that is smaller than the overall cache size but larger
+ * than single partition max size.
+ *
+ * It is not possible to write entries to the cache that are larger than a single
+ * partition's max size, even if it is smaller than the entire cache max size. This is because
+ * the cache's budget is configured/regulated on the partition level (cacheSize /
+ * numPartitions). This makes sense as each entry is written to a specific partition, but might
+ * not be immediately obvious so worthy to highlight.
+ *
+ * 3. Too few partitions can cause unnecessary evictions
+ *
+ * Every class that implements the PartitionedCache template provides a partitioner() that
+ * returns the id of the partition to which to write the entry. In existing implementations,
+ * partitioner() returns the remainder after dividing the entry's key hash by numPartitions. In
+ * the case where we have only two partitions, every odd key hash will be written to the first
+ * partition (and vice versa). In this way, it can quickly be the case that one partition
+ * fills up completely but the partitioner() call keeps returning the already full partition and
+ * the cache evict old entries from it to put the new one in. At the end of all the write
+ * operations, the cache is below it's budget (as the second partition is only partially full)
+ * but we don't have all the entries we expect. It is therefore important to have sufficient
+ * enough number of partitions so the entries can be more equally dispersed to avoid unnecessary
+ * evictions.
*/
explicit PartitionedCache(size_t cacheSize, size_t numPartitions)
: _numPartitions(numPartitions) {
diff --git a/src/mongo/db/query/plan_cache.h b/src/mongo/db/query/plan_cache.h
index 04b2cf118f9..6f7ca15145a 100644
--- a/src/mongo/db/query/plan_cache.h
+++ b/src/mongo/db/query/plan_cache.h
@@ -327,6 +327,7 @@ class PlanCacheBase
std::shared_ptr<const PlanCacheEntryBase<CachedPlanType, DebugInfoType>>,
KeyBudgetEstimator,
Partitioner,
+ NoopInsertionEvictionListener,
KeyHasher> {
private:
PlanCacheBase(const PlanCacheBase&) = delete;
@@ -338,6 +339,7 @@ public:
std::shared_ptr<const PlanCacheEntryBase<CachedPlanType, DebugInfoType>>,
KeyBudgetEstimator,
Partitioner,
+ NoopInsertionEvictionListener,
KeyHasher>;
using Entry = PlanCacheEntryBase<CachedPlanType, DebugInfoType>;
diff --git a/src/mongo/db/query/query_feature_flags.idl b/src/mongo/db/query/query_feature_flags.idl
index f726b0bcf4a..0b701e5a311 100644
--- a/src/mongo/db/query/query_feature_flags.idl
+++ b/src/mongo/db/query/query_feature_flags.idl
@@ -82,7 +82,7 @@ feature_flags:
default: false
featureFlagQueryStats:
- description: "Feature flag for enabling the queryStats store."
+ description: "Feature flag for enabling full queryStats collection."
cpp_varname: gFeatureFlagQueryStats
default: false
@@ -117,6 +117,11 @@ feature_flags:
default: true
version: 7.0
+ featureFlagQueryStatsFindCommand:
+ description: "Feature flag for enabling queryStats collection for the find command."
+ cpp_varname: gFeatureFlagQueryStatsFindCommand
+ default: false
+
featureFlagSearchBatchSizeLimit:
description: "Feature flag to enable the search batchsize and limit optimization."
cpp_varname: gFeatureFlagSearchBatchSizeLimit
diff --git a/src/mongo/db/query/query_knobs.idl b/src/mongo/db/query/query_knobs.idl
index 4cc34456274..330462550e5 100644
--- a/src/mongo/db/query/query_knobs.idl
+++ b/src/mongo/db/query/query_knobs.idl
@@ -36,7 +36,7 @@ global:
- "mongo/db/query/ce_mode_parameter.h"
- "mongo/db/query/explain_version_validator.h"
- "mongo/db/query/sbe_plan_cache_on_parameter_change.h"
- - "mongo/db/query/query_stats_util.h"
+ - "mongo/db/query/query_stats/util.h"
- "mongo/platform/atomic_proxy.h"
- "mongo/platform/atomic_word.h"
diff --git a/src/mongo/db/query/query_shape.cpp b/src/mongo/db/query/query_shape.cpp
index cb94a23490f..90860f3eb23 100644
--- a/src/mongo/db/query/query_shape.cpp
+++ b/src/mongo/db/query/query_shape.cpp
@@ -34,6 +34,7 @@
#include "mongo/db/query/projection_ast_util.h"
#include "mongo/db/query/query_request_helper.h"
#include "mongo/db/query/query_shape_gen.h"
+#include "mongo/db/query/shape_helpers.h"
#include "mongo/db/query/sort_pattern.h"
namespace mongo::query_shape {
@@ -100,7 +101,6 @@ BSONObj extractSortShape(const BSONObj& sortSpec,
return bob.obj();
}
-static std::string hintSpecialField = "$hint";
void addShapeLiterals(BSONObjBuilder* bob,
const FindCommandRequest& findCommand,
const SerializationOptions& opts) {
@@ -144,34 +144,6 @@ void addRemainingFindCommandFields(BSONObjBuilder* bob,
}
}
-BSONObj extractHintShape(BSONObj obj, const SerializationOptions& opts, bool preserveValue) {
- BSONObjBuilder bob;
- for (BSONElement elem : obj) {
- if (hintSpecialField.compare(elem.fieldName()) == 0) {
- if (elem.type() == BSONType::String) {
- bob.append(hintSpecialField, opts.serializeFieldPathFromString(elem.String()));
- } else if (elem.type() == BSONType::Object) {
- opts.appendLiteral(&bob, hintSpecialField, elem.Obj());
- } else {
- uasserted(ErrorCodes::FailedToParse, "$hint must be a string or an object");
- }
- continue;
- }
-
- // $natural doesn't need to be redacted.
- if (elem.fieldNameStringData().compare(query_request_helper::kNaturalSortField) == 0) {
- bob.append(elem);
- continue;
- }
-
- if (preserveValue) {
- bob.appendAs(elem, opts.serializeFieldPathFromString(elem.fieldName()));
- } else {
- opts.appendLiteral(&bob, opts.serializeFieldPathFromString(elem.fieldName()), elem);
- }
- }
- return bob.obj();
-}
/**
* In a let specification all field names are variable names, and all values are either expressions
@@ -195,20 +167,10 @@ void appendCmdNs(BSONObjBuilder& bob,
const NamespaceString& nss,
const SerializationOptions& opts) {
BSONObjBuilder nsObj = bob.subobjStart("cmdNs");
- appendNamespaceShape(nsObj, nss, opts);
+ shape_helpers::appendNamespaceShape(nsObj, nss, opts);
nsObj.doneFast();
}
-void appendNamespaceShape(BSONObjBuilder& bob,
- const NamespaceString& nss,
- const SerializationOptions& opts) {
- if (nss.tenantId()) {
- bob.append("tenantId", opts.serializeIdentifier(nss.tenantId().value().toString()));
- }
- bob.append("db", opts.serializeIdentifier(nss.db()));
- bob.append("coll", opts.serializeIdentifier(nss.coll()));
-}
-
BSONObj extractQueryShape(const ParsedFindCommand& findRequest,
const SerializationOptions& opts,
const boost::intrusive_ptr<ExpressionContext>& expCtx) {
@@ -231,7 +193,6 @@ BSONObj extractQueryShape(const ParsedFindCommand& findRequest,
std::unique_ptr<MatchExpression> filterExpr;
// Filter.
bob.append(FindCommandRequest::kFilterFieldName, findRequest.filter->serialize(opts));
-
// Let Spec.
if (auto letSpec = findCmd.getLet()) {
auto redactedObj = extractLetSpecShape(letSpec.get(), opts, expCtx);
@@ -244,21 +205,13 @@ BSONObj extractQueryShape(const ParsedFindCommand& findRequest,
projection_ast::serialize(*findRequest.proj->root(), opts));
}
- // Assume the hint is correct and contains field names. It is possible that this hint
- // doesn't actually represent an index, but we can't detect that here.
- // Hint, max, and min won't serialize if the object is empty.
- if (!findCmd.getHint().isEmpty()) {
- bob.append(FindCommandRequest::kHintFieldName,
- extractHintShape(findCmd.getHint(), opts, true));
- // Max/Min aren't valid without hint.
- if (!findCmd.getMax().isEmpty()) {
- bob.append(FindCommandRequest::kMaxFieldName,
- extractHintShape(findCmd.getMax(), opts, false));
- }
- if (!findCmd.getMin().isEmpty()) {
- bob.append(FindCommandRequest::kMinFieldName,
- extractHintShape(findCmd.getMin(), opts, false));
- }
+ if (!findCmd.getMax().isEmpty()) {
+ bob.append(FindCommandRequest::kMaxFieldName,
+ shape_helpers::extractMinOrMaxShape(findCmd.getMax(), opts));
+ }
+ if (!findCmd.getMin().isEmpty()) {
+ bob.append(FindCommandRequest::kMinFieldName,
+ shape_helpers::extractMinOrMaxShape(findCmd.getMin(), opts));
}
// Sort.
@@ -316,12 +269,6 @@ BSONObj extractQueryShape(const AggregateCommandRequest& aggregateCommand,
bob.append(AggregateCommandRequest::kCollationFieldName, param.get());
}
- // hint
- if (auto hint = aggregateCommand.getHint()) {
- bob.append(AggregateCommandRequest::kHintFieldName,
- extractHintShape(hint.get(), opts, true));
- }
-
// let
if (auto letSpec = aggregateCommand.getLet()) {
auto redactedObj = extractLetSpecShape(letSpec.get(), opts, expCtx);
@@ -331,26 +278,6 @@ BSONObj extractQueryShape(const AggregateCommandRequest& aggregateCommand,
return bob.obj();
}
-NamespaceStringOrUUID parseNamespaceShape(BSONElement cmdNsElt) {
- tassert(7632900, "cmdNs must be an object.", cmdNsElt.type() == BSONType::Object);
- auto cmdNs = CommandNamespace::parse(IDLParserContext("cmdNs"), cmdNsElt.embeddedObject());
-
- boost::optional<TenantId> tenantId = cmdNs.getTenantId().map(TenantId::parseFromString);
-
- if (cmdNs.getColl().has_value()) {
- tassert(7632903,
- "Exactly one of 'uuid' and 'coll' can be defined.",
- !cmdNs.getUuid().has_value());
- return NamespaceString(cmdNs.getDb(), cmdNs.getColl().value());
- } else {
- tassert(7632904,
- "Exactly one of 'uuid' and 'coll' can be defined.",
- !cmdNs.getColl().has_value());
- UUID uuid = uassertStatusOK(UUID::parse(cmdNs.getUuid().value().toString()));
- return NamespaceStringOrUUID(cmdNs.getDb().toString(), uuid, tenantId);
- }
-}
-
QueryShapeHash hash(const BSONObj& queryShape) {
return QueryShapeHash::computeHash(reinterpret_cast<const uint8_t*>(queryShape.objdata()),
queryShape.objsize());
diff --git a/src/mongo/db/query/query_shape.h b/src/mongo/db/query/query_shape.h
index 9415b7387ce..ca699019523 100644
--- a/src/mongo/db/query/query_shape.h
+++ b/src/mongo/db/query/query_shape.h
@@ -37,6 +37,7 @@
namespace mongo::query_shape {
using QueryShapeHash = SHA256Block;
+
/**
* Computes a BSONObj that is meant to be used to classify queries according to their shape, for the
* purposes of collecting queryStats.
@@ -73,10 +74,5 @@ BSONObj extractQueryShape(const AggregateCommandRequest& aggregateCommand,
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const NamespaceString& nss);
-NamespaceStringOrUUID parseNamespaceShape(BSONElement cmdNsElt);
-void appendNamespaceShape(BSONObjBuilder& bob,
- const NamespaceString& nss,
- const SerializationOptions& opts);
-
QueryShapeHash hash(const BSONObj& queryShape);
} // namespace mongo::query_shape
diff --git a/src/mongo/db/query/query_shape_test.cpp b/src/mongo/db/query/query_shape_test.cpp
index 059495e9797..b6b5097f3e6 100644
--- a/src/mongo/db/query/query_shape_test.cpp
+++ b/src/mongo/db/query/query_shape_test.cpp
@@ -35,6 +35,7 @@
#include "mongo/db/pipeline/expression_context_for_test.h"
#include "mongo/db/query/query_shape.h"
#include "mongo/db/query/query_shape_test_gen.h"
+#include "mongo/db/query/shape_helpers.h"
#include "mongo/unittest/bson_test_util.h"
#include "mongo/unittest/unittest.h"
@@ -204,13 +205,16 @@ TEST(QueryPredicateShape, Exists) {
}
TEST(QueryPredicateShape, In) {
- // Any number of children is always the same shape
+ // Any number of children in any order is always the same shape
ASSERT_SHAPE_EQ_AUTO( // NOLINT
R"({"a":{"$in":"?array<?number>"}})",
"{a: {$in: [1]}}");
ASSERT_SHAPE_EQ_AUTO( // NOLINT
R"({"a":{"$in":"?array<>"}})",
"{a: {$in: [1, 4, 'str', /regex/]}}");
+ ASSERT_SHAPE_EQ_AUTO( // NOLINT
+ R"({"a":{"$in":"?array<>"}})",
+ "{a: {$in: ['str', /regex/, 1, 4]}}");
}
TEST(QueryPredicateShape, BitTestOperators) {
diff --git a/src/mongo/db/query/query_stats/README.md b/src/mongo/db/query/query_stats/README.md
new file mode 100644
index 00000000000..c6e9a164f6c
--- /dev/null
+++ b/src/mongo/db/query/query_stats/README.md
@@ -0,0 +1,28 @@
+# Query Stats
+This directory is the home of the infrastructure related to recording query statistics for the
+database. It is not to be confused with src/mongo/db/query/stats/ which is the home of the logic for
+computing and maintaining statistics about a collection or index's data distribution - for use by
+the query planner.
+
+At the center of everything here is the QueryStatsStore, which essentially maps query shapes to some
+metrics about how often each one occurs. For example, if the client does this:
+```js
+db.example.findOne({x: 24});
+db.example.findOne({x: 53});
+```
+then the QueryStatsStore should contain an entry for a single query shape which would record 2
+executions and some related statistics (see the QueryStatsEntry for details).
+
+The query stats store actually uses _more_ dimensions (i.e. more granularity) to group incoming
+queries than just the query shape. For example, these queries would all three have the same shape
+but the first would have a different query stats store entry from the other two:
+```js
+db.example.find({x: 55});
+db.example.find({x: 55}).batchSize(2);
+db.example.find({x: 55}).batchSize(3);
+```
+There are two distinct query stats store entries here - both the examples which include the batch
+size will be treated separately from the example which does not specify a batch size.
+
+The dimensions considered will depend on the command, but can generally be found in the KeyGenerator
+iterface, which will generate the QueryStatsStore keys by which we accumulate statistics.
diff --git a/src/mongo/db/query/query_stats/SConscript b/src/mongo/db/query/query_stats/SConscript
new file mode 100644
index 00000000000..0a478733d7b
--- /dev/null
+++ b/src/mongo/db/query/query_stats/SConscript
@@ -0,0 +1,102 @@
+# -*- mode: python -*-
+
+Import([
+ "env",
+ "get_option",
+])
+
+env = env.Clone()
+
+env.Library(
+ target='rate_limiting',
+ source=[
+ 'rate_limiting.cpp',
+ ],
+ LIBDEPS=[
+ '$BUILD_DIR/mongo/base',
+ '$BUILD_DIR/mongo/util/clock_sources',
+ ],
+)
+
+env.Library(target='query_stats_parse', source=['transform_algorithm.idl'], LIBDEPS=[
+ '$BUILD_DIR/mongo/base',
+ '$BUILD_DIR/mongo/idl/idl_parser',
+])
+
+env.Library(
+ target='query_stats',
+ source=['query_stats.cpp', 'query_stats_entry.cpp'],
+ LIBDEPS=[
+ '$BUILD_DIR/mongo/base',
+ '$BUILD_DIR/mongo/bson/mutable/mutable_bson',
+ '$BUILD_DIR/mongo/db/commands',
+ '$BUILD_DIR/mongo/db/profile_filter',
+ '$BUILD_DIR/mongo/db/query/command_request_response',
+ '$BUILD_DIR/mongo/db/query/memory_util',
+ '$BUILD_DIR/mongo/db/query/query_knobs',
+ '$BUILD_DIR/mongo/db/server_options',
+ '$BUILD_DIR/mongo/db/service_context',
+ '$BUILD_DIR/mongo/db/stats/counters',
+ '$BUILD_DIR/mongo/db/storage/storage_engine_parameters',
+ '$BUILD_DIR/mongo/rpc/client_metadata',
+ '$BUILD_DIR/mongo/util/fail_point',
+ '$BUILD_DIR/mongo/util/net/network',
+ '$BUILD_DIR/mongo/util/processinfo',
+ 'query_stats_parse',
+ 'rate_limiting',
+ ],
+ LIBDEPS_PRIVATE=[
+ '$BUILD_DIR/mongo/db/auth/auth',
+ '$BUILD_DIR/mongo/util/namespace_string_database_name_util',
+ ],
+)
+
+env.CppUnitTest(
+ target="db_query_query_stats_test",
+ source=[
+ "query_stats_store_test.cpp",
+ "rate_limiting_test.cpp",
+ ],
+ LIBDEPS=[
+ "$BUILD_DIR/mongo/db/auth/authmocks",
+ "$BUILD_DIR/mongo/db/query/query_shape",
+ "$BUILD_DIR/mongo/db/query/query_test_service_context",
+ "$BUILD_DIR/mongo/db/service_context_d_test_fixture",
+ "query_stats",
+ "rate_limiting",
+ ],
+)
+
+env.Benchmark(
+ target='rate_limiting_bm',
+ source=[
+ 'rate_limiting_bm.cpp',
+ ],
+ LIBDEPS=[
+ '$BUILD_DIR/mongo/base',
+ '$BUILD_DIR/mongo/unittest/unittest',
+ '$BUILD_DIR/mongo/util/processinfo',
+ 'rate_limiting',
+ ],
+)
+
+env.Benchmark(
+ target='shapifying_bm',
+ source=[
+ 'shapifying_bm.cpp',
+ ],
+ LIBDEPS=[
+ '$BUILD_DIR/mongo/base',
+ '$BUILD_DIR/mongo/db/auth/auth',
+ '$BUILD_DIR/mongo/db/pipeline/pipeline',
+ '$BUILD_DIR/mongo/db/query/canonical_query',
+ '$BUILD_DIR/mongo/db/query/query_shape',
+ '$BUILD_DIR/mongo/db/query/query_test_service_context',
+ '$BUILD_DIR/mongo/db/server_base',
+ '$BUILD_DIR/mongo/db/service_context',
+ '$BUILD_DIR/mongo/rpc/client_metadata',
+ '$BUILD_DIR/mongo/unittest/unittest',
+ '$BUILD_DIR/mongo/util/processinfo',
+ 'query_stats',
+ ],
+)
diff --git a/src/mongo/db/query/query_stats_aggregate_key_generator.cpp b/src/mongo/db/query/query_stats/aggregate_key_generator.cpp
index cd340991406..f2b501b39bb 100644
--- a/src/mongo/db/query/query_stats_aggregate_key_generator.cpp
+++ b/src/mongo/db/query/query_stats/aggregate_key_generator.cpp
@@ -27,11 +27,12 @@
* it in the license file.
*/
-#include "mongo/db/query/query_stats_aggregate_key_generator.h"
+#include "mongo/db/query/query_stats/aggregate_key_generator.h"
#include "mongo/db/pipeline/pipeline.h"
#include "mongo/db/query/query_shape.h"
#include "mongo/db/query/serialization_options.h"
+#include "mongo/db/query/shape_helpers.h"
namespace mongo::query_stats {
@@ -81,7 +82,7 @@ void AggregateKeyGenerator::appendCommandSpecificComponents(
BSONArrayBuilder otherNss = bob.subarrayStart(kOtherNssFieldName);
for (const auto& nss : _involvedNamespaces) {
BSONObjBuilder otherNsEntryBob = otherNss.subobjStart();
- query_shape::appendNamespaceShape(otherNsEntryBob, nss, opts);
+ shape_helpers::appendNamespaceShape(otherNsEntryBob, nss, opts);
otherNsEntryBob.doneFast();
}
otherNss.doneFast();
diff --git a/src/mongo/db/query/query_stats_aggregate_key_generator.h b/src/mongo/db/query/query_stats/aggregate_key_generator.h
index 619f8a126a3..ab6aa0ef688 100644
--- a/src/mongo/db/query/query_stats_aggregate_key_generator.h
+++ b/src/mongo/db/query/query_stats/aggregate_key_generator.h
@@ -32,7 +32,7 @@
#include "mongo/db/collection_type.h"
#include "mongo/db/pipeline/aggregate_command_gen.h"
#include "mongo/db/pipeline/pipeline.h"
-#include "mongo/db/query/query_stats_key_generator.h"
+#include "mongo/db/query/query_stats/key_generator.h"
namespace mongo::query_stats {
@@ -51,15 +51,17 @@ public:
const boost::intrusive_ptr<ExpressionContext>& expCtx,
stdx::unordered_set<NamespaceString> involvedNamespaces,
const NamespaceString& origNss,
- query_shape::CollectionType collectionType = query_shape::CollectionType::unknown)
+ query_shape::CollectionType collectionType = query_shape::CollectionType ::kUnknown)
: KeyGenerator(
expCtx->opCtx,
// TODO: SERVER-76330 Store representative agg query shape in telemetry store.
BSONObj(),
+ request.getHint(),
collectionType),
_request(std::move(request)),
_involvedNamespaces(std::move(involvedNamespaces)),
_origNss(origNss),
+ _inMongos(expCtx->inMongos),
_initialQueryStatsKey(_makeQueryStatsKeyHelper(
SerializationOptions::kDebugQueryShapeSerializeOptions, expCtx, pipeline)) {
_queryShapeHash = query_shape::hash(*_initialQueryStatsKey);
@@ -104,6 +106,7 @@ private:
// per a given query.
expCtx->stopExpressionCounters();
expCtx->addResolvedNamespaces(_involvedNamespaces);
+ expCtx->inMongos = _inMongos;
return expCtx;
}
@@ -118,6 +121,10 @@ private:
// The original NSS of the request before views are resolved.
const NamespaceString _origNss;
+ // Flag to denote if the query was run on mongos. Needed to rebuild the "dummy" expression
+ // context for re-parsing.
+ bool _inMongos;
+
// This is computed and cached upon construction until asked for once - at which point this
// transitions to boost::none. This both a performance and a memory optimization.
//
diff --git a/src/mongo/db/query/query_stats/aggregated_metric.h b/src/mongo/db/query/query_stats/aggregated_metric.h
new file mode 100644
index 00000000000..fca94c3183e
--- /dev/null
+++ b/src/mongo/db/query/query_stats/aggregated_metric.h
@@ -0,0 +1,78 @@
+/**
+ * Copyright (C) 2023-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <cstdint>
+
+#include "mongo/base/string_data.h"
+#include "mongo/bson/bsonobjbuilder.h"
+
+namespace mongo::query_stats {
+
+/**
+ * An aggregated metric stores a compressed view of data. It balances the loss of information
+ * with the reduction in required storage.
+ */
+struct AggregatedMetric {
+
+ /**
+ * Aggregate an observed value into the metric.
+ */
+ void aggregate(uint64_t val) {
+ sum += val;
+ max = std::max(val, max);
+ min = std::min(val, min);
+ sumOfSquares += val * val;
+ }
+
+ void appendTo(BSONObjBuilder& builder, const StringData& fieldName) const {
+ BSONObjBuilder metricsBuilder = builder.subobjStart(fieldName);
+ metricsBuilder.append("sum", (long long)sum);
+ metricsBuilder.append("max", (long long)max);
+ metricsBuilder.append("min", (long long)min);
+ metricsBuilder.append("sumOfSquares", (long long)sumOfSquares);
+ metricsBuilder.done();
+ }
+
+ uint64_t sum = 0;
+ // Default to the _signed_ maximum (which fits in unsigned range) because we cast to
+ // BSONNumeric when serializing.
+ uint64_t min = (uint64_t)std::numeric_limits<int64_t>::max;
+ uint64_t max = 0;
+
+ /**
+ * The sum of squares along with (an externally stored) count will allow us to compute the
+ * variance/stddev.
+ */
+ uint64_t sumOfSquares = 0;
+};
+
+} // namespace mongo::query_stats
diff --git a/src/mongo/db/query/query_stats_find_key_generator.cpp b/src/mongo/db/query/query_stats/find_key_generator.cpp
index 86c9555c59a..0d4fc25ac36 100644
--- a/src/mongo/db/query/query_stats_find_key_generator.cpp
+++ b/src/mongo/db/query/query_stats/find_key_generator.cpp
@@ -27,7 +27,7 @@
* it in the license file.
*/
-#include "mongo/db/query/query_stats_find_key_generator.h"
+#include "mongo/db/query/query_stats/find_key_generator.h"
#include "mongo/db/matcher/extensions_callback_real.h"
#include "mongo/db/namespace_string.h"
@@ -45,7 +45,7 @@ std::unique_ptr<FindCommandRequest> FindKeyGenerator::reparse(OperationContext*
// TODO: SERVER-76330 factor out building the parseable cmdObj into a helper function in
// query_shape.h.
BSONObjBuilder cmdBuilder;
- NamespaceStringOrUUID nss = query_shape::parseNamespaceShape(_parseableQueryShape["cmdNs"]);
+ NamespaceStringOrUUID nss = shape_helpers::parseNamespaceShape(_parseableQueryShape["cmdNs"]);
nss.serialize(&cmdBuilder, FindCommandRequest::kCommandName);
cmdBuilder.append("$db", nss.dbname());
diff --git a/src/mongo/db/query/query_stats_find_key_generator.h b/src/mongo/db/query/query_stats/find_key_generator.h
index 6175e67ae52..ca7207e95fb 100644
--- a/src/mongo/db/query/query_stats_find_key_generator.h
+++ b/src/mongo/db/query/query_stats/find_key_generator.h
@@ -35,7 +35,7 @@
#include "mongo/db/query/find_command.h"
#include "mongo/db/query/find_command_gen.h"
#include "mongo/db/query/parsed_find_command.h"
-#include "mongo/db/query/query_stats_key_generator.h"
+#include "mongo/db/query/query_stats/key_generator.h"
namespace mongo::query_stats {
@@ -45,8 +45,11 @@ public:
const boost::intrusive_ptr<ExpressionContext>& expCtx,
const ParsedFindCommand& request,
BSONObj parseableQueryShape,
- query_shape::CollectionType collectionType = query_shape::CollectionType::unknown)
- : KeyGenerator(expCtx->opCtx, parseableQueryShape, collectionType),
+ query_shape::CollectionType collectionType = query_shape::CollectionType::kUnknown)
+ : KeyGenerator(expCtx->opCtx,
+ parseableQueryShape,
+ request.findCommandRequest->getHint(),
+ collectionType),
_readConcern(request.findCommandRequest->getReadConcern().has_value()
? request.findCommandRequest->getReadConcern()->copy()
: BSONObj()),
diff --git a/src/mongo/db/query/query_stats_key_generator.h b/src/mongo/db/query/query_stats/key_generator.h
index c3e25fafce7..6b109e89d01 100644
--- a/src/mongo/db/query/query_stats_key_generator.h
+++ b/src/mongo/db/query/query_stats/key_generator.h
@@ -37,6 +37,7 @@
#include "mongo/db/pipeline/expression_context.h"
#include "mongo/db/query/query_shape.h"
#include "mongo/db/query/serialization_options.h"
+#include "mongo/db/query/shape_helpers.h"
#include "mongo/rpc/metadata/client_metadata.h"
namespace mongo {
@@ -89,7 +90,7 @@ public:
_parseableQueryShape.objsize() + /* _collectionType is not owned here */
(_apiParams ? sizeof(*_apiParams) + optionalSize(_apiParams->getAPIVersion()) : 0) +
(_hasField.clientMetaData ? _clientMetaData.objsize() : 0) + _commentObj.objsize() +
- (_hasField.readPreference ? _readPreference.objsize() : 0);
+ (_hasField.readPreference ? _readPreference.objsize() : 0) + _hintObj.objsize();
}
BSONObj getRepresentativeQueryShapeForDebug() const {
@@ -99,9 +100,11 @@ public:
protected:
KeyGenerator(OperationContext* opCtx,
BSONObj parseableQueryShape,
- query_shape::CollectionType collectionType = query_shape::CollectionType::unknown,
+ boost::optional<BSONObj> hint,
+ query_shape::CollectionType collectionType = query_shape::CollectionType::kUnknown,
boost::optional<query_shape::QueryShapeHash> queryShapeHash = boost::none)
: _parseableQueryShape(parseableQueryShape.getOwned()),
+ _hintObj(hint.value_or(BSONObj())),
_queryShapeHash(queryShapeHash.value_or(query_shape::hash(parseableQueryShape))),
_collectionType(collectionType) {
if (auto metadata = ClientMetadata::get(opCtx->getClient())) {
@@ -179,10 +182,13 @@ protected:
if (_hasField.clientMetaData) {
bob.append("client", _clientMetaData);
}
- if (_collectionType > query_shape::CollectionType::unknown &&
- _collectionType < query_shape::CollectionType::end) {
+ if (_collectionType != query_shape::CollectionType::kUnknown) {
bob.append("collectionType", toStringData(_collectionType));
}
+
+ if (!_hintObj.isEmpty()) {
+ bob.append("hint", shape_helpers::extractHintShape(_hintObj, opts));
+ }
}
/**
@@ -199,12 +205,17 @@ protected:
// minimize the struct's size as much as possible.
BSONObj _parseableQueryShape;
- // Preserve this value in the query shape.
+ // Preserve this value.
BSONObj _clientMetaData;
// Shapify this value.
BSONObj _commentObj;
- // Preserve this value in the query shape.
+ // Preserve this value.
BSONObj _readPreference;
+ // Preserve this value. Possibly empty.
+ // In the future a hint may not be part of every single type of request, but it is possibly
+ // set on a find, aggregate, distinct, and update, so this is going to be a "common" element
+ // for a while. It may not make sense on an insert request.
+ BSONObj _hintObj;
// Separate the possibly-enormous BSONObj from the remaining members
@@ -230,7 +241,7 @@ protected:
// This static assert checks to ensure that the struct's size is changed thoughtfully. If adding
// or otherwise changing the members, this assert may be updated with care.
static_assert(
- sizeof(KeyGenerator) <= 4 * sizeof(BSONObj) + sizeof(BSONElement) +
+ sizeof(KeyGenerator) <= 5 * sizeof(BSONObj) + sizeof(BSONElement) +
2 * sizeof(std::unique_ptr<APIParameters>) + sizeof(query_shape::CollectionType) +
sizeof(query_shape::QueryShapeHash) + sizeof(int64_t),
"Size of KeyGenerator is too large! "
diff --git a/src/mongo/db/query/query_stats.cpp b/src/mongo/db/query/query_stats/query_stats.cpp
index 3e94d415cf7..f36edce4b6c 100644
--- a/src/mongo/db/query/query_stats.cpp
+++ b/src/mongo/db/query/query_stats/query_stats.cpp
@@ -27,10 +27,10 @@
* it in the license file.
*/
-#include "mongo/db/query/query_stats.h"
+#include "mongo/db/query/query_stats/query_stats.h"
+
#include "mongo/crypto/hash_block.h"
-#include "mongo/crypto/sha256_block.h"
#include "mongo/db/concurrency/d_concurrency.h"
#include "mongo/db/concurrency/locker.h"
#include "mongo/db/curop.h"
@@ -45,8 +45,7 @@
#include "mongo/db/query/query_feature_flags_gen.h"
#include "mongo/db/query/query_planner_params.h"
#include "mongo/db/query/query_request_helper.h"
-#include "mongo/db/query/query_stats_util.h"
-#include "mongo/db/query/rate_limiting.h"
+#include "mongo/db/query/query_stats/util.h"
#include "mongo/db/query/serialization_options.h"
#include "mongo/db/query/sort_pattern.h"
#include "mongo/logv2/log.h"
@@ -59,11 +58,7 @@
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery
-namespace mongo {
-
-namespace query_stats {
-
-CounterMetric queryStatsStoreSizeEstimateBytesMetric("queryStats.queryStatsStoreSizeEstimateBytes");
+namespace mongo::query_stats {
namespace {
@@ -98,59 +93,6 @@ size_t getQueryStatsStoreSize() {
return capQueryStatsStoreSize(requestedSize);
}
-/**
- * A manager for the queryStats store allows a "pointer swap" on the queryStats store itself. The
- * usage patterns are as follows:
- *
- * - Updating the queryStats store uses the `getQueryStatsStore()` method. The queryStats store
- * instance is obtained, entries are looked up and mutated, or created anew.
- * - The queryStats store is "reset". This involves atomically allocating a new instance, once
- * there are no more updaters (readers of the store "pointer"), and returning the existing
- * instance.
- */
-class QueryStatsStoreManager {
-public:
- template <typename... QueryStatsStoreArgs>
- QueryStatsStoreManager(size_t cacheSize, size_t numPartitions)
- : _queryStatsStore(std::make_unique<QueryStatsStore>(cacheSize, numPartitions)),
- _maxSize(cacheSize) {}
-
- /**
- * Acquire the instance of the queryStats store.
- */
- QueryStatsStore& getQueryStatsStore() {
- return *_queryStatsStore;
- }
-
- size_t getMaxSize() {
- return _maxSize;
- }
-
- /**
- * Resize the queryStats store and return the number of evicted
- * entries.
- */
- size_t resetSize(size_t cacheSize) {
- _maxSize = cacheSize;
- return _queryStatsStore->reset(cacheSize);
- }
-
-private:
- std::unique_ptr<QueryStatsStore> _queryStatsStore;
-
- /**
- * Max size of the queryStats store. Tracked here to avoid having to recompute after it's
- * divided up into partitions.
- */
- size_t _maxSize;
-};
-
-const auto queryStatsStoreDecoration =
- ServiceContext::declareDecoration<std::unique_ptr<QueryStatsStoreManager>>();
-
-const auto queryStatsRateLimiter =
- ServiceContext::declareDecoration<std::unique_ptr<RateLimiting>>();
-
class TelemetryOnParamChangeUpdaterImpl final : public query_stats_util::OnParamChangeUpdater {
public:
void updateCacheSize(ServiceContext* serviceCtx, memory_util::MemorySize memSize) final {
@@ -171,7 +113,9 @@ ServiceContext::ConstructorActionRegisterer queryStatsStoreManagerRegisterer{
// It is possible that this is called before FCV is properly set up. Setting up the store if
// the flag is enabled but FCV is incorrect is safe, and guards against the FCV being
// changed to a supported version later.
- if (!feature_flags::gFeatureFlagQueryStats.isEnabledAndIgnoreFCVUnsafeAtStartup()) {
+ if (!feature_flags::gFeatureFlagQueryStats.isEnabledAndIgnoreFCVUnsafeAtStartup() &&
+ !feature_flags::gFeatureFlagQueryStatsFindCommand
+ .isEnabledAndIgnoreFCVUnsafeAtStartup()) {
// featureFlags are not allowed to be changed at runtime. Therefore it's not an issue
// to not create a queryStats store in ConstructorActionRegisterer at start up with the
// flag off - because the flag can not be turned on at any point afterwards.
@@ -184,7 +128,6 @@ ServiceContext::ConstructorActionRegisterer queryStatsStoreManagerRegisterer{
std::make_unique<TelemetryOnParamChangeUpdaterImpl>();
size_t size = getQueryStatsStoreSize();
auto&& globalQueryStatsStoreManager = queryStatsStoreDecoration(serviceCtx);
-
// Initially the queryStats store used the same number of partitions as the plan cache, that
// is the number of cpu cores. However, with performance investigation we found that when
// the size of the partitions was too large, it took too long to copy out and read one
@@ -207,21 +150,20 @@ ServiceContext::ConstructorActionRegisterer queryStatsStoreManagerRegisterer{
std::make_unique<QueryStatsStoreManager>(size, numPartitions);
auto configuredSamplingRate = internalQueryStatsRateLimit.load();
queryStatsRateLimiter(serviceCtx) = std::make_unique<RateLimiting>(
- configuredSamplingRate < 0 ? INT_MAX : configuredSamplingRate);
+ configuredSamplingRate < 0 ? INT_MAX : configuredSamplingRate, Seconds{1});
}};
/**
* Top-level checks for whether queryStats collection is enabled. If this returns false, we must go
* no further.
+ * TODO SERVER-79494 Remove requiresFullQueryStatsFeatureFlag parameter.
*/
-bool isQueryStatsEnabled(const ServiceContext* serviceCtx) {
- // During initialization FCV may not yet be setup but queries could be run. We can't
+bool isQueryStatsEnabled(const ServiceContext* serviceCtx, bool requiresFullQueryStatsFeatureFlag) {
+ // During initialization, FCV may not yet be setup but queries could be run. We can't
// check whether queryStats should be enabled without FCV, so default to not recording
// those queries.
// TODO SERVER-75935 Remove FCV Check.
- const auto fcvSnapshot = serverGlobalParams.featureCompatibility.acquireFCVSnapshot();
- return fcvSnapshot.isVersionInitialized() &&
- feature_flags::gFeatureFlagQueryStats.isEnabled(fcvSnapshot) &&
+ return isQueryStatsFeatureEnabled(requiresFullQueryStatsFeatureFlag) &&
queryStatsStoreDecoration(serviceCtx)->getMaxSize() > 0;
}
@@ -230,10 +172,6 @@ bool isQueryStatsEnabled(const ServiceContext* serviceCtx) {
* configuration for a global on/off decision and, if enabled, delegates to the rate limiter.
*/
bool shouldCollect(const ServiceContext* serviceCtx) {
- // Quick escape if queryStats is turned off.
- if (!isQueryStatsEnabled(serviceCtx)) {
- return false;
- }
// Cannot collect queryStats if sampling rate is not greater than 0. Note that we do not
// increment queryStatsRateLimitedRequestsMetric here since queryStats is entirely disabled.
auto samplingRate = queryStatsRateLimiter(serviceCtx)->getSamplingRate();
@@ -249,33 +187,29 @@ bool shouldCollect(const ServiceContext* serviceCtx) {
return true;
}
-std::string sha256HmacStringDataHasher(std::string key, const StringData& sd) {
- auto hashed = SHA256Block::computeHmac(
- (const uint8_t*)key.data(), key.size(), (const uint8_t*)sd.rawData(), sd.size());
- return hashed.toString();
-}
-
-std::size_t hash(const BSONObj& obj) {
- return absl::hash_internal::CityHash64(obj.objdata(), obj.objsize());
-}
-
} // namespace
-BSONObj QueryStatsEntry::computeQueryStatsKey(OperationContext* opCtx,
- TransformAlgorithmEnum algorithm,
- std::string hmacKey) const {
- return keyGenerator->generate(
- opCtx,
- algorithm == TransformAlgorithmEnum::kHmacSha256
- ? boost::optional<SerializationOptions::TokenizeIdentifierFunc>(
- [&](StringData sd) { return sha256HmacStringDataHasher(hmacKey, sd); })
- : boost::none);
+/**
+ * Indicates whether or not query stats is enabled via the feature flags. If
+ * requiresFullQueryStatsFeatureFlag is true, it will only return true if featureFlagQueryStats is
+ * enabled. Otherwise, it will return true if either featureFlagQueryStats or
+ * featureFlagQueryStatsFindCommand is enabled.
+ *
+ * TODO SERVER-79494 Remove this function and collapse feature flag check into isQueryStatsEnabled.
+ */
+bool isQueryStatsFeatureEnabled(bool requiresFullQueryStatsFeatureFlag) {
+ const auto fcvSnapshot = serverGlobalParams.featureCompatibility.acquireFCVSnapshot();
+ return fcvSnapshot.isVersionInitialized() &&
+ (feature_flags::gFeatureFlagQueryStats.isEnabled(fcvSnapshot) ||
+ (!requiresFullQueryStatsFeatureFlag &&
+ feature_flags::gFeatureFlagQueryStatsFindCommand.isEnabled(fcvSnapshot)));
}
void registerRequest(OperationContext* opCtx,
const NamespaceString& collection,
- std::function<std::unique_ptr<KeyGenerator>(void)> makeKeyGenerator) {
- if (!isQueryStatsEnabled(opCtx->getServiceContext())) {
+ std::function<std::unique_ptr<KeyGenerator>(void)> makeKeyGenerator,
+ bool requiresFullQueryStatsFeatureFlag) {
+ if (!isQueryStatsEnabled(opCtx->getServiceContext(), requiresFullQueryStatsFeatureFlag)) {
return;
}
@@ -298,16 +232,30 @@ void registerRequest(OperationContext* opCtx,
"collection"_attr = collection);
return;
}
-
- opDebug.queryStatsKeyGenerator = makeKeyGenerator();
+ // There are a few cases where a query shape can be larger than the original query. For example,
+ // {$exists: false} in the input query serializes to {$not: {$exists: true}. In rare cases where
+ // an input query has thousands of clauses, the cumulative bloat that shapification adds results
+ // in a BSON object that exceeds the 16 MB memory limit. In these cases, we want to exclude the
+ // original query from queryStats metrics collection and let it execute normally.
+ try {
+ opDebug.queryStatsKeyGenerator = makeKeyGenerator();
+ } catch (ExceptionFor<ErrorCodes::BSONObjectTooLarge>&) {
+ LOGV2_DEBUG(7979400,
+ 1,
+ "Query Stats shapification has exceeded the 16 MB memory limit. Metrics will "
+ "not be collected ");
+ queryStatsStoreWriteErrorsMetric.increment();
+ return;
+ }
opDebug.queryStatsStoreKeyHash = opDebug.queryStatsKeyGenerator->hash();
}
QueryStatsStore& getQueryStatsStore(OperationContext* opCtx) {
uassert(6579000,
- "Telemetry is not enabled without the feature flag on and a cache size greater than 0 "
- "bytes",
- isQueryStatsEnabled(opCtx->getServiceContext()));
+ "Query stats is not enabled without the feature flag on and a cache size greater than "
+ "0 bytes",
+ isQueryStatsEnabled(opCtx->getServiceContext(),
+ /*requiresFullQueryStatsFeatureFlag*/ false));
return queryStatsStoreDecoration(opCtx->getServiceContext())->getQueryStatsStore();
}
@@ -321,11 +269,12 @@ void writeQueryStats(OperationContext* opCtx,
return;
}
- // It's possible that telemetry was enabled in registerRequest but has been disabled since
+ // It's possible that query stats was enabled in registerRequest but has been disabled since
// (e.g., by FCV downgrade or setting the store size to 0). Rather than calling
- // getTelemetryStore (which would trigger a uassert if telemetry is disabled), we return and
- // log a message if telemetry is disabled, and otherwise grab the telemetry store directly.
- if (!isQueryStatsEnabled(opCtx->getServiceContext())) {
+ // getQueryStatsStore (which would trigger a uassert if telemetry is disabled), we return and
+ // log a message if query stats is disabled, and otherwise grab the query stats store directly.
+ if (!isQueryStatsEnabled(opCtx->getServiceContext(),
+ /*requiresFullQueryStatsFeatureFlag*/ false)) {
LOGV2_DEBUG(8456700,
2,
"Query stats was enabled when the command started but is now disabled. "
@@ -371,5 +320,4 @@ void writeQueryStats(OperationContext* opCtx,
metrics->firstResponseExecMicros.aggregate(firstResponseExecMicros);
metrics->docsReturned.aggregate(docsReturned);
}
-} // namespace query_stats
-} // namespace mongo
+} // namespace mongo::query_stats
diff --git a/src/mongo/db/query/query_stats.h b/src/mongo/db/query/query_stats/query_stats.h
index fc4a6448ca0..90c942d9b3e 100644
--- a/src/mongo/db/query/query_stats.h
+++ b/src/mongo/db/query/query_stats/query_stats.h
@@ -36,168 +36,115 @@
#include "mongo/db/namespace_string.h"
#include "mongo/db/query/partitioned_cache.h"
#include "mongo/db/query/plan_explainer.h"
-#include "mongo/db/query/query_stats_key_generator.h"
-#include "mongo/db/query/query_stats_transform_algorithm_gen.h"
-#include "mongo/db/query/util/memory_util.h"
+#include "mongo/db/query/query_stats/key_generator.h"
+#include "mongo/db/query/query_stats/query_stats_entry.h"
+#include "mongo/db/query/query_stats/rate_limiting.h"
#include "mongo/db/service_context.h"
#include "mongo/db/views/view.h"
#include <cstdint>
#include <memory>
-namespace mongo {
+namespace mongo::query_stats {
-namespace {
-/**
- * Type we use to render values to BSON.
- */
-using BSONNumeric = long long;
-} // namespace
-
-namespace query_stats {
-/**
- * An aggregated metric stores a compressed view of data. It balances the loss of information
- * with the reduction in required storage.
- */
-struct AggregatedMetric {
-
- /**
- * Aggregate an observed value into the metric.
- */
- void aggregate(uint64_t val) {
- sum += val;
- max = std::max(val, max);
- min = std::min(val, min);
- sumOfSquares += val * val;
+struct QueryStatsPartitioner {
+ // The partitioning function for use with the 'Partitioned' utility.
+ std::size_t operator()(const std::size_t k, const std::size_t nPartitions) const {
+ return k % nPartitions;
}
+};
- void appendTo(BSONObjBuilder& builder, const StringData& fieldName) const {
- BSONObjBuilder metricsBuilder = builder.subobjStart(fieldName);
- metricsBuilder.append("sum", (BSONNumeric)sum);
- metricsBuilder.append("max", (BSONNumeric)max);
- metricsBuilder.append("min", (BSONNumeric)min);
- metricsBuilder.append("sumOfSquares", (BSONNumeric)sumOfSquares);
- metricsBuilder.done();
+struct QueryStatsStoreEntryBudgetor {
+ size_t operator()(const std::size_t key, const std::shared_ptr<QueryStatsEntry>& value) {
+ return sizeof(decltype(key)) + value->size();
}
-
- uint64_t sum = 0;
- // Default to the _signed_ maximum (which fits in unsigned range) because we cast to
- // BSONNumeric when serializing.
- uint64_t min = (uint64_t)std::numeric_limits<int64_t>::max;
- uint64_t max = 0;
-
- /**
- * The sum of squares along with (an externally stored) count will allow us to compute the
- * variance/stddev.
- */
- uint64_t sumOfSquares = 0;
};
-extern CounterMetric queryStatsStoreSizeEstimateBytesMetric;
-const auto kKeySize = sizeof(std::size_t);
-// Used to aggregate the metrics for one query stats key over all its executions.
-class QueryStatsEntry {
-public:
- QueryStatsEntry(std::unique_ptr<KeyGenerator> keyGenerator)
- : firstSeenTimestamp(Date_t::now()), keyGenerator(std::move(keyGenerator)) {
- // Increment by size of query stats store key (hash returns size_t) and value
- // (QueryStatsEntry)
- queryStatsStoreSizeEstimateBytesMetric.increment(kKeySize + size());
+/*
+ * 'QueryStatsStore insertion and eviction listener implementation. This class adjusts the
+ * 'queryStatsStoreSize' serverStatus metric when entries are inserted or evicted.
+ */
+struct QueryStatsStoreInsertionEvictionListener {
+ void onInsert(const std::size_t&,
+ const std::shared_ptr<QueryStatsEntry>&,
+ size_t estimatedSize) {
+ queryStatsStoreSizeEstimateBytesMetric.increment(estimatedSize);
}
- ~QueryStatsEntry() {
- // Decrement by size of query stats store key (hash returns size_t) and value
- // (QueryStatsEntry)
- queryStatsStoreSizeEstimateBytesMetric.decrement(kKeySize + size());
+ void onEvict(const std::size_t&,
+ const std::shared_ptr<QueryStatsEntry>&,
+ size_t estimatedSize) {
+ queryStatsStoreSizeEstimateBytesMetric.decrement(estimatedSize);
}
- BSONObj toBSON() const {
- BSONObjBuilder builder{sizeof(QueryStatsEntry) + 100};
- builder.append("lastExecutionMicros", (BSONNumeric)lastExecutionMicros);
- builder.append("execCount", (BSONNumeric)execCount);
- totalExecMicros.appendTo(builder, "totalExecMicros");
- firstResponseExecMicros.appendTo(builder, "firstResponseExecMicros");
- docsReturned.appendTo(builder, "docsReturned");
- builder.append("firstSeenTimestamp", firstSeenTimestamp);
- builder.append("latestSeenTimestamp", latestSeenTimestamp);
- return builder.obj();
+ void onClear(size_t estimatedSize) {
+ queryStatsStoreSizeEstimateBytesMetric.decrement(estimatedSize);
}
+};
+using QueryStatsStore = PartitionedCache<std::size_t,
+ std::shared_ptr<QueryStatsEntry>,
+ QueryStatsStoreEntryBudgetor,
+ QueryStatsPartitioner,
+ QueryStatsStoreInsertionEvictionListener>;
- int64_t size() {
- return sizeof(*this) + (keyGenerator ? keyGenerator->size() : 0);
- }
+/**
+ * A manager for the queryStats store allows a "pointer swap" on the queryStats store itself. The
+ * usage patterns are as follows:
+ *
+ * - Updating the queryStats store uses the `getQueryStatsStore()` method. The queryStats store
+ * instance is obtained, entries are looked up and mutated, or created anew.
+ * - The queryStats store is "reset". This involves atomically allocating a new instance, once
+ * there are no more updaters (readers of the store "pointer"), and returning the existing
+ * instance.
+ */
+class QueryStatsStoreManager {
+public:
+ template <typename... QueryStatsStoreArgs>
+ QueryStatsStoreManager(size_t cacheSize, size_t numPartitions)
+ : _queryStatsStore(std::make_unique<QueryStatsStore>(cacheSize, numPartitions)),
+ _maxSize(cacheSize) {}
/**
- * Generate the queryStats key for this entry's request. If algorithm is not
- * TransformAlgorithm::kNone, any identifying information (field names, namespace) will be
- * anonymized.
+ * Acquire the instance of the queryStats store.
*/
- BSONObj computeQueryStatsKey(OperationContext* opCtx,
- TransformAlgorithmEnum algorithm,
- std::string hmacKey) const;
-
- BSONObj getRepresentativeQueryShapeForDebug() const {
- return keyGenerator->getRepresentativeQueryShapeForDebug();
+ QueryStatsStore& getQueryStatsStore() {
+ return *_queryStatsStore;
}
- /**
- * Timestamp for when this query shape was added to the store. Set on construction.
- */
- const Date_t firstSeenTimestamp;
-
- /**
- * Timestamp for when the latest time this query shape was seen.
- */
- Date_t latestSeenTimestamp;
-
- /**
- * Last execution time in microseconds.
- */
- uint64_t lastExecutionMicros = 0;
-
- /**
- * Number of query executions.
- */
- uint64_t execCount = 0;
-
- /**
- * Aggregates the total time for execution including getMore requests.
- */
- AggregatedMetric totalExecMicros;
+ size_t getMaxSize() {
+ return _maxSize;
+ }
/**
- * Aggregates the time for execution for first batch only.
+ * Resize the queryStats store and return the number of evicted
+ * entries.
*/
- AggregatedMetric firstResponseExecMicros;
+ size_t resetSize(size_t cacheSize) {
+ _maxSize = cacheSize;
+ return _queryStatsStore->reset(cacheSize);
+ }
- AggregatedMetric docsReturned;
+private:
+ std::unique_ptr<QueryStatsStore> _queryStatsStore;
/**
- * The KeyGenerator that can generate the query stats key for this request.
+ * Max size of the queryStats store. Tracked here to avoid having to recompute after it's
+ * divided up into partitions.
*/
- const std::shared_ptr<const KeyGenerator> keyGenerator;
-};
-struct TelemetryPartitioner {
- // The partitioning function for use with the 'Partitioned' utility.
- std::size_t operator()(const std::size_t k, const std::size_t nPartitions) const {
- return k % nPartitions;
- }
+ size_t _maxSize;
};
-struct QueryStatsStoreEntryBudgetor {
- size_t operator()(const std::size_t key, const std::shared_ptr<QueryStatsEntry>& value) {
- return sizeof(decltype(key)) + value->size();
- }
-};
-using QueryStatsStore = PartitionedCache<std::size_t,
- std::shared_ptr<QueryStatsEntry>,
- QueryStatsStoreEntryBudgetor,
- TelemetryPartitioner>;
+const auto queryStatsStoreDecoration =
+ ServiceContext::declareDecoration<std::unique_ptr<QueryStatsStoreManager>>();
+const auto queryStatsRateLimiter =
+ ServiceContext::declareDecoration<std::unique_ptr<RateLimiting>>();
/**
* Acquire a reference to the global queryStats store.
*/
QueryStatsStore& getQueryStatsStore(OperationContext* opCtx);
+bool isQueryStatsFeatureEnabled(bool requiresFullQueryStatsFeatureFlag);
+
/**
* Registers a request for query stats collection. The function may decide not to collect anything,
* so this should be called for all requests. The decision is made based on the feature flag and
@@ -230,10 +177,15 @@ QueryStatsStore& getQueryStatsStore(OperationContext* opCtx);
* deferred construction callback to ensure that this feature does not impact performance if
* collecting stats is not needed due to the feature being disabled or the request being rate
* limited.
+ * - Since we currently have 2 feature flags (one for full query stats, and one for
+ * find-command-only query stats), we use the requiresFullQueryStatsFeatureFlag parameter to
+ * denote which requests should only be registered when the full feature flag is enabled. TODO
+ * SERVER-79494 Remove requiresFullQueryStatsFeatureFlag parameter.
*/
void registerRequest(OperationContext* opCtx,
const NamespaceString& collection,
- std::function<std::unique_ptr<KeyGenerator>(void)> makeKeyGenerator);
+ std::function<std::unique_ptr<KeyGenerator>(void)> makeKeyGenerator,
+ bool requiresFullQueryStatsFeatureFlag = true);
/**
* Writes query stats to the query stats store for the operation identified by `queryStatsKeyHash`.
@@ -251,5 +203,4 @@ void writeQueryStats(OperationContext* opCtx,
uint64_t queryExecMicros,
uint64_t firstResponseExecMicros,
uint64_t docsReturned);
-} // namespace query_stats
-} // namespace mongo
+} // namespace mongo::query_stats
diff --git a/src/mongo/db/query/query_stats/query_stats_entry.cpp b/src/mongo/db/query/query_stats/query_stats_entry.cpp
new file mode 100644
index 00000000000..922d0572f10
--- /dev/null
+++ b/src/mongo/db/query/query_stats/query_stats_entry.cpp
@@ -0,0 +1,76 @@
+/**
+ * Copyright (C) 2023-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#include "mongo/db/query/query_stats/query_stats_entry.h"
+
+#include <boost/optional.hpp>
+
+#include "mongo/crypto/hash_block.h"
+#include "mongo/crypto/sha256_block.h"
+
+#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery
+
+namespace mongo::query_stats {
+
+CounterMetric queryStatsStoreSizeEstimateBytesMetric("queryStats.queryStatsStoreSizeEstimateBytes");
+
+namespace {
+
+std::string sha256HmacStringDataHasher(std::string key, const StringData& sd) {
+ auto hashed = SHA256Block::computeHmac(
+ (const uint8_t*)key.data(), key.size(), (const uint8_t*)sd.rawData(), sd.size());
+ return hashed.toString();
+}
+
+} // namespace
+
+BSONObj QueryStatsEntry::computeQueryStatsKey(OperationContext* opCtx,
+ TransformAlgorithmEnum algorithm,
+ std::string hmacKey) const {
+ return keyGenerator->generate(
+ opCtx,
+ algorithm == TransformAlgorithmEnum::kHmacSha256
+ ? boost::optional<SerializationOptions::TokenizeIdentifierFunc>(
+ [&](StringData sd) { return sha256HmacStringDataHasher(hmacKey, sd); })
+ : boost::none);
+}
+
+BSONObj QueryStatsEntry::toBSON() const {
+ BSONObjBuilder builder{sizeof(QueryStatsEntry) + 100};
+ builder.append("lastExecutionMicros", (long long)lastExecutionMicros);
+ builder.append("execCount", (long long)execCount);
+ totalExecMicros.appendTo(builder, "totalExecMicros");
+ firstResponseExecMicros.appendTo(builder, "firstResponseExecMicros");
+ docsReturned.appendTo(builder, "docsReturned");
+ builder.append("firstSeenTimestamp", firstSeenTimestamp);
+ builder.append("latestSeenTimestamp", latestSeenTimestamp);
+ return builder.obj();
+}
+
+} // namespace mongo::query_stats
diff --git a/src/mongo/db/query/query_stats/query_stats_entry.h b/src/mongo/db/query/query_stats/query_stats_entry.h
new file mode 100644
index 00000000000..714ed864070
--- /dev/null
+++ b/src/mongo/db/query/query_stats/query_stats_entry.h
@@ -0,0 +1,117 @@
+/**
+ * Copyright (C) 2023-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <cstdint>
+#include <memory>
+
+#include "mongo/db/commands/server_status_metric.h"
+#include "mongo/db/query/query_stats/aggregated_metric.h"
+#include "mongo/db/query/query_stats/key_generator.h"
+#include "mongo/db/query/query_stats/transform_algorithm_gen.h"
+#include "mongo/util/time_support.h"
+
+namespace mongo::query_stats {
+
+extern CounterMetric queryStatsStoreSizeEstimateBytesMetric;
+
+const auto kKeySize = sizeof(std::size_t);
+
+/**
+ * The value stored in the query stats store. It contains a KeyGenerator representing this "kind" of
+ * query, and some metrics about that shape. This class is responsible for knowing its size and
+ * updating our server status metrics about the size of the query stats store accordingly. At the
+ * time of this writing, the LRUCache utility does not easily expose its size in a way we could use
+ * as server status metrics.
+ */
+class QueryStatsEntry {
+public:
+ QueryStatsEntry(std::unique_ptr<KeyGenerator> keyGenerator)
+ : firstSeenTimestamp(Date_t::now()), keyGenerator(std::move(keyGenerator)) {}
+
+ BSONObj toBSON() const;
+
+ int64_t size() {
+ return sizeof(*this) + (keyGenerator ? keyGenerator->size() : 0);
+ }
+
+ /**
+ * Generate the queryStats key for this entry's request. If algorithm is not
+ * TransformAlgorithm::kNone, any identifying information (field names, namespace) will be
+ * anonymized.
+ */
+ BSONObj computeQueryStatsKey(OperationContext* opCtx,
+ TransformAlgorithmEnum algorithm,
+ std::string hmacKey) const;
+
+ BSONObj getRepresentativeQueryShapeForDebug() const {
+ return keyGenerator->getRepresentativeQueryShapeForDebug();
+ }
+
+ /**
+ * Timestamp for when this query shape was added to the store. Set on construction.
+ */
+ const Date_t firstSeenTimestamp;
+
+ /**
+ * Timestamp for when the latest time this query shape was seen.
+ */
+ Date_t latestSeenTimestamp;
+
+ /**
+ * Last execution time in microseconds.
+ */
+ uint64_t lastExecutionMicros = 0;
+
+ /**
+ * Number of query executions.
+ */
+ uint64_t execCount = 0;
+
+ /**
+ * Aggregates the total time for execution including getMore requests.
+ */
+ AggregatedMetric totalExecMicros;
+
+ /**
+ * Aggregates the time for execution for first batch only.
+ */
+ AggregatedMetric firstResponseExecMicros;
+
+ AggregatedMetric docsReturned;
+
+ /**
+ * The KeyGenerator that can generate the query stats key for this request.
+ */
+ const std::shared_ptr<const KeyGenerator> keyGenerator;
+};
+
+} // namespace mongo::query_stats
diff --git a/src/mongo/db/query/query_stats_store_test.cpp b/src/mongo/db/query/query_stats/query_stats_store_test.cpp
index cbc92445950..43d791efac8 100644
--- a/src/mongo/db/query/query_stats_store_test.cpp
+++ b/src/mongo/db/query/query_stats/query_stats_store_test.cpp
@@ -33,9 +33,10 @@
#include "mongo/db/pipeline/expression_context_for_test.h"
#include "mongo/db/query/query_feature_flags_gen.h"
#include "mongo/db/query/query_shape.h"
-#include "mongo/db/query/query_stats.h"
-#include "mongo/db/query/query_stats_aggregate_key_generator.h"
-#include "mongo/db/query/query_stats_find_key_generator.h"
+#include "mongo/db/query/query_stats/aggregate_key_generator.h"
+#include "mongo/db/query/query_stats/find_key_generator.h"
+#include "mongo/db/query/query_stats/key_generator.h"
+#include "mongo/db/query/query_stats/query_stats.h"
#include "mongo/db/service_context_test_fixture.h"
#include "mongo/idl/server_parameter_test_util.h"
#include "mongo/unittest/inline_auto_update.h"
@@ -55,7 +56,7 @@ std::size_t hash(const BSONObj& obj) {
class QueryStatsStoreTest : public ServiceContextTest {
public:
- static constexpr auto collectionType = query_shape::CollectionType::collection;
+ static constexpr auto collectionType = query_shape::CollectionType::kCollection;
BSONObj makeQueryStatsKeyFindRequest(const FindCommandRequest& fcr,
const boost::intrusive_ptr<ExpressionContext>& expCtx,
bool applyHmac) {
@@ -94,19 +95,19 @@ public:
};
TEST_F(QueryStatsStoreTest, BasicUsage) {
- QueryStatsStore telStore{5000000, 1000};
+ QueryStatsStore queryStatsStore{5000000, 1000};
auto getMetrics = [&](const BSONObj& key) {
- auto lookupResult = telStore.lookup(hash(key));
+ auto lookupResult = queryStatsStore.lookup(hash(key));
return *lookupResult.getValue();
};
auto collectMetrics = [&](BSONObj& key) {
std::shared_ptr<QueryStatsEntry> metrics;
- auto lookupResult = telStore.lookup(hash(key));
+ auto lookupResult = queryStatsStore.lookup(hash(key));
if (!lookupResult.isOK()) {
- telStore.put(hash(key), std::make_shared<QueryStatsEntry>(nullptr));
- lookupResult = telStore.lookup(hash(key));
+ queryStatsStore.put(hash(key), std::make_shared<QueryStatsEntry>(nullptr));
+ lookupResult = queryStatsStore.lookup(hash(key));
}
metrics = *lookupResult.getValue();
metrics->execCount += 1;
@@ -128,7 +129,7 @@ TEST_F(QueryStatsStoreTest, BasicUsage) {
ASSERT_EQ(getMetrics(query2)->execCount, 1);
auto collectMetricsWithLock = [&](BSONObj& key) {
- auto [lookupResult, lock] = telStore.getWithPartitionLock(hash(key));
+ auto [lookupResult, lock] = queryStatsStore.getWithPartitionLock(hash(key));
auto metrics = *lookupResult.getValue();
metrics->execCount += 1;
metrics->lastExecutionMicros += 123456;
@@ -143,31 +144,154 @@ TEST_F(QueryStatsStoreTest, BasicUsage) {
int numKeys = 0;
- telStore.forEach(
+ queryStatsStore.forEach(
[&](std::size_t key, const std::shared_ptr<QueryStatsEntry>& entry) { numKeys++; });
ASSERT_EQ(numKeys, 2);
}
+TEST_F(QueryStatsStoreTest, EvictionTest) {
+ // This creates a queryStats store with a single partition to specifically test the eviction
+ // behavior with very large queries.
+ const auto cacheSize = 500;
+ const auto numPartitions = 1;
+ QueryStatsStore queryStatsStore{cacheSize, numPartitions};
-TEST_F(QueryStatsStoreTest, EvictEntries) {
- // This creates a queryStats store with 2 partitions, each with a size of 1200 bytes.
- const auto cacheSize = 2400;
- const auto numPartitions = 2;
- QueryStatsStore telStore{cacheSize, numPartitions};
-
- for (int i = 0; i < 30; i++) {
- auto query = BSON("query" + std::to_string(i) << 1 << "xEquals" << 42);
- telStore.put(hash(query), std::make_shared<QueryStatsEntry>(nullptr));
- }
+ // Add an entry that is smaller than the max partition size.
+ auto query = BSON("query" << 1 << "xEquals" << 42);
+ queryStatsStore.put(hash(query), std::make_shared<QueryStatsEntry>(nullptr));
int numKeys = 0;
- telStore.forEach(
+ queryStatsStore.forEach(
[&](std::size_t key, const std::shared_ptr<QueryStatsEntry>& entry) { numKeys++; });
+ ASSERT_EQ(numKeys, 1);
+ // Add an entry that is larger than the max partition size to the non-empty partition. This
+ // should evict both entries, the first small entry written to the partition and the current too
+ // large entry we wish to write to the partition. The reason is because entries are evicted from
+ // the partition in order of least recently used. Thus, the small entry will be evicted first
+ // but the partition will still be over budget so the final, too large entry will also be
+ // evicted.
+ auto opCtx = makeOperationContext();
+ auto fcr = std::make_unique<FindCommandRequest>(
+ NamespaceStringOrUUID(NamespaceString::createNamespaceString_forTest("testDB.testColl")));
+ fcr->setLet(BSON("var" << 2));
+ fcr->setFilter(fromjson("{$expr: [{$eq: ['$a', '$$var']}]}"));
+ fcr->setProjection(fromjson("{varIs: '$$var'}"));
+ fcr->setLimit(5);
+ fcr->setSkip(2);
+ fcr->setBatchSize(25);
+ fcr->setMaxTimeMS(1000);
+ fcr->setNoCursorTimeout(false);
+ opCtx->setComment(BSON("comment"
+ << " foo"));
+ fcr->setSingleBatch(false);
+ fcr->setAllowDiskUse(false);
+ fcr->setAllowPartialResults(true);
+ fcr->setAllowDiskUse(false);
+ fcr->setShowRecordId(true);
+ fcr->setMirrored(true);
+ fcr->setHint(BSON("z" << 1 << "c" << 1));
+ fcr->setMax(BSON("z" << 25));
+ fcr->setMin(BSON("z" << 80));
+ fcr->setSort(BSON("sortVal" << 1 << "otherSort" << -1));
- int entriesPerPartition =
- (cacheSize / numPartitions) / (sizeof(std::size_t) + sizeof(QueryStatsEntry));
+ auto&& [expCtx, parsedFind] =
+ uassertStatusOK(parsed_find_command::parse(opCtx.get(), std::move(fcr)));
+ auto queryShape = query_shape::extractQueryShape(
+ *parsedFind, SerializationOptions::kRepresentativeQueryShapeSerializeOptions, expCtx);
+ QueryStatsEntry testMetrics{std::make_unique<query_stats::FindKeyGenerator>(
+ expCtx, *parsedFind, queryShape, collectionType)};
+ queryStatsStore.put(hash(testMetrics.computeQueryStatsKey(
+ opCtx.get(), TransformAlgorithmEnum::kNone, std::string{})),
+ std::make_shared<QueryStatsEntry>(testMetrics));
+ numKeys = 0;
+ queryStatsStore.forEach(
+ [&](std::size_t key, const std::shared_ptr<QueryStatsEntry>& entry) { numKeys++; });
+ ASSERT_EQ(numKeys, 0);
- ASSERT_EQ(numKeys, entriesPerPartition * numPartitions);
+ // This creates a queryStats store where each partition has a max size of 500 bytes.
+ QueryStatsStore queryStatsStoreTwo{/*cacheSize*/ 1500, /*numPartitions*/ 3};
+ // Adding a queryStats store entry that is smaller than the overal cache size but larger than a
+ // single partition max size, will cause an eviction. testMetrics is larger than 500 bytes and
+ // thus over budget for the partitions of this cache.
+ queryStatsStoreTwo.put(hash(testMetrics.computeQueryStatsKey(
+ opCtx.get(), TransformAlgorithmEnum::kNone, std::string{})),
+ std::make_shared<QueryStatsEntry>(testMetrics));
+ queryStatsStoreTwo.forEach(
+ [&](std::size_t key, const std::shared_ptr<QueryStatsEntry>& entry) { numKeys++; });
+ ASSERT_EQ(numKeys, 0);
+}
+
+TEST_F(QueryStatsStoreTest, GenerateMaxBsonSizeQueryShape) {
+ const NamespaceString nss = NamespaceString::createNamespaceString_forTest("testDB.testColl");
+ FindCommandRequest fcr((NamespaceStringOrUUID(nss)));
+ // This creates a query that is just below the 16 MB memory limit.
+ int limit = 225500;
+ BSONObjBuilder bob;
+ BSONArrayBuilder andBob(bob.subarrayStart("$and"));
+ for (int i = 1; i <= limit; i++) {
+ BSONObjBuilder childrenBob;
+ childrenBob.append("x", BSON("$lt" << i << "$gte" << i));
+ andBob.append(childrenBob.obj());
+ }
+ andBob.doneFast();
+ fcr.setFilter(bob.obj());
+ auto fcrCopy = std::make_unique<FindCommandRequest>(fcr);
+ auto opCtx = makeOperationContext();
+ auto parsedFindPair =
+ uassertStatusOK(parsed_find_command::parse(opCtx.get(), std::move(fcrCopy)));
+
+ // TODO SERVER-84011 backport: re-enable SERVER-79794 test. This test is currently
+ // broken since the queryStatsfeatureFlags are off by default. Query stats assumes that the
+ // feature flag can never change without a restart, so enabling the flag in the test still
+ // doesn't allow for query stats functionality. The following replicates what queryStats would
+ // have done.
+
+ auto& opDebug = CurOp::get(*opCtx)->debug();
+ ASSERT_DOES_NOT_THROW(
+ try {
+ BSONObj queryShape = query_shape::extractQueryShape(
+ *parsedFindPair.second,
+ SerializationOptions::kRepresentativeQueryShapeSerializeOptions,
+ parsedFindPair.first);
+ opDebug.queryStatsKeyGenerator = std::make_unique<query_stats::FindKeyGenerator>(
+ parsedFindPair.first,
+ *parsedFindPair.second,
+ std::move(queryShape),
+ query_shape::CollectionType::kCollection);
+ } catch (ExceptionFor<ErrorCodes::BSONObjectTooLarge>&) { return; })
+
+ opDebug.queryStatsStoreKeyHash = opDebug.queryStatsKeyGenerator->hash();
+ ASSERT_EQ(opDebug.queryStatsStoreKeyHash, boost::none);
+
+ // TODO SERVER-84011 backport. Below is the proper test using queryStats.
+ // RAIIServerParameterControllerForTest controller("featureFlagQueryStats", true);
+ // RAIIServerParameterControllerForTest queryKnobController{"internalQueryStatsRateLimit", -1};
+
+ // auto&& globalQueryStatsStoreManager = queryStatsStoreDecoration(opCtx->getServiceContext());
+ // globalQueryStatsStoreManager = std::make_unique<QueryStatsStoreManager>(500000, 1000);
+
+ // // The shapification process will bloat the input query over the 16 MB memory limit. Assert
+ // that
+ // // calling registerRequest() doesn't throw and that the opDebug isn't registered with a key
+ // hash
+ // // (thus metrics won't be tracked for this query).
+ // ASSERT_DOES_NOT_THROW(query_stats::registerRequest(
+ // opCtx.get(),
+ // nss,
+ // [&]() {
+ // BSONObj queryShape = query_shape::extractQueryShape(
+ // *parsedFindPair.second,
+ // SerializationOptions::kRepresentativeQueryShapeSerializeOptions,
+ // parsedFindPair.first);
+ // return std::make_unique<query_stats::FindKeyGenerator>(
+ // parsedFindPair.first,
+ // *parsedFindPair.second,
+ // std::move(queryShape),
+ // query_shape::CollectionType::kCollection);
+ // },
+ // /*requiresFullQueryStatsFeatureFlag*/ false));
+ // auto& opDebug = CurOp::get(*opCtx)->debug();
+ // ASSERT_EQ(opDebug.queryStatsStoreKeyHash, boost::none);
}
TEST_F(QueryStatsStoreTest, CorrectlyRedactsFindCommandRequestAllFields) {
@@ -313,10 +437,6 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsFindCommandRequestAllFields) {
"HASH<f>": true,
"HASH<_id>": true
},
- "hint": {
- "HASH<z>": 1,
- "HASH<c>": 1
- },
"max": {
"HASH<z>": "?number"
},
@@ -328,7 +448,11 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsFindCommandRequestAllFields) {
"HASH<otherSort>": -1
}
},
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "HASH<z>": 1,
+ "HASH<c>": 1
+ }
})",
key);
@@ -363,10 +487,6 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsFindCommandRequestAllFields) {
"HASH<f>": true,
"HASH<_id>": true
},
- "hint": {
- "HASH<z>": 1,
- "HASH<c>": 1
- },
"max": {
"HASH<z>": "?number"
},
@@ -382,7 +502,11 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsFindCommandRequestAllFields) {
},
"maxTimeMS": "?number",
"batchSize": "?number",
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "HASH<z>": 1,
+ "HASH<c>": 1
+ }
})",
key);
@@ -417,10 +541,6 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsFindCommandRequestAllFields) {
"HASH<f>": true,
"HASH<_id>": true
},
- "hint": {
- "HASH<z>": 1,
- "HASH<c>": 1
- },
"max": {
"HASH<z>": "?number"
},
@@ -441,7 +561,11 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsFindCommandRequestAllFields) {
"allowPartialResults": true,
"maxTimeMS": "?number",
"batchSize": "?number",
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "HASH<z>": 1,
+ "HASH<c>": 1
+ }
})",
key);
@@ -470,10 +594,6 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsFindCommandRequestAllFields) {
"HASH<f>": true,
"HASH<_id>": true
},
- "hint": {
- "HASH<z>": 1,
- "HASH<c>": 1
- },
"max": {
"HASH<z>": "?number"
},
@@ -494,7 +614,11 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsFindCommandRequestAllFields) {
"allowPartialResults": false,
"maxTimeMS": "?number",
"batchSize": "?number",
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "HASH<z>": 1,
+ "HASH<c>": 1
+ }
})",
key);
@@ -512,13 +636,13 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsFindCommandRequestAllFields) {
},
"command": "find",
"filter": {},
- "hint": {
- "$natural": 1
- },
"tailable": true,
"awaitData": true
},
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "$natural": 1
+ }
})",
key);
}
@@ -570,10 +694,6 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsHintsWithOptions) {
"$eq": "?number"
}
},
- "hint": {
- "z": 1,
- "c": 1
- },
"max": {
"z": "?number"
},
@@ -581,7 +701,11 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsHintsWithOptions) {
"z": "?number"
}
},
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "z": 1,
+ "c": 1
+ }
})",
key);
// Test with a string hint. Note that this is the internal representation of the string hint
@@ -603,9 +727,6 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsHintsWithOptions) {
"$eq": "?number"
}
},
- "hint": {
- "$hint": "z"
- },
"max": {
"z": "?number"
},
@@ -613,7 +734,10 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsHintsWithOptions) {
"z": "?number"
}
},
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "$hint": "z"
+ }
})",
key);
@@ -632,10 +756,6 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsHintsWithOptions) {
"$eq": "?number"
}
},
- "hint": {
- "HASH<z>": 1,
- "HASH<c>": 1
- },
"max": {
"HASH<z>": "?number"
},
@@ -643,7 +763,11 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsHintsWithOptions) {
"HASH<z>": "?number"
}
},
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "HASH<z>": 1,
+ "HASH<c>": 1
+ }
})",
key);
@@ -663,9 +787,6 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsHintsWithOptions) {
"$eq": "?number"
}
},
- "hint": {
- "$natural": -1
- },
"max": {
"HASH<z>": "?number"
},
@@ -673,7 +794,10 @@ TEST_F(QueryStatsStoreTest, CorrectlyRedactsHintsWithOptions) {
"HASH<z>": "?number"
}
},
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "$natural": -1
+ }
})",
key);
}
@@ -758,7 +882,8 @@ TEST_F(QueryStatsStoreTest, DefinesLetVariables) {
"adaJc6H3zDirh5/52MLv5yvnb6nXNP15Z4HzGfumvx8=": "?number"
},
"projection": {
- "BL649QER7lTs0+8ozTMVNAa6JNjbhf57YT8YQ4EkT1E=": "$$adaJc6H3zDirh5/52MLv5yvnb6nXNP15Z4HzGfumvx8=",
+ "BL649QER7lTs0+8ozTMVNAa6JNjbhf57YT8YQ4EkT1E=":
+ "$$adaJc6H3zDirh5/52MLv5yvnb6nXNP15Z4HzGfumvx8=",
"ljovqLSfuj6o2syO1SynOzHQK1YVij6+Wlx1fL8frUo=": true
}
},
@@ -967,13 +1092,13 @@ TEST_F(QueryStatsStoreTest, CorrectlyTokenizesAggregateCommandRequestAllFieldsSi
"allowDiskUse": false,
"collation": {
"locale": "simple"
- },
- "hint": {
- "HASH<z>": 1,
- "HASH<c>": 1
}
},
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "HASH<z>": 1,
+ "HASH<c>": 1
+ }
})",
shapified);
@@ -1040,16 +1165,16 @@ TEST_F(QueryStatsStoreTest, CorrectlyTokenizesAggregateCommandRequestAllFieldsSi
"collation": {
"locale": "simple"
},
- "hint": {
- "HASH<z>": 1,
- "HASH<c>": 1
- },
"let": {
"HASH<var1>": "$HASH<foo>",
"HASH<var2>": "?string"
}
},
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "HASH<z>": 1,
+ "HASH<c>": 1
+ }
})",
shapified);
@@ -1119,10 +1244,6 @@ TEST_F(QueryStatsStoreTest, CorrectlyTokenizesAggregateCommandRequestAllFieldsSi
"collation": {
"locale": "simple"
},
- "hint": {
- "HASH<z>": 1,
- "HASH<c>": 1
- },
"let": {
"HASH<var1>": "$HASH<foo>",
"HASH<var2>": "?string"
@@ -1134,7 +1255,11 @@ TEST_F(QueryStatsStoreTest, CorrectlyTokenizesAggregateCommandRequestAllFieldsSi
"maxTimeMS": "?number",
"bypassDocumentValidation": "?bool",
"comment": "?string",
- "collectionType": "collection"
+ "collectionType": "collection",
+ "hint": {
+ "HASH<z>": 1,
+ "HASH<c>": 1
+ }
})",
shapified);
}
diff --git a/src/mongo/db/query/rate_limiting.cpp b/src/mongo/db/query/query_stats/rate_limiting.cpp
index 3aee39facae..029a42b9bdb 100644
--- a/src/mongo/db/query/rate_limiting.cpp
+++ b/src/mongo/db/query/query_stats/rate_limiting.cpp
@@ -28,10 +28,14 @@
*/
#include "rate_limiting.h"
+#include "mongo/stdx/mutex.h"
+#include "mongo/util/clock_source.h"
namespace mongo {
-RateLimiting::RateLimiting(RequestCount samplingRate, Milliseconds timePeriod)
- : _clockSource(SystemClockSource::get()),
+RateLimiting::RateLimiting(RequestCount samplingRate,
+ Milliseconds timePeriod,
+ ClockSource* clockSource)
+ : _clockSource(clockSource != nullptr ? clockSource : SystemClockSource::get()),
_samplingRate(samplingRate),
_timePeriod(timePeriod),
_windowStart(_clockSource->now()),
@@ -62,6 +66,7 @@ bool RateLimiting::handleRequestFixedWindow() {
}
bool RateLimiting::handleRequestSlidingWindow() {
+ // TODO SERVER-80006: Determine the best RAII type to hold the lock.
stdx::unique_lock windowLock{_windowMutex};
Date_t currentTime = tickWindow();
diff --git a/src/mongo/db/query/rate_limiting.h b/src/mongo/db/query/query_stats/rate_limiting.h
index 67b8a7fc106..66e38d7119b 100644
--- a/src/mongo/db/query/rate_limiting.h
+++ b/src/mongo/db/query/query_stats/rate_limiting.h
@@ -30,6 +30,7 @@
#pragma once
#include "mongo/util/clock_source.h"
+#include "mongo/util/concurrency/mutex.h"
#include "mongo/util/system_clock_source.h"
namespace mongo {
@@ -47,7 +48,9 @@ public:
* Constructor for a rate limiter. Specify the number of requests you want to take place, as
* well as the time period in milliseconds.
*/
- RateLimiting(RequestCount samplingRate, Milliseconds timePeriod = Seconds{1});
+ RateLimiting(RequestCount samplingRate,
+ Milliseconds timePeriod = Seconds{1},
+ ClockSource* clockSource = nullptr);
/*
* Getter for the sampling rate.
@@ -118,6 +121,6 @@ private:
/*
* Mutex used when reading/writing the window.
*/
- stdx::recursive_mutex _windowMutex;
+ SimpleMutex _windowMutex;
};
} // namespace mongo
diff --git a/src/mongo/db/query/query_stats/rate_limiting_bm.cpp b/src/mongo/db/query/query_stats/rate_limiting_bm.cpp
new file mode 100644
index 00000000000..540c3c63486
--- /dev/null
+++ b/src/mongo/db/query/query_stats/rate_limiting_bm.cpp
@@ -0,0 +1,144 @@
+/**
+ * Copyright (C) 2023-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+
+#include <benchmark/benchmark.h>
+#include <climits>
+#include <memory>
+
+#include "mongo/bson/json.h"
+#include "mongo/db/matcher/expression_leaf.h"
+#include "mongo/db/matcher/expression_parser.h"
+#include "mongo/db/query/query_shape.h"
+#include "mongo/db/query/query_stats/rate_limiting.h"
+#include "mongo/util/duration.h"
+#include "mongo/util/processinfo.h"
+#include "mongo/util/time_support.h"
+
+namespace mongo {
+namespace {
+
+// Local testing determined that these parameter values drove the most lock contention, which is
+// what we want to capture in this benchmark.
+constexpr long long rateLimitedWorkTimeMicros = 5;
+constexpr long long consistentWorkTimeMicros = 10;
+
+constexpr long long numThreads = 256;
+
+// Rate limit some fraction of the overall work for a request with a sliding window.
+int requestWithSlidingWindow(RateLimiting& limit) {
+ if (limit.handleRequestSlidingWindow()) {
+ sleepmicros(rateLimitedWorkTimeMicros);
+ }
+ sleepmicros(consistentWorkTimeMicros);
+ return 0;
+}
+
+// Represent a request that bypasses the rate limiter.
+int requestUnlimited() {
+ constexpr long long totalTime = rateLimitedWorkTimeMicros + consistentWorkTimeMicros;
+ sleepmicros(totalTime);
+ return 0;
+}
+
+// Represent a request without the rate limited work.
+int requestDeactivated() {
+ sleepmicros(consistentWorkTimeMicros);
+ return 0;
+}
+
+// Benchmark sliding window rate limiting.
+void BM_SlidingWindow(benchmark::State& state) {
+ // The rate limiter needs a clock source passed in.
+ static std::unique_ptr<ClockSource> clockSource;
+ static std::unique_ptr<RateLimiting> rateLimit;
+
+ // Initialize the rate limiter only on the first thread to start up.
+ if (state.thread_index == 0) {
+ clockSource = std::make_unique<SystemClockSource>();
+ rateLimit =
+ std::make_unique<RateLimiting>(state.range(0), Milliseconds(1), clockSource.get());
+ }
+
+ // Run the benchmark.
+ for (auto keepRunning : state) {
+ benchmark::DoNotOptimize(requestWithSlidingWindow(*rateLimit));
+ }
+
+ // Clean up the rate limiter when the benchmark is done.
+ if (state.thread_index == 0) {
+ rateLimit.reset();
+ clockSource.reset();
+ }
+}
+
+// "Control" benchmark that does not rate limit requests. In other words, the extra work is always
+// done for every request. This benchmark can be thought of as the "goal" performance for the peak,
+// or the highest rate limit in BM_SlidingWindow, to compare against.
+void BM_Unlimited(benchmark::State& state) {
+ for (auto keepRunning : state) {
+ benchmark::DoNotOptimize(requestUnlimited());
+ }
+}
+// Another control benchmark, where the extra work is never done for any request. This can be
+// thought of as the goal performance for when rate limit equals 0.
+void BM_Deactivated(benchmark::State& state) {
+ for (auto keepRunning : state) {
+ benchmark::DoNotOptimize(requestDeactivated());
+ }
+}
+
+// Google microbenchmarks report time T (in nanoseconds) spent per operation. But at Mongo we are
+// interested in total opereations performed per second. The former can easily be converted to the
+// latter by diving 10^6 by T. Use this benchmark to determine the natural throughput of the
+// operation. This can be compared to the rate limited benchmarks (BM_SlidingWindow) to determine
+// the overhead of rate limiting. Looking at the percentage change in throughput between the control
+// benchmarks and the rate limited benchmark, will indicate how much overhead is due to lock
+// contention.
+BENCHMARK(BM_Unlimited)->Threads(numThreads);
+
+BENCHMARK(BM_Deactivated)->Threads(numThreads);
+
+// Local testing has confirmed that the higher the rate limit, the worse the throughput. This makes
+// sense as putting a higher upper bound on number of requests allowed in a given time period, means
+// longer wait times for the lock.
+BENCHMARK(BM_SlidingWindow)
+ ->ArgName("rate limit")
+ ->Arg(0)
+ ->Arg(64)
+ ->Arg(128)
+ ->Arg(256)
+ ->Arg(512)
+ ->Arg(1024)
+ ->Arg(2048)
+ ->Arg(4816)
+ ->Threads(numThreads);
+
+} // namespace
+} // namespace mongo
diff --git a/src/mongo/db/query/rate_limiting_test.cpp b/src/mongo/db/query/query_stats/rate_limiting_test.cpp
index 2d9ef35647a..380636a2a20 100644
--- a/src/mongo/db/query/rate_limiting_test.cpp
+++ b/src/mongo/db/query/query_stats/rate_limiting_test.cpp
@@ -27,7 +27,7 @@
* it in the license file.
*/
-#include "mongo/db/query/rate_limiting.h"
+#include "mongo/db/query/query_stats/rate_limiting.h"
#include "mongo/unittest/unittest.h"
#include "mongo/util/time_support.h"
diff --git a/src/mongo/db/query/query_stats/shapifying_bm.cpp b/src/mongo/db/query/query_stats/shapifying_bm.cpp
new file mode 100644
index 00000000000..b7f5715233a
--- /dev/null
+++ b/src/mongo/db/query/query_stats/shapifying_bm.cpp
@@ -0,0 +1,143 @@
+/**
+ * Copyright (C) 2023-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+
+#include <benchmark/benchmark.h>
+#include <climits>
+#include <memory>
+
+#include "mongo/bson/json.h"
+#include "mongo/db/matcher/expression_leaf.h"
+#include "mongo/db/matcher/expression_parser.h"
+#include "mongo/db/pipeline/expression_context_for_test.h"
+#include "mongo/db/query/find_command.h"
+#include "mongo/db/query/query_shape.h"
+#include "mongo/db/query/query_stats/find_key_generator.h"
+#include "mongo/db/query/query_stats/query_stats.h"
+#include "mongo/db/query/query_stats/rate_limiting.h"
+#include "mongo/idl/server_parameter_test_util.h"
+#include "mongo/rpc/metadata/client_metadata.h"
+#include "mongo/util/duration.h"
+#include "mongo/util/processinfo.h"
+#include "mongo/util/testing_proctor.h"
+#include "mongo/util/time_support.h"
+
+namespace mongo {
+namespace {
+
+static const NamespaceStringOrUUID kDefaultTestNss =
+ NamespaceStringOrUUID{NamespaceString::createNamespaceString_forTest("testDB.testColl")};
+
+static constexpr auto kCollectionType = query_shape::CollectionType::kCollection;
+
+// This is a snapshot of the client metadata generated from our IDHACK genny workload. The
+// specifics aren't so important, but it chosen in an attempt to be indicative of the size/shape
+// of this kind of thing "in the wild".
+const auto kMetadataWrapper = fromjson(R"({metadata: {
+ "application" : {
+ "name" : "Genny"
+ },
+ "driver" : {
+ "name" : "mongoc / mongocxx",
+ "version" : "1.23.2 / 3.7.0"
+ },
+ "os" : {
+ "type" : "Linux",
+ "name" : "Ubuntu",
+ "version" : "22.04",
+ "architecture" : "aarch64"
+ },
+ "platform" : "cfg=0x03215e88e9 posix=200809 stdc=201710 CC=GCC 11.3.0 CFLAGS=\"-fPIC\" LDFLAGS=\"\""
+ }})");
+auto kMockClientMetadataElem = kMetadataWrapper["metadata"];
+
+auto makeFindKeyGenerator(const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const ParsedFindCommand& parsedFind) {
+ return std::make_unique<query_stats::FindKeyGenerator>(
+ expCtx,
+ parsedFind,
+ query_shape::extractQueryShape(
+ parsedFind, SerializationOptions::kRepresentativeQueryShapeSerializeOptions, expCtx),
+ kCollectionType);
+}
+
+int shapifyAndHashRequest(const boost::intrusive_ptr<ExpressionContext>& expCtx,
+ const ParsedFindCommand& parsedFind) {
+ auto keyGenerator = makeFindKeyGenerator(expCtx, parsedFind);
+ [[maybe_unused]] auto hash = keyGenerator->hash();
+ return 0;
+}
+
+// Benchmark the performance of computing and hashing the query stats key for an IDHACK query.
+void BM_ShapfiyIDHack(benchmark::State& state) {
+ auto serviceCtx = ServiceContext::make();
+ auto client = serviceCtx->makeClient("query_test");
+
+ auto opCtx = client->makeOperationContext();
+ auto expCtx = make_intrusive<ExpressionContextForTest>(opCtx.get());
+ auto fcr = std::make_unique<FindCommandRequest>(expCtx->ns);
+ fcr->setFilter(fromjson("{_id: 4}"));
+ ClientMetadata::setFromMetadata(opCtx->getClient(), kMockClientMetadataElem, false);
+ auto parsedFind = uassertStatusOK(parsed_find_command::parse(expCtx, std::move(fcr)));
+
+ // Run the benchmark.
+ for (auto keepRunning : state) {
+ benchmark::DoNotOptimize(shapifyAndHashRequest(expCtx, *parsedFind));
+ }
+}
+
+// Benchmark computing the query stats key and its hash for a mildly complex query predicate.
+void BM_ShapfiyMildlyComplex(benchmark::State& state) {
+ auto serviceCtx = ServiceContext::make();
+ auto client = serviceCtx->makeClient("query_test");
+
+ auto opCtx = client->makeOperationContext();
+ auto expCtx = make_intrusive<ExpressionContextForTest>(opCtx.get());
+ auto fcr = std::make_unique<FindCommandRequest>(expCtx->ns);
+ fcr->setFilter(fromjson(R"({
+ clientId: {$nin: ["432345", "4386945", "111111"]},
+ nEmployees: {$gte: 4, $lt: 20},
+ deactivated: false,
+ region: "US",
+ yearlySpend: {$lte: 1000}
+ })"));
+ ClientMetadata::setFromMetadata(opCtx->getClient(), kMockClientMetadataElem, false);
+ auto parsedFind = uassertStatusOK(parsed_find_command::parse(expCtx, std::move(fcr)));
+
+ // Run the benchmark.
+ for (auto keepRunning : state) {
+ benchmark::DoNotOptimize(shapifyAndHashRequest(expCtx, *parsedFind));
+ }
+}
+
+BENCHMARK(BM_ShapfiyIDHack)->Threads(1);
+BENCHMARK(BM_ShapfiyMildlyComplex)->Threads(1);
+
+} // namespace
+} // namespace mongo
diff --git a/src/mongo/db/query/query_stats_transform_algorithm.idl b/src/mongo/db/query/query_stats/transform_algorithm.idl
index cd0a5ba43db..cd0a5ba43db 100644
--- a/src/mongo/db/query/query_stats_transform_algorithm.idl
+++ b/src/mongo/db/query/query_stats/transform_algorithm.idl
diff --git a/src/mongo/db/query/query_stats_util.cpp b/src/mongo/db/query/query_stats/util.cpp
index 4c102d983dc..c93da189ff6 100644
--- a/src/mongo/db/query/query_stats_util.cpp
+++ b/src/mongo/db/query/query_stats/util.cpp
@@ -27,7 +27,7 @@
* it in the license file.
*/
-#include "mongo/db/query/query_stats_util.h"
+#include "mongo/db/query/query_stats/util.h"
#include "mongo/base/status.h"
#include "mongo/db/concurrency/d_concurrency.h"
diff --git a/src/mongo/db/query/query_stats_util.h b/src/mongo/db/query/query_stats/util.h
index ebd8f1e2fbd..ebd8f1e2fbd 100644
--- a/src/mongo/db/query/query_stats_util.h
+++ b/src/mongo/db/query/query_stats/util.h
diff --git a/src/mongo/db/query/serialization_options.cpp b/src/mongo/db/query/serialization_options.cpp
index 2a6ba72def4..271d6cf2d49 100644
--- a/src/mongo/db/query/serialization_options.cpp
+++ b/src/mongo/db/query/serialization_options.cpp
@@ -87,6 +87,21 @@ static const StringMap<StringData> kArrayTypeStringConstants{
{kMaxKeyTypeString.rawData(), "?array<?maxKey>"_sd},
};
+static constexpr auto kRepresentativeString = "?"_sd;
+static constexpr auto kRepresentativeNumber = 1;
+static const auto kRepresentativeObject = BSON("?"
+ << "?");
+static const auto kRepresentativeArray = BSONArray();
+static constexpr auto kRepresentativeBinData = BSONBinData();
+static const auto kRepresentativeObjectId = OID::max();
+static constexpr auto kRepresentativeBool = true;
+static const auto kRepresentativeDate = Date_t::fromMillisSinceEpoch(0);
+static const auto kRepresentativeRegex = BSONRegEx("/\?/");
+static const auto kRepresentativeDbPointer = BSONDBRef("?.?", OID::max());
+static const auto kRepresentativeJavascript = BSONCode("return ?;");
+static const auto kRepresentativeJavascriptWithScope = BSONCodeWScope("return ?;", BSONObj());
+static const auto kRepresentativeTimestamp = Timestamp::min();
+
/**
* Computes a debug string meant to represent "any value of type t", where "t" is the type of the
* provided argument. For example "?number" for any number (int, double, etc.).
@@ -154,39 +169,39 @@ ImplicitValue defaultLiteralOfType(BSONType t) {
return BSONUndefined;
case Symbol:
case String:
- return "?"_sd;
+ return kRepresentativeString;
case NumberInt:
case NumberLong:
case NumberDouble:
case NumberDecimal:
- return 1;
+ return kRepresentativeNumber;
case MinKey:
return MINKEY;
case Object:
- return Document{{"?"_sd, "?"_sd}};
+ return kRepresentativeObject;
case Array:
// This case should only happen if we have an array within an array.
- return BSONArray();
+ return kRepresentativeArray;
case BinData:
- return BSONBinData();
+ return kRepresentativeBinData;
case jstOID:
- return OID::max();
+ return kRepresentativeObjectId;
case Bool:
- return true;
+ return kRepresentativeBool;
case Date:
- return Date_t::fromMillisSinceEpoch(0);
+ return kRepresentativeDate;
case jstNULL:
return BSONNULL;
case RegEx:
- return BSONRegEx("/\?/");
+ return kRepresentativeRegex;
case DBRef:
- return BSONDBRef("?.?", OID::max());
+ return kRepresentativeDbPointer;
case Code:
- return BSONCode("return ?;");
+ return kRepresentativeJavascript;
case CodeWScope:
- return BSONCodeWScope("return ?;", BSONObj());
+ return kRepresentativeJavascriptWithScope;
case bsonTimestamp:
- return Timestamp::min();
+ return kRepresentativeTimestamp;
case MaxKey:
return MAXKEY;
default:
@@ -306,6 +321,68 @@ ArraySubtypeInfo getSubTypeFromValueArray(const Value& arrayVal) {
return determineArraySubType(arrayVal.getArray());
}
+void appendDefaultOfNonArrayType(BSONObjBuilder* bob, StringData name, const BSONElement& e) {
+ switch (e.type()) {
+ case EOO:
+ case Undefined:
+ bob->appendUndefined(name);
+ return;
+ case Symbol:
+ case String:
+ bob->append(name, kRepresentativeString);
+ return;
+ case NumberInt:
+ case NumberLong:
+ case NumberDouble:
+ case NumberDecimal:
+ bob->append(name, kRepresentativeNumber);
+ return;
+ case MinKey:
+ bob->appendMinKey(name);
+ return;
+ case Object:
+ bob->append(name, kRepresentativeObject);
+ return;
+ case Array:
+ // This case is more complicated and callers should use a more generic helper.
+ MONGO_UNREACHABLE_TASSERT(8094100);
+ case BinData:
+ bob->append(name, kRepresentativeBinData);
+ return;
+ case jstOID:
+ bob->append(name, kRepresentativeObjectId);
+ return;
+ case Bool:
+ bob->append(name, kRepresentativeBool);
+ return;
+ case Date:
+ bob->append(name, kRepresentativeDate);
+ return;
+ case jstNULL:
+ bob->appendNull(name);
+ return;
+ case RegEx:
+ bob->append(name, kRepresentativeRegex);
+ return;
+ case DBRef:
+ bob->append(name, kRepresentativeDbPointer);
+ return;
+ case Code:
+ bob->append(name, kRepresentativeJavascript);
+ return;
+ case CodeWScope:
+ bob->append(name, kRepresentativeJavascriptWithScope);
+ return;
+ case bsonTimestamp:
+ bob->append(name, kRepresentativeTimestamp);
+ return;
+ case MaxKey:
+ bob->appendMaxKey(name);
+ return;
+ default:
+ MONGO_UNREACHABLE_TASSERT(8094101);
+ };
+}
} // namespace
const SerializationOptions SerializationOptions::kRepresentativeQueryShapeSerializeOptions =
@@ -332,7 +409,34 @@ ImplicitValue defaultLiteralOfType(BSONElement e) {
}
void SerializationOptions::appendLiteral(BSONObjBuilder* bob, const BSONElement& e) const {
- serializeLiteral(e).addToBsonObj(bob, e.fieldNameStringData());
+ appendLiteral(bob, e.fieldNameStringData(), e);
+}
+void SerializationOptions::appendLiteral(BSONObjBuilder* bob,
+ StringData name,
+ const BSONElement& e) const {
+ // The first two cases are particularly performance sensitive. We could answer everything here
+ // with the code inside the 'kToDebugTypeString' branch, but there are some relatively easy ways
+ // to accomplish the first two policy cases (in the common cases), so we'll special case those
+ // in order to avoid constructing a temporary Value.
+ switch (literalPolicy) {
+ case LiteralSerializationPolicy::kUnchanged:
+ bob->appendAs(e, name);
+ return;
+ case LiteralSerializationPolicy::kToRepresentativeParseableValue: {
+ if (e.type() != BSONType::Array) {
+ appendDefaultOfNonArrayType(bob, name, e);
+ return;
+ }
+ // If it's an array we'll default to the slow but general codepath below.
+ [[fallthrough]];
+ }
+ case LiteralSerializationPolicy::kToDebugTypeString: {
+ // Performance isn't as sensitive here.
+ return serializeLiteral(e).addToBsonObj(bob, name);
+ }
+ default:
+ MONGO_UNREACHABLE_TASSERT(8094102);
+ }
}
void SerializationOptions::appendLiteral(BSONObjBuilder* bob,
diff --git a/src/mongo/db/query/serialization_options.h b/src/mongo/db/query/serialization_options.h
index 1661bc88819..f6bbc210a34 100644
--- a/src/mongo/db/query/serialization_options.h
+++ b/src/mongo/db/query/serialization_options.h
@@ -173,6 +173,7 @@ struct SerializationOptions {
* using the same name as 'e'.
*/
void appendLiteral(BSONObjBuilder* bob, const BSONElement& e) const;
+ void appendLiteral(BSONObjBuilder* bob, StringData name, const BSONElement& e) const;
/**
* Helper method to call 'serializeLiteral()' on 'v' and append the result to 'bob' using field
* name 'fieldName'.
diff --git a/src/mongo/db/query/shape_helpers.cpp b/src/mongo/db/query/shape_helpers.cpp
new file mode 100644
index 00000000000..6a5601d2362
--- /dev/null
+++ b/src/mongo/db/query/shape_helpers.cpp
@@ -0,0 +1,112 @@
+/**
+ * Copyright (C) 2023-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#include "mongo/db/query/shape_helpers.h"
+
+#include "mongo/db/query/query_shape_gen.h"
+
+namespace mongo::shape_helpers {
+
+static constexpr StringData hintSpecialField = "$hint"_sd;
+// A "Flat" object is one with only top-level fields. We won't descend recursively to shapify any
+// sub-objects.
+BSONObj shapifyFlatObj(BSONObj obj, const SerializationOptions& opts, bool valuesAreLiterals) {
+ if (obj.isEmpty()) {
+ // fast-path for the common case.
+ return obj;
+ }
+
+ BSONObjBuilder bob;
+ for (BSONElement elem : obj) {
+ if (hintSpecialField.compare(elem.fieldNameStringData()) == 0) {
+ if (elem.type() == BSONType::String) {
+ bob.append(hintSpecialField, opts.serializeFieldPathFromString(elem.String()));
+ } else if (elem.type() == BSONType::Object) {
+ opts.appendLiteral(&bob, hintSpecialField, elem.Obj());
+ } else {
+ uasserted(ErrorCodes::FailedToParse, "$hint must be a string or an object");
+ }
+ continue;
+ }
+
+ // $natural doesn't need to be redacted.
+ if (elem.fieldNameStringData().compare(query_request_helper::kNaturalSortField) == 0) {
+ bob.append(elem);
+ continue;
+ }
+
+ if (valuesAreLiterals) {
+ opts.appendLiteral(&bob, opts.serializeFieldPathFromString(elem.fieldName()), elem);
+ } else {
+ bob.appendAs(elem, opts.serializeFieldPathFromString(elem.fieldName()));
+ }
+ }
+ return bob.obj();
+}
+
+BSONObj extractHintShape(BSONObj hintObj, const SerializationOptions& opts) {
+ return shapifyFlatObj(hintObj, opts, /* valuesAreLiterals = */ false);
+}
+
+BSONObj extractMinOrMaxShape(BSONObj obj, const SerializationOptions& opts) {
+ return shapifyFlatObj(obj, opts, /* valuesAreLiterals = */ true);
+}
+
+void appendNamespaceShape(BSONObjBuilder& bob,
+ const NamespaceString& nss,
+ const SerializationOptions& opts) {
+ if (nss.tenantId()) {
+ bob.append("tenantId", opts.serializeIdentifier(nss.tenantId().value().toString()));
+ }
+ bob.append("db", opts.serializeIdentifier(nss.db()));
+ bob.append("coll", opts.serializeIdentifier(nss.coll()));
+}
+
+NamespaceStringOrUUID parseNamespaceShape(BSONElement cmdNsElt) {
+ tassert(7632900, "cmdNs must be an object.", cmdNsElt.type() == BSONType::Object);
+ auto cmdNs =
+ query_shape::CommandNamespace::parse(IDLParserContext("cmdNs"), cmdNsElt.embeddedObject());
+
+ boost::optional<TenantId> tenantId = cmdNs.getTenantId().map(TenantId::parseFromString);
+
+ if (cmdNs.getColl().has_value()) {
+ tassert(7632903,
+ "Exactly one of 'uuid' and 'coll' can be defined.",
+ !cmdNs.getUuid().has_value());
+ return NamespaceString(cmdNs.getDb(), cmdNs.getColl().value());
+ } else {
+ tassert(7632904,
+ "Exactly one of 'uuid' and 'coll' can be defined.",
+ !cmdNs.getColl().has_value());
+ UUID uuid = uassertStatusOK(UUID::parse(cmdNs.getUuid().value().toString()));
+ return NamespaceStringOrUUID(cmdNs.getDb().toString(), uuid, tenantId);
+ }
+}
+
+} // namespace mongo::shape_helpers
diff --git a/src/mongo/db/query/shape_helpers.h b/src/mongo/db/query/shape_helpers.h
new file mode 100644
index 00000000000..1f0e8d72389
--- /dev/null
+++ b/src/mongo/db/query/shape_helpers.h
@@ -0,0 +1,49 @@
+/**
+ * Copyright (C) 2023-present MongoDB, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the Server Side Public License, version 1,
+ * as published by MongoDB, Inc.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * Server Side Public License for more details.
+ *
+ * You should have received a copy of the Server Side Public License
+ * along with this program. If not, see
+ * <http://www.mongodb.com/licensing/server-side-public-license>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the Server Side Public License in all respects for
+ * all of the code used other than as permitted herein. If you modify file(s)
+ * with this exception, you may extend this exception to your version of the
+ * file(s), but you are not obligated to do so. If you do not wish to do so,
+ * delete this exception statement from your version. If you delete this
+ * exception statement from all source files in the program, then also delete
+ * it in the license file.
+ */
+
+#pragma once
+
+#include "mongo/db/query/query_shape.h"
+
+namespace mongo::shape_helpers {
+
+/**
+ * Serializes the given 'hintObj' in accordance with the options. Assumes the hint is correct and
+ * contains field names. It is possible that this hint doesn't actually represent an index, but we
+ * can't detect that here.
+ */
+BSONObj extractHintShape(BSONObj hintObj, const SerializationOptions& opts);
+BSONObj extractMinOrMaxShape(BSONObj obj, const SerializationOptions& opts);
+
+NamespaceStringOrUUID parseNamespaceShape(BSONElement cmdNsElt);
+void appendNamespaceShape(BSONObjBuilder& bob,
+ const NamespaceString& nss,
+ const SerializationOptions& opts);
+
+} // namespace mongo::shape_helpers
diff --git a/src/mongo/db/query/stats/SConscript b/src/mongo/db/query/stats/SConscript
index 13457cccaea..c9c3e3a59ec 100644
--- a/src/mongo/db/query/stats/SConscript
+++ b/src/mongo/db/query/stats/SConscript
@@ -5,7 +5,7 @@ Import("env")
env = env.Clone()
env.Library(
- target="query_stats",
+ target="stats",
source=[
'collection_statistics_impl.cpp',
'stats_catalog.cpp',
diff --git a/src/mongo/db/repl/SConscript b/src/mongo/db/repl/SConscript
index ac3f69518f4..a54d6d0dfe8 100644
--- a/src/mongo/db/repl/SConscript
+++ b/src/mongo/db/repl/SConscript
@@ -631,7 +631,7 @@ env.Library(
],
LIBDEPS=[
'$BUILD_DIR/mongo/db/auth/authorization_manager_global',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/query_exec',
'$BUILD_DIR/mongo/db/stats/timer_stats',
'$BUILD_DIR/mongo/db/storage/storage_options',
@@ -1578,7 +1578,7 @@ env.Library(
'$BUILD_DIR/mongo/db/concurrency/lock_manager',
'$BUILD_DIR/mongo/db/not_primary_error_tracker',
'$BUILD_DIR/mongo/db/op_observer/op_observer',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/query_exec',
'$BUILD_DIR/mongo/db/server_base',
'$BUILD_DIR/mongo/db/service_context',
diff --git a/src/mongo/db/s/SConscript b/src/mongo/db/s/SConscript
index 6921f8424bd..8e7ed846248 100644
--- a/src/mongo/db/s/SConscript
+++ b/src/mongo/db/s/SConscript
@@ -29,7 +29,7 @@ env.Library(
'$BUILD_DIR/mongo/db/concurrency/exception_util',
'$BUILD_DIR/mongo/db/dbhelpers',
'$BUILD_DIR/mongo/db/ops/write_ops',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/shard_role',
'$BUILD_DIR/mongo/db/shard_role_api',
'$BUILD_DIR/mongo/s/common_s',
diff --git a/src/mongo/db/service_context.cpp b/src/mongo/db/service_context.cpp
index c2b7f11898c..62f6a32a92c 100644
--- a/src/mongo/db/service_context.cpp
+++ b/src/mongo/db/service_context.cpp
@@ -256,7 +256,10 @@ ServiceContext::UniqueOperationContext ServiceContext::makeOperationContext(Clie
onCreate(opCtx.get(), _clientObservers);
ScopeGuard onCreateGuard([&] { onDestroy(opCtx.get(), _clientObservers); });
- invariant(opCtx->lockState(), ProcessInfo().getProcessName());
+ invariant(
+ opCtx->lockState(),
+ str::stream() << "No lock state configured. This could be a missing build dependency. "
+ << ProcessInfo().getProcessName());
if (!opCtx->recoveryUnit()) {
opCtx->setRecoveryUnit(std::make_unique<RecoveryUnitNoop>(),
diff --git a/src/mongo/db/stats/SConscript b/src/mongo/db/stats/SConscript
index ce98fc6ee13..ec7bc1490d0 100644
--- a/src/mongo/db/stats/SConscript
+++ b/src/mongo/db/stats/SConscript
@@ -21,7 +21,7 @@ env.Library(
'operation_latency_histogram.cpp',
],
LIBDEPS=[
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/server_base',
'$BUILD_DIR/mongo/db/service_context',
'$BUILD_DIR/mongo/util/namespace_string_database_name_util',
diff --git a/src/mongo/db/storage/kv/SConscript b/src/mongo/db/storage/kv/SConscript
index 3c3b9237b97..8b2ec215195 100644
--- a/src/mongo/db/storage/kv/SConscript
+++ b/src/mongo/db/storage/kv/SConscript
@@ -11,7 +11,7 @@ env.Library(
LIBDEPS=[
'$BUILD_DIR/mongo/db/concurrency/exception_util',
'$BUILD_DIR/mongo/db/concurrency/lock_manager',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/storage/write_unit_of_work',
],
)
diff --git a/src/mongo/db/storage/wiredtiger/SConscript b/src/mongo/db/storage/wiredtiger/SConscript
index f752ee0dd60..56345c5b40b 100644
--- a/src/mongo/db/storage/wiredtiger/SConscript
+++ b/src/mongo/db/storage/wiredtiger/SConscript
@@ -56,7 +56,7 @@ wtEnv.Library(
'$BUILD_DIR/mongo/db/global_settings',
'$BUILD_DIR/mongo/db/index/index_access_method',
'$BUILD_DIR/mongo/db/prepare_conflict_tracker',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/record_id_helpers',
'$BUILD_DIR/mongo/db/repl/repl_settings',
'$BUILD_DIR/mongo/db/server_options_servers',
diff --git a/src/mongo/db/timeseries/SConscript b/src/mongo/db/timeseries/SConscript
index b57794e8f8f..8575d3c2410 100644
--- a/src/mongo/db/timeseries/SConscript
+++ b/src/mongo/db/timeseries/SConscript
@@ -21,7 +21,7 @@ env.Library(
'timeseries_options.cpp',
],
LIBDEPS_PRIVATE=[
- '$BUILD_DIR/mongo/db/serialization_options',
+ '$BUILD_DIR/mongo/db/query_shape_common',
'$BUILD_DIR/mongo/db/server_base',
'$BUILD_DIR/mongo/db/storage/storage_options',
'$BUILD_DIR/mongo/util/processinfo',
@@ -113,7 +113,7 @@ env.Library(
'$BUILD_DIR/mongo/db/catalog/collection_query_info',
'$BUILD_DIR/mongo/db/catalog/document_validation',
'$BUILD_DIR/mongo/db/ops/write_ops_exec_util',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/record_id_helpers',
'$BUILD_DIR/mongo/db/repl/tenant_migration_decoration',
'$BUILD_DIR/mongo/db/shard_role',
diff --git a/src/mongo/logv2/log_component.h b/src/mongo/logv2/log_component.h
index 65647b00771..e0da86ac55f 100644
--- a/src/mongo/logv2/log_component.h
+++ b/src/mongo/logv2/log_component.h
@@ -62,6 +62,7 @@ namespace mongo::logv2 {
X(kNetwork, , "network" , "NETWORK" , kDefault) \
X(kProcessHealth, , "processHealth" , "HEALTH" , kDefault) \
X(kQuery, , "query" , "QUERY" , kDefault) \
+ X(kQueryStats, , "queryStats" , "QRYSTATS", kDefault) \
X(kReplication, , "replication" , "REPL" , kDefault) \
X(kReplicationElection, , "election" , "ELECTION", kReplication) \
X(kReplicationHeartbeats, , "heartbeats" , "REPL_HB" , kReplication) \
diff --git a/src/mongo/s/SConscript b/src/mongo/s/SConscript
index a13c87641ab..ce9582fd785 100644
--- a/src/mongo/s/SConscript
+++ b/src/mongo/s/SConscript
@@ -104,8 +104,8 @@ env.Library(
'$BUILD_DIR/mongo/db/catalog/collection_uuid_mismatch_info',
'$BUILD_DIR/mongo/db/internal_transactions_feature_flag',
'$BUILD_DIR/mongo/db/mongohasher',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/db/query/query_planner',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/session/sessions_collection',
],
)
@@ -287,7 +287,7 @@ env.Library(
'$BUILD_DIR/mongo/db/commands/set_user_write_block_mode_idl',
'$BUILD_DIR/mongo/db/common',
'$BUILD_DIR/mongo/db/index_commands_idl',
- '$BUILD_DIR/mongo/db/serialization_options',
+ '$BUILD_DIR/mongo/db/query_shape_common',
'$BUILD_DIR/mongo/rpc/message',
'$BUILD_DIR/mongo/util/caching',
'analyze_shard_key_common',
@@ -468,7 +468,7 @@ env.Library(
'$BUILD_DIR/mongo/db/logical_time_metadata_hook',
'$BUILD_DIR/mongo/db/pipeline/process_interface/mongos_process_interface_factory',
'$BUILD_DIR/mongo/db/process_health/fault_manager',
- '$BUILD_DIR/mongo/db/query/op_metrics',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/read_write_concern_defaults',
'$BUILD_DIR/mongo/db/server_options',
'$BUILD_DIR/mongo/db/server_options_base',
diff --git a/src/mongo/s/commands/cluster_find_cmd.h b/src/mongo/s/commands/cluster_find_cmd.h
index ee246c26870..adc429e1163 100644
--- a/src/mongo/s/commands/cluster_find_cmd.h
+++ b/src/mongo/s/commands/cluster_find_cmd.h
@@ -40,8 +40,8 @@
#include "mongo/db/pipeline/query_request_conversion.h"
#include "mongo/db/query/cursor_response.h"
#include "mongo/db/query/query_shape.h"
-#include "mongo/db/query/query_stats.h"
-#include "mongo/db/query/query_stats_find_key_generator.h"
+#include "mongo/db/query/query_stats/find_key_generator.h"
+#include "mongo/db/query/query_stats/query_stats.h"
#include "mongo/db/stats/counters.h"
#include "mongo/db/views/resolved_view.h"
#include "mongo/rpc/get_status_from_command_result.h"
@@ -215,14 +215,20 @@ public:
auto& parsedFind = parsedFindResult.second;
if (!_didDoFLERewrite) {
- BSONObj queryShape = query_shape::extractQueryShape(
- *parsedFind,
- SerializationOptions::kRepresentativeQueryShapeSerializeOptions,
- expCtx);
- query_stats::registerRequest(opCtx, expCtx->ns, [&]() {
- return std::make_unique<query_stats::FindKeyGenerator>(
- expCtx, *parsedFind, std::move(queryShape));
- });
+ query_stats::registerRequest(
+ opCtx,
+ expCtx->ns,
+ [&]() {
+ // This callback is either never invoked or invoked immediately within
+ // registerRequest, so use-after-move of parsedFind isn't an issue.
+ BSONObj queryShape = query_shape::extractQueryShape(
+ *parsedFind,
+ SerializationOptions::kRepresentativeQueryShapeSerializeOptions,
+ expCtx);
+ return std::make_unique<query_stats::FindKeyGenerator>(
+ expCtx, *parsedFind, std::move(queryShape));
+ },
+ /*requiresFullQueryStatsFeatureFlag*/ false);
}
auto cq = uassertStatusOK(CanonicalQuery::canonicalize(expCtx, std::move(parsedFind)));
diff --git a/src/mongo/s/query/SConscript b/src/mongo/s/query/SConscript
index 7b73be8f98a..e3c580513db 100644
--- a/src/mongo/s/query/SConscript
+++ b/src/mongo/s/query/SConscript
@@ -16,8 +16,8 @@ env.Library(
'$BUILD_DIR/mongo/db/commands',
'$BUILD_DIR/mongo/db/curop_failpoint_helpers',
'$BUILD_DIR/mongo/db/query/command_request_response',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/db/query/query_common',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/s/sharding_router_api',
'async_results_merger',
'cluster_cursor_cleanup_job',
@@ -114,8 +114,8 @@ env.Library(
'$BUILD_DIR/mongo/db/auth/auth',
'$BUILD_DIR/mongo/db/auth/authprivilege',
'$BUILD_DIR/mongo/db/generic_cursor',
- '$BUILD_DIR/mongo/db/query/op_metrics',
'$BUILD_DIR/mongo/db/query/query_knobs',
+ '$BUILD_DIR/mongo/db/query/query_stats/query_stats',
'$BUILD_DIR/mongo/db/session/kill_sessions',
'$BUILD_DIR/mongo/db/session/logical_session_cache',
'$BUILD_DIR/mongo/db/session/logical_session_id',
diff --git a/src/mongo/s/query/cluster_aggregate.cpp b/src/mongo/s/query/cluster_aggregate.cpp
index c18f98d5acb..f49b9c9505a 100644
--- a/src/mongo/s/query/cluster_aggregate.cpp
+++ b/src/mongo/s/query/cluster_aggregate.cpp
@@ -56,8 +56,9 @@
#include "mongo/db/query/explain_common.h"
#include "mongo/db/query/find_common.h"
#include "mongo/db/query/fle/server_rewrite.h"
-#include "mongo/db/query/query_stats.h"
-#include "mongo/db/query/query_stats_aggregate_key_generator.h"
+#include "mongo/db/query/query_stats/aggregate_key_generator.h"
+#include "mongo/db/query/query_stats/key_generator.h"
+#include "mongo/db/query/query_stats/query_stats.h"
#include "mongo/db/timeseries/timeseries_gen.h"
#include "mongo/db/timeseries/timeseries_options.h"
#include "mongo/db/views/resolved_view.h"
diff --git a/src/mongo/s/query/cluster_client_cursor_impl.cpp b/src/mongo/s/query/cluster_client_cursor_impl.cpp
index 3523aba0e0e..eb6ccfc4929 100644
--- a/src/mongo/s/query/cluster_client_cursor_impl.cpp
+++ b/src/mongo/s/query/cluster_client_cursor_impl.cpp
@@ -32,7 +32,7 @@
#include <memory>
#include "mongo/db/curop.h"
-#include "mongo/db/query/query_stats.h"
+#include "mongo/db/query/query_stats/query_stats.h"
#include "mongo/logv2/log.h"
#include "mongo/s/query/router_stage_limit.h"
#include "mongo/s/query/router_stage_merge.h"
diff --git a/src/mongo/s/query/cluster_client_cursor_mock.h b/src/mongo/s/query/cluster_client_cursor_mock.h
index d6bcb754a18..fbfca636533 100644
--- a/src/mongo/s/query/cluster_client_cursor_mock.h
+++ b/src/mongo/s/query/cluster_client_cursor_mock.h
@@ -33,7 +33,7 @@
#include <functional>
#include <queue>
-#include "mongo/db/query/query_stats_key_generator.h"
+#include "mongo/db/query/query_stats/key_generator.h"
#include "mongo/db/session/logical_session_id.h"
#include "mongo/s/query/cluster_client_cursor.h"
diff --git a/src/mongo/s/query/cluster_cursor_manager.cpp b/src/mongo/s/query/cluster_cursor_manager.cpp
index 20e65d08dd3..c22640f7552 100644
--- a/src/mongo/s/query/cluster_cursor_manager.cpp
+++ b/src/mongo/s/query/cluster_cursor_manager.cpp
@@ -38,7 +38,7 @@
#include "mongo/db/allocate_cursor_id.h"
#include "mongo/db/curop.h"
#include "mongo/db/query/query_knobs_gen.h"
-#include "mongo/db/query/query_stats.h"
+#include "mongo/db/query/query_stats/query_stats.h"
#include "mongo/db/session/kill_sessions_common.h"
#include "mongo/db/session/logical_session_cache.h"
#include "mongo/logv2/log.h"
diff --git a/src/mongo/s/query/cluster_find.cpp b/src/mongo/s/query/cluster_find.cpp
index 830e2f68683..773fc90ab4d 100644
--- a/src/mongo/s/query/cluster_find.cpp
+++ b/src/mongo/s/query/cluster_find.cpp
@@ -48,7 +48,7 @@
#include "mongo/db/query/find_common.h"
#include "mongo/db/query/getmore_command_gen.h"
#include "mongo/db/query/query_planner_common.h"
-#include "mongo/db/query/query_stats.h"
+#include "mongo/db/query/query_stats/query_stats.h"
#include "mongo/executor/task_executor_pool.h"
#include "mongo/logv2/log.h"
#include "mongo/platform/overflow_arithmetic.h"