summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMilena Ivanova <milena.ivanova@mongodb.com>2021-05-28 09:24:11 +0000
committerMilena Ivanova <milena.ivanova@mongodb.com>2021-10-25 10:36:50 +0000
commit53d1e21a3afc2fd0a39564e1bd3f3fd7d3ba2501 (patch)
tree84f04f42bb1d1ab46489495ff0d77eb9626702bd
parent6d6a3e814c3a36b180d696eef3a5ae4914c1f306 (diff)
downloadmongo-53d1e21a3afc2fd0a39564e1bd3f3fd7d3ba2501.tar.gz
SERVER-56602 Track usage of match expressions in serverStatus
(cherry picked from commit 98b4a0a2b2c4a230efe1c2ddc3da487a575d670d)
-rw-r--r--jstests/noPassthrough/operator_counters_match.js289
-rw-r--r--src/mongo/db/commands/find_cmd.cpp2
-rw-r--r--src/mongo/db/commands/run_aggregate.cpp2
-rw-r--r--src/mongo/db/matcher/SConscript1
-rw-r--r--src/mongo/db/matcher/expression_leaf.cpp28
-rw-r--r--src/mongo/db/matcher/expression_leaf.h2
-rw-r--r--src/mongo/db/matcher/expression_parser.cpp55
-rw-r--r--src/mongo/db/pipeline/SConscript1
-rw-r--r--src/mongo/db/pipeline/expression_context.cpp21
-rw-r--r--src/mongo/db/pipeline/expression_context.h30
-rw-r--r--src/mongo/db/query/canonical_query.cpp5
-rw-r--r--src/mongo/db/stats/counters.cpp1
-rw-r--r--src/mongo/db/stats/counters.h33
13 files changed, 449 insertions, 21 deletions
diff --git a/jstests/noPassthrough/operator_counters_match.js b/jstests/noPassthrough/operator_counters_match.js
new file mode 100644
index 00000000000..facbb87e6ea
--- /dev/null
+++ b/jstests/noPassthrough/operator_counters_match.js
@@ -0,0 +1,289 @@
+/**
+ * Tests counters for match expressions.
+ * @tags: [requires_fcv_44]
+ */
+
+(function() {
+"use strict";
+const mongod = MongoRunner.runMongod();
+const db = mongod.getDB(jsTest.name());
+const collName = jsTest.name();
+const coll = db[collName];
+coll.drop();
+
+assert.commandWorked(coll.insert({_id: 0, a: 0, b: "foo", c: [10, 50]}));
+assert.commandWorked(coll.insert({_id: 1, a: 1, b: "foo", c: [10, 20, 30]}));
+assert.commandWorked(coll.insert({_id: 2, a: 2, b: "bar"}));
+assert.commandWorked(coll.insert({_id: 3, a: 3, c: [20]}));
+
+/**
+ * Run the command and check that the specified match expression counters are increased by the given
+ * value.
+ * - countersToIncrease: document with elements <match expression> : < increment value>.
+ */
+function checkCountersWithValues(command, countersToIncrease) {
+ let beforeMetrics = {};
+ const metrics = db.serverStatus().metrics.operatorCounters.match;
+ for (let [exprName, inc] of Object.entries(countersToIncrease)) {
+ beforeMetrics[exprName] = metrics[exprName];
+ }
+
+ command();
+
+ const afterMetrics = db.serverStatus().metrics.operatorCounters.match;
+ for (let [exprName, inc] of Object.entries(countersToIncrease)) {
+ assert.eq(afterMetrics[exprName], beforeMetrics[exprName] + inc, exprName);
+ }
+}
+
+/**
+ * Simplified version of the above function where the counters are expected to increase by 1.
+ * - countersToIncrease: array of match expression names or a single expression name.
+ */
+function checkCounters(command, countersToIncrease) {
+ if (!Array.isArray(countersToIncrease)) {
+ countersToIncrease = [countersToIncrease];
+ }
+ checkCountersWithValues(command, countersToIncrease.reduce((acc, val) => {
+ acc[val] = 1;
+ return acc;
+ }, {}));
+}
+
+/**
+ * Run a command expected to fail and check that the specified match expression counters are not
+ * increased.
+ * - countersNotToIncrease: array of match expression names.
+ */
+function checkCountersWithError(command, errorCode, countersNotToIncrease) {
+ let beforeMetrics = {};
+ const metrics = db.serverStatus().metrics.operatorCounters.match;
+ countersNotToIncrease.forEach((exprName) => {
+ beforeMetrics[exprName] = metrics[exprName];
+ });
+
+ const error = assert.throws(command);
+ assert.commandFailedWithCode(error, errorCode);
+
+ const afterMetrics = db.serverStatus().metrics.operatorCounters.match;
+ countersNotToIncrease.forEach((exprName) => {
+ assert.eq(afterMetrics[exprName], beforeMetrics[exprName], exprName);
+ });
+}
+
+// Match expression counters in find.
+checkCounters(() => assert.eq(1, coll.find({a: 1}).itcount()), "$eq");
+
+checkCounters(() => assert.eq(2, coll.find({b: {$eq: "foo"}}).itcount()), "$eq");
+
+checkCounters(() => assert.eq(2, coll.find({a: {$gt: 1}}).itcount()), "$gt");
+
+checkCounters(() => assert.eq(3, coll.find({a: {$gte: 1}}).itcount()), "$gte");
+
+checkCounters(() => assert.eq(1, coll.find({a: {$lt: 1}}).itcount()), "$lt");
+
+checkCounters(() => assert.eq(2, coll.find({a: {$lte: 1}}).itcount()), "$lte");
+
+checkCounters(() => assert.eq(3, coll.find({a: {$ne: 1}}).itcount()), "$ne");
+
+checkCounters(() => assert.eq(2, coll.find({a: {$in: [1, 2]}}).itcount()), "$in");
+
+checkCounters(() => assert.eq(2, coll.find({a: {$nin: [0, 2]}}).itcount()), "$nin");
+
+checkCounters(() => assert.eq(1, coll.find({a: {$gt: 1, $in: [1, 2]}}).itcount()), ["$in", "$gt"]);
+
+checkCounters(() => assert.eq(3, coll.find({c: {$exists: true}}).itcount()), "$exists");
+
+checkCounters(() => assert.eq(4, coll.find({a: {$type: 1}}).itcount()), "$type");
+
+checkCounters(() => assert.eq(2, coll.find({a: {$mod: [2, 0]}}).itcount()), "$mod");
+
+checkCounters(() => assert.eq(2, coll.find({b: {$regex: /^f/}}).itcount()), "$regex");
+
+checkCounters(() => assert.eq(2, coll.find({b: /^f/}).itcount()), "$regex");
+
+checkCounters(() => assert.eq(2, coll.find({b: {$not: /^f/}}).itcount()), ["$regex", "$not"]);
+
+checkCounters(() => assert.eq(2, coll.find({$jsonSchema: {required: ["a", "b", "c"]}}).itcount()),
+ "$jsonSchema");
+
+checkCounters(() => assert.eq(4, coll.find({$alwaysTrue: 1}).itcount()), "$alwaysTrue");
+
+checkCounters(() => assert.eq(0, coll.find({$alwaysFalse: 1}).itcount()), "$alwaysFalse");
+
+// Increments only the $expr counter, counters for $rand, $lt, and $add are recorded in
+// operatorCounters.expressions metrics.
+checkCounters(
+ () => assert.eq(4, coll.find({$expr: {$lt: ["$a", {$add: [{$rand: {}}, 10]}]}}).itcount()),
+ "$expr");
+
+checkCounters(
+ () => assert.eq(2, coll.find({a: {$mod: [2, 0]}, $comment: "Find even values."}).itcount()),
+ ["$mod", "$comment"]);
+
+checkCounters(() => assert.eq(1,
+ coll.find({
+ $where: function() {
+ return (this.a == 1);
+ }
+ })
+ .itcount()),
+ "$where");
+
+// Array query operators.
+checkCounters(() => assert.eq(2, coll.find({c: {$elemMatch: {$gt: 10, $lt: 50}}}).itcount()),
+ ["$elemMatch", "$gt", "$lt"]);
+
+checkCounters(() => assert.eq(1, coll.find({c: {$size: 2}}).itcount()), "$size");
+
+checkCounters(() => assert.eq(1, coll.find({c: {$all: [10, 20]}}).itcount()), "$all");
+
+// Logical operators.
+checkCountersWithValues(() => assert.eq(1, coll.find({$and: [{c: 10}, {c: 20}]}).itcount()),
+ {"$and": 1, "$eq": 2});
+
+checkCounters(() => assert.eq(2, coll.find({$and: [{c: 10}, {a: {$lt: 2}}]}).itcount()),
+ ["$and", "$lt", "$eq"]);
+
+checkCountersWithValues(() => assert.eq(2, coll.find({$or: [{c: 50}, {a: 2}]}).itcount()),
+ {"$or": 1, "$eq": 2});
+
+checkCounters(() => assert.eq(1, coll.find({$nor: [{a: {$gt: 1}}, {c: 50}]}).itcount()),
+ ["$nor", "$eq", "$gt"]);
+
+checkCounters(() => assert.eq(2, coll.find({b: {$not: {$eq: "foo"}}}).itcount()), ["$not", "$eq"]);
+
+// Bitwise query operators.
+checkCounters(() => assert.eq(2, coll.find({a: {$bitsAllClear: [0]}}).itcount()), "$bitsAllClear");
+
+checkCounters(() => assert.eq(1, coll.find({a: {$bitsAllSet: [0, 1]}}).itcount()), "$bitsAllSet");
+
+checkCounters(() => assert.eq(3, coll.find({a: {$bitsAnyClear: [0, 1]}}).itcount()),
+ "$bitsAnyClear");
+
+checkCounters(() => assert.eq(3, coll.find({a: {$bitsAnySet: [0, 1]}}).itcount()), "$bitsAnySet");
+
+// Invalid expressions do not increment any counter.
+checkCountersWithError(() => coll.find({$or: [{c: {$size: 'a'}}, {a: 2}]}).itcount(),
+ ErrorCodes.BadValue,
+ ["$or", "$eq", "$size"]);
+
+// Match expression counters in aggregation pipelines.
+
+let pipeline = [{$match: {_id: 1}}];
+checkCounters(() => assert.eq(1, coll.aggregate(pipeline).itcount()), "$eq");
+
+pipeline = [{$match: {_id: {$gt: 0}}}];
+checkCounters(() => assert.eq(3, coll.aggregate(pipeline).itcount()), "$gt");
+
+pipeline = [{$match: {$and: [{c: 10}, {a: {$lt: 2}}]}}];
+checkCounters(() => assert.eq(2, coll.aggregate(pipeline).itcount()), ["$and", "$eq", "$lt"]);
+
+pipeline = [{$match: {c: {$type: 4}}}];
+checkCounters(() => assert.eq(3, coll.aggregate(pipeline).itcount()), "$type");
+
+pipeline = [{$match: {c: {$exists: false}}}];
+checkCounters(() => assert.eq(1, coll.aggregate(pipeline).itcount()), "$exists");
+
+pipeline = [{$match: {c: {$size: 1}}}];
+checkCounters(() => assert.eq(1, coll.aggregate(pipeline).itcount()), "$size");
+
+pipeline = [{$match: {a: {$bitsAllSet: [0, 1]}}}];
+checkCounters(() => assert.eq(1, coll.aggregate(pipeline).itcount()), "$bitsAllSet");
+
+pipeline = [{$match: {$sampleRate: 0.25}}];
+checkCounters(() => assert.lte(0, coll.aggregate(pipeline).itcount()), "$sampleRate");
+
+// Invalid expressions do not increment any counter.
+checkCountersWithError(
+ () => coll.aggregate([{$match: {$and: [{b: /^fo/}, {a: {$mod: [2, 'z']}}]}}]).itcount(),
+ ErrorCodes.BadValue,
+ ["$and", "$regex", "$mod"]);
+
+// Text search.
+const textCollName = "myTextCollection";
+const textColl = db[textCollName];
+
+textColl.drop();
+assert.commandWorked(textColl.insert({_id: 0, title: "coffee"}));
+assert.commandWorked(textColl.insert({_id: 1, title: "Bake a cake"}));
+assert.commandWorked(textColl.insert({_id: 2, title: "Cake with coffee"}));
+
+assert.commandWorked(textColl.createIndex({title: "text"}));
+
+checkCounters(
+ () => assert.eq(1, textColl.find({$text: {$search: "cake", $caseSensitive: true}}).itcount()),
+ "$text");
+
+// Geospatial expressions.
+const geoCollName = "myGeoCollection";
+const geoColl = db[geoCollName];
+
+geoColl.drop();
+assert.commandWorked(geoColl.insert({_id: 0, location: {type: "Point", coordinates: [10, 20]}}));
+assert.commandWorked(geoColl.createIndex({location: "2dsphere"}));
+
+checkCounters(() => assert.eq(1,
+ geoColl
+ .find({
+ location: {
+ $near: {
+ $geometry: {type: "Point", coordinates: [10, 20]},
+ $minDistance: 0,
+ $maxDistance: 100
+ }
+ }
+ })
+ .itcount()),
+ "$near");
+
+checkCounters(() => assert.eq(1,
+ geoColl
+ .find({
+ location: {
+ $nearSphere: {
+ $geometry: {type: "Point", coordinates: [10.001, 20]},
+ $minDistance: 100,
+ $maxDistance: 500
+ }
+ }
+ })
+ .itcount()),
+ "$nearSphere");
+
+checkCounters(
+ () => assert.eq(
+ 1,
+ geoColl
+ .find({
+ location: {
+ $geoWithin: {
+ $geometry: {
+ type: "Polygon",
+ coordinates: [[[12, 18], [12, 21], [8, 21], [8, 18], [12, 18]]]
+ },
+ }
+ }
+ })
+ .itcount()),
+ "$geoWithin");
+
+checkCounters(
+ () => assert.eq(
+ 1,
+ geoColl
+ .find({
+ location: {
+ $geoIntersects: {
+ $geometry: {
+ type: "Polygon",
+ coordinates: [[[12, 18], [12, 21], [8, 21], [8, 18], [12, 18]]]
+ },
+ }
+ }
+ })
+ .itcount()),
+ "$geoIntersects");
+
+MongoRunner.stopMongod(mongod);
+})();
diff --git a/src/mongo/db/commands/find_cmd.cpp b/src/mongo/db/commands/find_cmd.cpp
index 9f495c9760e..f29635aebf7 100644
--- a/src/mongo/db/commands/find_cmd.cpp
+++ b/src/mongo/db/commands/find_cmd.cpp
@@ -127,6 +127,8 @@ boost::intrusive_ptr<ExpressionContext> makeExpressionContext(
// support it. Use default value for the ExpressionContext's 'sortKeyFormat' member
// variable, which is the newest format.
}
+ expCtx->startExpressionCounters();
+
return expCtx;
}
diff --git a/src/mongo/db/commands/run_aggregate.cpp b/src/mongo/db/commands/run_aggregate.cpp
index 10b0754b9a2..93ef6659d87 100644
--- a/src/mongo/db/commands/run_aggregate.cpp
+++ b/src/mongo/db/commands/run_aggregate.cpp
@@ -685,7 +685,9 @@ Status runAggregate(OperationContext* opCtx,
// Use default value for the ExpressionContext's 'sortKeyFormat' member variable.
}
+ expCtx->startExpressionCounters();
auto pipeline = Pipeline::parse(request.getPipeline(), expCtx);
+ expCtx->stopExpressionCounters();
// Check that the view's collation matches the collation of any views involved in the
// pipeline.
diff --git a/src/mongo/db/matcher/SConscript b/src/mongo/db/matcher/SConscript
index 5cd4d51aa1e..62061a89895 100644
--- a/src/mongo/db/matcher/SConscript
+++ b/src/mongo/db/matcher/SConscript
@@ -68,6 +68,7 @@ env.Library(
'$BUILD_DIR/mongo/db/query/collation/collator_interface',
'$BUILD_DIR/mongo/db/query/query_knobs',
'$BUILD_DIR/mongo/db/pipeline/expression',
+ '$BUILD_DIR/mongo/db/stats/counters',
'$BUILD_DIR/mongo/idl/idl_parser',
'$BUILD_DIR/mongo/util/regex_util',
'$BUILD_DIR/third_party/shim_pcrecpp',
diff --git a/src/mongo/db/matcher/expression_leaf.cpp b/src/mongo/db/matcher/expression_leaf.cpp
index 412343328bc..677342f1d6a 100644
--- a/src/mongo/db/matcher/expression_leaf.cpp
+++ b/src/mongo/db/matcher/expression_leaf.cpp
@@ -759,29 +759,29 @@ bool BitTestMatchExpression::matchesSingleElement(const BSONElement& e,
return performBitTest(eValue);
}
-void BitTestMatchExpression::debugString(StringBuilder& debug, int indentationLevel) const {
- _debugAddSpace(debug, indentationLevel);
-
- debug << path() << " ";
-
+std::string BitTestMatchExpression::name() const {
switch (matchType()) {
case BITS_ALL_SET:
- debug << "$bitsAllSet:";
- break;
+ return "$bitsAllSet";
+
case BITS_ALL_CLEAR:
- debug << "$bitsAllClear:";
- break;
+ return "$bitsAllClear";
+
case BITS_ANY_SET:
- debug << "$bitsAnySet:";
- break;
+ return "$bitsAnySet";
+
case BITS_ANY_CLEAR:
- debug << "$bitsAnyClear:";
- break;
+ return "$bitsAnyClear";
+
default:
MONGO_UNREACHABLE;
}
+}
+
+void BitTestMatchExpression::debugString(StringBuilder& debug, int indentationLevel) const {
+ _debugAddSpace(debug, indentationLevel);
- debug << " [";
+ debug << path() << " " << name() << ": [";
for (size_t i = 0; i < _bitPositions.size(); i++) {
debug << _bitPositions[i];
if (i != _bitPositions.size() - 1) {
diff --git a/src/mongo/db/matcher/expression_leaf.h b/src/mongo/db/matcher/expression_leaf.h
index 64c191ec879..45951a4b5d9 100644
--- a/src/mongo/db/matcher/expression_leaf.h
+++ b/src/mongo/db/matcher/expression_leaf.h
@@ -519,6 +519,8 @@ public:
return _bitPositions;
}
+ std::string name() const;
+
private:
ExpressionOptimizerFunc getOptimizer() const final {
return [](std::unique_ptr<MatchExpression> expression) { return expression; };
diff --git a/src/mongo/db/matcher/expression_parser.cpp b/src/mongo/db/matcher/expression_parser.cpp
index 1b14db6d3ef..0f1d1b6b2ce 100644
--- a/src/mongo/db/matcher/expression_parser.cpp
+++ b/src/mongo/db/matcher/expression_parser.cpp
@@ -67,6 +67,7 @@
#include "mongo/db/matcher/schema/json_schema_parser.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/query/query_knobs_gen.h"
+#include "mongo/db/stats/counters.h"
#include "mongo/util/str.h"
#include "mongo/util/string_map.h"
@@ -136,10 +137,12 @@ std::function<StatusWithMatchExpression(StringData,
DocumentParseLevel)>
retrievePathlessParser(StringData name);
-StatusWithMatchExpression parseRegexElement(StringData name, BSONElement e) {
+StatusWithMatchExpression parseRegexElement(StringData name,
+ BSONElement e,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx) {
if (e.type() != BSONType::RegEx)
return {Status(ErrorCodes::BadValue, "not a regex")};
-
+ expCtx->incrementMatchExprCounter("$regex");
return {std::make_unique<RegexMatchExpression>(name, e.regex(), e.regexFlags())};
}
@@ -275,6 +278,7 @@ StatusWithMatchExpression parse(const BSONObj& obj,
root->add(parsedExpression.getValue().release());
}
+ expCtx->incrementMatchExprCounter(e.fieldNameStringData());
continue;
}
@@ -292,7 +296,7 @@ StatusWithMatchExpression parse(const BSONObj& obj,
}
if (e.type() == BSONType::RegEx) {
- auto result = parseRegexElement(e.fieldNameStringData(), e);
+ auto result = parseRegexElement(e.fieldNameStringData(), e, expCtx);
if (!result.isOK())
return result;
root->add(result.getValue().release());
@@ -308,6 +312,7 @@ StatusWithMatchExpression parse(const BSONObj& obj,
if (!eq.isOK())
return eq;
+ expCtx->incrementMatchExprCounter("$eq");
root->add(eq.getValue().release());
}
@@ -372,7 +377,9 @@ StatusWithMatchExpression parseSampleRate(StringData name,
// DeMorgan's law here you will be suprised that $sampleRate will accept NaN as a valid
// argument.
return {Status(ErrorCodes::BadValue, "numeric argument to $sampleRate must be in [0, 1]")};
- } else if (x == kRandomMinValue) {
+ }
+
+ if (x == kRandomMinValue) {
return std::make_unique<ExprMatchExpression>(
ExpressionConstant::create(expCtx.get(), Value(false)), expCtx);
} else if (x == kRandomMaxValue) {
@@ -1079,6 +1086,7 @@ StatusWithMatchExpression parseInternalSchemaMatchArrayIndex(
StatusWithMatchExpression parseGeo(StringData name,
PathAcceptingKeyword type,
const BSONObj& section,
+ const boost::intrusive_ptr<ExpressionContext>& expCtx,
MatchExpressionParser::AllowedFeatureSet allowedFeatures) {
if (PathAcceptingKeyword::WITHIN == type || PathAcceptingKeyword::GEO_INTERSECTS == type) {
auto gq = std::make_unique<GeoExpression>(name.toString());
@@ -1100,6 +1108,8 @@ StatusWithMatchExpression parseGeo(StringData name,
if (!status.isOK()) {
return status;
}
+
+ expCtx->incrementMatchExprCounter(section.firstElementFieldName());
return {std::make_unique<GeoNearMatchExpression>(name, nq.release(), section)};
}
}
@@ -1331,7 +1341,7 @@ StatusWithMatchExpression parseNot(StringData name,
MatchExpressionParser::AllowedFeatureSet allowedFeatures,
DocumentParseLevel currentLevel) {
if (elem.type() == BSONType::RegEx) {
- auto regex = parseRegexElement(name, elem);
+ auto regex = parseRegexElement(name, elem, expCtx);
if (!regex.isOK()) {
return regex;
}
@@ -1547,7 +1557,7 @@ StatusWithMatchExpression parseSubField(const BSONObj& context,
case PathAcceptingKeyword::WITHIN:
case PathAcceptingKeyword::GEO_INTERSECTS:
- return parseGeo(name, *parseExpMatchType, context, allowedFeatures);
+ return parseGeo(name, *parseExpMatchType, context, expCtx, allowedFeatures);
case PathAcceptingKeyword::GEO_NEAR:
return {Status(ErrorCodes::BadValue,
@@ -1751,7 +1761,8 @@ Status parseSub(StringData name,
if (firstElt.isABSONObj()) {
if (MatchExpressionParser::parsePathAcceptingKeyword(firstElt) ==
PathAcceptingKeyword::GEO_NEAR) {
- auto s = parseGeo(name, PathAcceptingKeyword::GEO_NEAR, sub, allowedFeatures);
+ auto s =
+ parseGeo(name, PathAcceptingKeyword::GEO_NEAR, sub, expCtx, allowedFeatures);
if (s.isOK()) {
root->add(s.getValue().release());
}
@@ -1768,6 +1779,7 @@ Status parseSub(StringData name,
if (!s.isOK())
return s.getStatus();
+ expCtx->incrementMatchExprCounter(deep.fieldNameStringData());
if (s.getValue())
root->add(s.getValue().release());
}
@@ -1918,6 +1930,35 @@ retrievePathlessParser(StringData name) {
}
return func->second;
}
+
+MONGO_INITIALIZER_WITH_PREREQUISITES(MatchExpressionCounters,
+ ("PathlessOperatorMap", "MatchExpressionParser"))
+(InitializerContext* context) {
+ static const std::set<std::string> exceptionsSet{"within", // deprecated
+ "geoNear", // aggregation stage
+ "db", // $-prefixed field names
+ "id",
+ "ref",
+ "options"};
+
+ for (auto&& [name, keyword] : *queryOperatorMap) {
+ if (name[0] == '_' || exceptionsSet.count(name) > 0) {
+ continue;
+ }
+ operatorCountersMatchExpressions.addMatchExprCounter("$" + name);
+ }
+ for (auto&& [name, fn] : *pathlessOperatorMap) {
+ if (name[0] == '_' || exceptionsSet.count(name) > 0) {
+ continue;
+ }
+ operatorCountersMatchExpressions.addMatchExprCounter("$" + name);
+ }
+ operatorCountersMatchExpressions.addMatchExprCounter("$not");
+ operatorCountersMatchExpressions.addMatchExprCounter("$eq");
+ return Status::OK();
+}
+
+
} // namespace
boost::optional<PathAcceptingKeyword> MatchExpressionParser::parsePathAcceptingKeyword(
diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript
index 98dc11fe92e..da74b908548 100644
--- a/src/mongo/db/pipeline/SConscript
+++ b/src/mongo/db/pipeline/SConscript
@@ -66,6 +66,7 @@ env.Library(
'$BUILD_DIR/mongo/db/query/collation/collator_factory_interface',
'$BUILD_DIR/mongo/db/query/query_knobs',
'$BUILD_DIR/mongo/db/service_context',
+ '$BUILD_DIR/mongo/db/stats/counters',
'$BUILD_DIR/mongo/scripting/scripting',
'$BUILD_DIR/mongo/util/intrusive_counter',
]
diff --git a/src/mongo/db/pipeline/expression_context.cpp b/src/mongo/db/pipeline/expression_context.cpp
index 12f4d1b1e9a..5bfc56283f7 100644
--- a/src/mongo/db/pipeline/expression_context.cpp
+++ b/src/mongo/db/pipeline/expression_context.cpp
@@ -35,6 +35,7 @@
#include "mongo/db/pipeline/process_interface/stub_mongo_process_interface.h"
#include "mongo/db/query/collation/collation_spec.h"
#include "mongo/db/query/collation/collator_factory_interface.h"
+#include "mongo/db/stats/counters.h"
#include "mongo/util/intrusive_counter.h"
namespace mongo {
@@ -213,4 +214,24 @@ intrusive_ptr<ExpressionContext> ExpressionContext::copyWith(
return expCtx;
}
+void ExpressionContext::startExpressionCounters() {
+ if (!_expressionCounters) {
+ _expressionCounters = boost::make_optional<ExpressionCounters>({});
+ }
+}
+
+void ExpressionContext::incrementMatchExprCounter(StringData name) {
+ if (_expressionCounters) {
+ ++_expressionCounters.get().matchExprCountersMap[name];
+ }
+}
+
+void ExpressionContext::stopExpressionCounters() {
+ if (_expressionCounters) {
+ operatorCountersMatchExpressions.mergeCounters(
+ _expressionCounters.get().matchExprCountersMap);
+ }
+ _expressionCounters = boost::none;
+}
+
} // namespace mongo
diff --git a/src/mongo/db/pipeline/expression_context.h b/src/mongo/db/pipeline/expression_context.h
index 45b228d0d25..341bc4317cb 100644
--- a/src/mongo/db/pipeline/expression_context.h
+++ b/src/mongo/db/pipeline/expression_context.h
@@ -57,6 +57,17 @@
namespace mongo {
+class AggregateCommandRequest;
+
+/**
+ * The structure ExpressionCounters encapsulates counters for match, aggregate, and other
+ * expression types as seen in the end-user queries.
+ */
+struct ExpressionCounters {
+ StringMap<uint64_t> aggExprCountersMap;
+ StringMap<uint64_t> matchExprCountersMap;
+};
+
class ExpressionContext : public RefCountable {
public:
static constexpr size_t kMaxSubPipelineViewDepth = 20;
@@ -294,6 +305,22 @@ public:
opCtx, scope.get_value_or(BSONObj()), ns.db(), loadStoredProcedures, jsHeapLimitMB);
}
+ /**
+ * Create optional internal expression counters and start counting.
+ */
+ void startExpressionCounters();
+
+ /**
+ * Increment the counter for the match expression with a given name.
+ */
+ void incrementMatchExprCounter(StringData name);
+
+ /**
+ * Merge expression counters from the current expression context into the global maps
+ * and stop counting.
+ */
+ void stopExpressionCounters();
+
// The explain verbosity requested by the user, or boost::none if no explain was requested.
boost::optional<ExplainOptions::Verbosity> explain;
@@ -380,6 +407,9 @@ protected:
StringMap<ResolvedNamespace> _resolvedNamespaces;
int _interruptCounter = kInterruptCheckPeriod;
+
+private:
+ boost::optional<ExpressionCounters> _expressionCounters = boost::none;
};
} // namespace mongo
diff --git a/src/mongo/db/query/canonical_query.cpp b/src/mongo/db/query/canonical_query.cpp
index b23dc90013c..c49c382b01f 100644
--- a/src/mongo/db/query/canonical_query.cpp
+++ b/src/mongo/db/query/canonical_query.cpp
@@ -164,6 +164,11 @@ StatusWith<std::unique_ptr<CanonicalQuery>> CanonicalQuery::canonicalize(
if (!statusWithMatcher.isOK()) {
return statusWithMatcher.getStatus();
}
+
+ // Stop counting match expressions after they have been parsed to exclude expressions created
+ // during optimization and other processing steps.
+ newExpCtx->stopExpressionCounters();
+
std::unique_ptr<MatchExpression> me = std::move(statusWithMatcher.getValue());
// Make the CQ we'll hopefully return.
diff --git a/src/mongo/db/stats/counters.cpp b/src/mongo/db/stats/counters.cpp
index d8168112ba9..852440b366f 100644
--- a/src/mongo/db/stats/counters.cpp
+++ b/src/mongo/db/stats/counters.cpp
@@ -295,4 +295,5 @@ OpCounters replOpCounters;
NetworkCounter networkCounter;
AuthCounter authCounter;
AggStageCounters aggStageCounters;
+OperatorCountersMatchExpressions operatorCountersMatchExpressions;
} // namespace mongo
diff --git a/src/mongo/db/stats/counters.h b/src/mongo/db/stats/counters.h
index 0330b43fd00..154131ffc30 100644
--- a/src/mongo/db/stats/counters.h
+++ b/src/mongo/db/stats/counters.h
@@ -251,4 +251,37 @@ public:
};
extern AggStageCounters aggStageCounters;
+
+/**
+ * Global counters for match expressions.
+ */
+class OperatorCountersMatchExpressions {
+private:
+ struct MatchExprCounter {
+ MatchExprCounter(StringData name) : metric("operatorCounters.match." + name, &counter) {}
+
+ Counter64 counter;
+ ServerStatusMetricField<Counter64> metric;
+ };
+
+public:
+ void addMatchExprCounter(StringData name) {
+ operatorCountersMatchExprMap[name] = std::make_unique<MatchExprCounter>(name);
+ }
+
+ void mergeCounters(StringMap<uint64_t>& toMerge) {
+ for (auto&& [name, cnt] : toMerge) {
+ if (auto it = operatorCountersMatchExprMap.find(name);
+ it != operatorCountersMatchExprMap.end()) {
+ it->second->counter.increment(cnt);
+ }
+ }
+ }
+
+private:
+ // Map of match expressions to the number of occurrences in queries.
+ StringMap<std::unique_ptr<MatchExprCounter>> operatorCountersMatchExprMap = {};
+};
+
+extern OperatorCountersMatchExpressions operatorCountersMatchExpressions;
} // namespace mongo