summaryrefslogtreecommitdiff
path: root/src/mongo/db/pipeline/document_source_query_stats.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/db/pipeline/document_source_query_stats.cpp')
-rw-r--r--src/mongo/db/pipeline/document_source_query_stats.cpp301
1 files changed, 301 insertions, 0 deletions
diff --git a/src/mongo/db/pipeline/document_source_query_stats.cpp b/src/mongo/db/pipeline/document_source_query_stats.cpp
new file mode 100644
index 00000000000..69170760ace
--- /dev/null
+++ b/src/mongo/db/pipeline/document_source_query_stats.cpp
@@ -0,0 +1,301 @@
+/**
+ * Copyright (C) 2022-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/pipeline/document_source_query_stats.h"
+
+#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQueryStats
+
+#include "mongo/idl/idl_parser.h"
+#include "mongo/logv2/log.h"
+#include "mongo/util/assert_util.h"
+#include "mongo/util/debug_util.h"
+
+namespace mongo {
+namespace {
+Counter64 queryStatsHmacApplicationErrors;
+ServerStatusMetricField<Counter64> displayWriteErrorsMetric("queryStats.numHmacApplicationErrors",
+ &queryStatsHmacApplicationErrors);
+} // namespace
+
+REGISTER_DOCUMENT_SOURCE(queryStats,
+ DocumentSourceQueryStats::LiteParsed::parse,
+ DocumentSourceQueryStats::createFromBson,
+ AllowedWithApiStrict::kNeverInVersion1);
+
+namespace {
+
+/**
+ * Parse the spec object calling the `ctor` with the TransformAlgorithm enum algorithm and
+ * std::string hmacKey arguments.
+ */
+template <typename Ctor>
+auto parseSpec(const BSONElement& spec, const Ctor& ctor) {
+ uassert(ErrorCodes::FailedToParse,
+ str::stream() << DocumentSourceQueryStats::kStageName
+ << " value must be an object. Found: " << typeName(spec.type()),
+ spec.type() == BSONType::Object);
+ BSONObj obj = spec.embeddedObject();
+ TransformAlgorithmEnum algorithm = TransformAlgorithmEnum::kNone;
+ std::string hmacKey;
+ auto parsed = DocumentSourceQueryStatsSpec::parse(DocumentSourceQueryStats::kStageName, obj);
+ boost::optional<TransformIdentifiersSpec> transformIdentifiers =
+ parsed.getTransformIdentifiers();
+
+ if (transformIdentifiers) {
+ algorithm = transformIdentifiers->getAlgorithm();
+ boost::optional<ConstDataRange> hmacKeyContainer = transformIdentifiers->getHmacKey();
+ 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);
+}
+
+} // namespace
+
+BSONObj DocumentSourceQueryStats::computeQueryStatsKey(std::shared_ptr<const Key> key) const {
+ static const auto 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();
+ };
+
+ auto opts = SerializationOptions{};
+ opts.literalPolicy = LiteralSerializationPolicy::kToDebugTypeString;
+ if (_algorithm == TransformAlgorithmEnum::kHmacSha256) {
+ opts.transformIdentifiers = true;
+ opts.transformIdentifiersCallback = [&](StringData sd) {
+ return sha256HmacStringDataHasher(_hmacKey, sd);
+ };
+ }
+ return key->toBson(pExpCtx->opCtx, opts);
+}
+
+std::unique_ptr<DocumentSourceQueryStats::LiteParsed> DocumentSourceQueryStats::LiteParsed::parse(
+ const NamespaceString& nss, const BSONElement& spec) {
+ return parseSpec(spec, [&](TransformAlgorithmEnum algorithm, std::string hmacKey) {
+ return std::make_unique<DocumentSourceQueryStats::LiteParsed>(
+ spec.fieldName(), algorithm, hmacKey);
+ });
+}
+
+boost::intrusive_ptr<DocumentSource> DocumentSourceQueryStats::createFromBson(
+ BSONElement spec, const boost::intrusive_ptr<ExpressionContext>& pExpCtx) {
+ const NamespaceString& nss = pExpCtx->ns;
+
+ uassert(ErrorCodes::InvalidNamespace,
+ "$queryStats must be run against the 'admin' database with {aggregate: 1}",
+ nss.db() == NamespaceString::kAdminDb && 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);
+ });
+}
+
+Value DocumentSourceQueryStats::serialize(const SerializationOptions& opts) const {
+ auto hmacKey = opts.serializeLiteral(
+ BSONBinData(_hmacKey.c_str(), _hmacKey.size(), BinDataType::Sensitive));
+ if (opts.literalPolicy == LiteralSerializationPolicy::kToRepresentativeParseableValue) {
+ // The default shape for a BinData under this policy is empty and has sub-type 0 (general).
+ // This doesn't quite work for us since we assert when we parse that it is at least 32 bytes
+ // and also is sub-type 8 (sensitive).
+ hmacKey =
+ Value(BSONBinData("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 32, BinDataType::Sensitive));
+ }
+ return Value{
+ Document{{kStageName,
+ _transformIdentifiers
+ ? Document{{"transformIdentifiers",
+ Document{{"algorithm", TransformAlgorithm_serializer(_algorithm)},
+ {"hmacKey", hmacKey}}}}
+ : Document{}}}};
+}
+
+DocumentSource::GetNextResult DocumentSourceQueryStats::doGetNext() {
+ const auto shouldLog = _algorithm != TransformAlgorithmEnum::kNone;
+ /**
+ * When a CopiedPartition is present (loaded) and contains more elements (QueryStatsEntry), we
+ * can process and return the next element in the _currentCopiedPartition.
+ *
+ * When the current CopiedPartition is exhausted (emptied), we move on to the next
+ * partition. Once we have iterated to the end of the valid partitions, we are done iteratiing
+ * over all the queryStatsStore entries.
+ *
+ * We iterate over a copied container (CopiedParitition) containing the entries in
+ * the partition to reduce the time under which the partition lock is held.
+ */
+ auto& queryStatsStore = getQueryStatsStore(getContext()->opCtx);
+
+ while (_currentCopiedPartition.isValidPartitionId(queryStatsStore.numPartitions())) {
+ if (!_currentCopiedPartition.isLoaded()) {
+ _currentCopiedPartition.load(queryStatsStore);
+ }
+ // CopiedPartition::load() will throw if any errors occur.
+ // Safe to assume _currentCopiedPartition is now loaded.
+
+ // Exhaust all elements in the current copied partition.
+ // Use a while loop here to handle cases where toDocument() may fail for a specific
+ // QueryStatsEntry, in which case we suppress the thrown exception and continue
+ // iterating to the next available entry.
+ while (!_currentCopiedPartition.empty()) {
+ auto& statsEntries = _currentCopiedPartition.statsEntries;
+ const auto& queryStatsEntry = statsEntries.front();
+ ON_BLOCK_EXIT([&statsEntries]() { statsEntries.pop_front(); });
+ if (auto doc =
+ toDocument(_currentCopiedPartition.getReadTimestamp(), queryStatsEntry)) {
+ if (shouldLog) {
+ LOGV2_DEBUG_OPTIONS(7808301,
+ 3,
+ {logv2::LogTruncation::Disabled},
+ "Logging all outputs of $queryStats",
+ "thisOutput"_attr = *doc);
+ }
+ return std::move(*doc);
+ }
+ }
+ // Once we have exhausted entries in this partition, move on to the next partition.
+ _currentCopiedPartition.incrementPartitionId();
+ }
+
+ if (shouldLog) {
+ LOGV2_DEBUG_OPTIONS(
+ 7808302, 3, {logv2::LogTruncation::Disabled}, "Finished logging output of $queryStats");
+ }
+ return DocumentSource::GetNextResult::makeEOF();
+}
+
+boost::optional<Document> DocumentSourceQueryStats::toDocument(
+ const Date_t& partitionReadTime, const QueryStatsEntry& queryStatsEntry) const {
+ const auto& key = queryStatsEntry.key;
+ try {
+ auto queryStatsKey = computeQueryStatsKey(key);
+ // We use the representative shape to generate the key hash. This avoids returning duplicate
+ // hashes if we have bugs that cause two different representative shapes to re-parse into
+ // the same debug shape.
+ auto representativeShapeKey = key->toBson(
+ pExpCtx->opCtx, SerializationOptions::kRepresentativeQueryShapeSerializeOptions);
+
+ // This SHA256 version of the hash is output to aid in data analytics use cases. In these
+ // cases, we often care about comparing hashes from different hosts, potentially on
+ // different versions and platforms. The thinking here is that the SHA256 algorithm is more
+ // stable across these different environments than the quicker 'absl::HashOf'
+ // implementation.
+ auto hash = SHA256Block::computeHash((const uint8_t*)representativeShapeKey.objdata(),
+ representativeShapeKey.objsize())
+ .toString();
+ return Document{{"key", std::move(queryStatsKey)},
+ {"keyHash", hash},
+ {"metrics", queryStatsEntry.toBSON()},
+ {"asOf", partitionReadTime}};
+ } catch (const DBException& ex) {
+ queryStatsHmacApplicationErrors.increment();
+ const auto& hash = absl::Hash<query_stats::Key>{}(*key);
+ const auto queryShape = key->universalComponents()._queryShape->toBson(
+ pExpCtx->opCtx, SerializationOptions::kRepresentativeQueryShapeSerializeOptions);
+ LOGV2_DEBUG(7349403,
+ 2,
+ "Error encountered when applying hmac to query shape, will not publish "
+ "queryStats for this entry.",
+ "status"_attr = ex.toStatus(),
+ "hash"_attr = hash,
+ "debugQueryShape"_attr = queryShape);
+
+ if (kDebugBuild || internalQueryStatsErrorsAreCommandFatal.load()) {
+ auto keyString = std::to_string(hash);
+ tasserted(7349401,
+ str::stream() << "Was not able to re-parse queryStats key when "
+ "reading queryStats.Status "
+ << ex.toString() << " Hash: " << keyString
+ << " Query Shape: " << queryShape.toString());
+ }
+ }
+ return {};
+}
+
+/**
+ * Loads the current CopiedPartition with copies of the QueryStatsEntries located in partition of
+ * cache corresponding to the partitionId of the current CopiedPartition. This ensures that the
+ * partition mutex is only held for the duration of copying.
+ */
+void DocumentSourceQueryStats::CopiedPartition::load(QueryStatsStore& queryStatsStore) {
+ tassert(7932100,
+ "Attempted to load invalid partition.",
+ _partitionId < queryStatsStore.numPartitions());
+ tassert(7932101, "Partition was already loaded.", !isLoaded());
+ // 'statsEntries' should be empty, clear just in case.
+ statsEntries.clear();
+
+ // Capture the time at which reading the partition begins.
+ _readTimestamp = Date_t::now();
+ {
+ // We only keep the partition (which holds a lock)
+ // for the time needed to collect the metrics (QueryStatsEntry)
+ const auto partition = queryStatsStore.getPartition(_partitionId);
+
+ // Note the intentional copy of QueryStatsEntry.
+ // This will give us a snapshot of all the metrics we want to report.
+ for (auto&& [hash, metrics] : *partition) {
+ statsEntries.push_back(metrics);
+ }
+ }
+ _isLoaded = true;
+}
+
+bool DocumentSourceQueryStats::CopiedPartition::isLoaded() const {
+ return _isLoaded;
+}
+
+void DocumentSourceQueryStats::CopiedPartition::incrementPartitionId() {
+ // Ensure loaded state is reset when partitionId is incremented.
+ ++_partitionId;
+ _isLoaded = false;
+}
+
+bool DocumentSourceQueryStats::CopiedPartition::isValidPartitionId(
+ QueryStatsStore::PartitionId maxNumPartitions) const {
+ return _partitionId < maxNumPartitions;
+}
+
+const Date_t& DocumentSourceQueryStats::CopiedPartition::getReadTimestamp() const {
+ return _readTimestamp;
+}
+
+bool DocumentSourceQueryStats::CopiedPartition::empty() const {
+ return statsEntries.empty();
+}
+} // namespace mongo