SERVER-123238 Add unit tests for serverStatus command with otel metrics (#51209)

GitOrigin-RevId: c2ee07f640f77a92b33a3cb41e9d3cddd68ea56d
This commit is contained in:
Cheahuychou Mao 2026-04-08 11:17:05 -04:00 committed by MongoDB Bot
parent 811c56cf0d
commit 2b0757a642
8 changed files with 500 additions and 50 deletions

View File

@ -1139,10 +1139,12 @@ mongo_cc_unit_test(
"//src/mongo/db:fle_mocks",
"//src/mongo/db:multitenancy",
"//src/mongo/db:query_exec",
"//src/mongo/db:read_write_concern_defaults_mock",
"//src/mongo/db:service_context_d_test_fixture",
"//src/mongo/db/auth:authmocks",
"//src/mongo/db/collection_crud",
"//src/mongo/db/commands/server_status:server_status_core",
"//src/mongo/db/commands/server_status:server_status_metric",
"//src/mongo/db/index_builds:index_builds_coordinator",
"//src/mongo/db/memory_tracking",
"//src/mongo/db/op_observer",

View File

@ -331,8 +331,60 @@ void MetricTree::_add(StringData path, std::unique_ptr<ServerStatusMetric> metri
}
}
void MetricTree::clearForTests() {
_children.clear();
void MetricTree::removeForTests(StringData path) {
if (path.empty()) {
return;
}
if (path.starts_with('.')) {
path.remove_prefix(1);
if (!path.empty()) {
_removeForTests(path);
}
} else {
_removeForTests(fmt::format("metrics.{}", path));
}
}
void MetricTree::_removeForTests(StringData path) {
// Walk the path, recording (parent, key) pairs so we can prune empty subtrees afterward.
struct Node {
MetricTree* parent;
std::string key;
};
std::vector<Node> stack;
StringData tail = path;
MetricTree* subTree = this;
while (true) {
auto dot = tail.find('.');
if (dot == std::string::npos) {
subTree->_children.erase(std::string{tail});
break;
}
StringData part = tail.substr(0, dot);
tail = tail.substr(dot + 1);
auto iter = subTree->_children.find(part);
if (iter == subTree->_children.end() || !iter->second.isSubtree()) {
return;
}
stack.push_back({subTree, std::string{part}});
subTree = iter->second.getSubtree().get();
}
// Prune empty parent subtrees bottom-up.
for (auto it = stack.rbegin(); it != stack.rend(); ++it) {
auto childIter = it->parent->_children.find(it->key);
if (childIter == it->parent->_children.end() || !childIter->second.isSubtree()) {
break;
}
if (!childIter->second.getSubtree()->_children.empty()) {
break;
}
it->parent->_children.erase(childIter);
}
}
void appendMergedTrees(std::vector<const MetricTree*> trees,
@ -360,11 +412,5 @@ MetricTreeSet& globalMetricTreeSet() {
return *obj;
}
void clearGlobalMetricTreeSetForTests() {
for (const auto role :
{ClusterRole::None, ClusterRole::ShardServer, ClusterRole::RouterServer}) {
globalMetricTreeSet()[role].clearForTests();
}
}
} // namespace mongo

View File

@ -194,11 +194,26 @@ public:
return _children;
}
void clearForTests();
/**
* Removes the metric at `path` from the tree, then prunes any intermediate subtrees that
* become empty as a result. The path follows the same leading-dot convention as `add`: a
* leading '.' means the path is absolute (relative to the root of the tree), while a path
* without a leading '.' is implicitly rooted under "metrics.". Does nothing if `path` is
* empty or does not exist in the tree. Intended for use in tests only.
*/
void removeForTests(StringData path);
private:
void _add(StringData path, std::unique_ptr<ServerStatusMetric> metric);
/**
* The helper for `removeForTests`. Removes the node at `path` (a dot-separated absolute path
* with no leading dot) and bottom-up prunes any intermediate subtrees that become empty after
* after the removal. Silently returns without modifying the tree when any component of
* `path` is missing or when an intermediate component is a leaf metric rather than a subtree.
*/
void _removeForTests(StringData path);
ChildMap _children;
};
@ -218,14 +233,6 @@ private:
MetricTreeSet& globalMetricTreeSet();
/**
* Used in unit tests only. Removes all metrics from globalMetricTreeSet() for every ClusterRole.
*
* MetricsService may register OtelMetricServerStatusAdapter entries that hold raw Metric* pointers.
* After a test destroys its MetricsService, those pointers are no longer be valid so they must be
* removed from the tree set before a subsequent test runs.
*/
void clearGlobalMetricTreeSetForTests();
/**
* Write a merger of the `trees` to `b`, under field `name`. `excludePaths` is a

View File

@ -29,29 +29,252 @@
#include "mongo/bson/bsonobj.h"
#include "mongo/db/commands/db_command_test_fixture.h"
#include "mongo/db/commands/server_status/server_status_metric.h"
#include "mongo/db/database_name.h"
#include "mongo/db/read_write_concern_defaults_cache_lookup_mock.h"
#include "mongo/db/service_context_test_fixture.h"
#include "mongo/db/service_entry_point_shard_role.h"
#include "mongo/db/topology/cluster_role.h"
#include "mongo/otel/metrics/metric_names.h"
#include "mongo/otel/metrics/metrics_service.h"
#include "mongo/otel/metrics/metrics_test_util.h"
#include "mongo/s/service_entry_point_router_role.h"
#include "mongo/unittest/unittest.h"
#include <cstdint>
#include <fmt/format.h>
namespace mongo {
namespace {
class ServerStatusServersTest : public DBCommandTestFixture {};
TEST_F(ServerStatusServersTest, IncludesOtelMetrics) {
otel::metrics::OtelMetricsCapturer capturer;
class ServerStatusServersTest : public DBCommandTestFixture {
public:
void setUp() override {
DBCommandTestFixture::setUp();
}
void tearDown() override {
otel::metrics::MetricsService::instance().clearForTests();
DBCommandTestFixture::tearDown();
}
};
TEST_F(ServerStatusServersTest, IncludeUnderMetricsSection) {
auto& metricsService = otel::metrics::MetricsService::instance();
otel::metrics::CounterOptions options{
.serverStatusOptions = otel::metrics::ServerStatusOptions{.dottedPath = "test.metric1",
.role = ClusterRole::None}};
auto& counter = metricsService.createInt64Counter(otel::metrics::MetricNames::kTest1,
"description",
otel::metrics::MetricUnit::kSeconds,
{.inServerStatus = true});
options);
counter.add(11);
BSONObj result = runCommand(BSON("serverStatus" << 1 << "otelMetrics" << 1));
ASSERT_TRUE(result.hasField("otelMetrics"));
BSONObj resultObj = runCommand(BSON("serverStatus" << 1 << "metrics" << 1));
ASSERT_TRUE(resultObj.hasField("metrics"));
BSONObj metricsObj = resultObj.getObjectField("metrics");
ASSERT_EQ(metricsObj["test"]["metric1"].Long(), 11);
}
BSONObj otelMetrics = result.getObjectField("otelMetrics");
ASSERT_TRUE(otelMetrics.hasField("test_only.metric1_seconds"));
EXPECT_EQ(otelMetrics.getIntField("test_only.metric1_seconds"), 11);
TEST_F(ServerStatusServersTest, IncludeUnderOtelMetricsSection) {
auto& metricsService = otel::metrics::MetricsService::instance();
otel::metrics::CounterOptions options{
.inServerStatus = true,
};
auto& counter = metricsService.createInt64Counter(otel::metrics::MetricNames::kTest1,
"description",
otel::metrics::MetricUnit::kSeconds,
options);
counter.add(11);
BSONObj resultObj = runCommand(BSON("serverStatus" << 1 << "otelMetrics" << 1));
ASSERT_TRUE(resultObj.hasField("otelMetrics"));
BSONObj otelMetricsObj = resultObj.getObjectField("otelMetrics");
ASSERT_EQ(otelMetricsObj["test_only.metric1_seconds"].Long(), 11);
}
TEST_F(ServerStatusServersTest, IncludeUnderOtelMetricsSectionAndMetricsSection) {
auto& metricsService = otel::metrics::MetricsService::instance();
otel::metrics::CounterOptions flatOptions{
.inServerStatus = true,
};
auto& flatCounter = metricsService.createInt64Counter(otel::metrics::MetricNames::kTest1,
"description",
otel::metrics::MetricUnit::kSeconds,
flatOptions);
flatCounter.add(11);
otel::metrics::CounterOptions nestedOptions{
.serverStatusOptions = otel::metrics::ServerStatusOptions{.dottedPath = "test.metric2",
.role = ClusterRole::None},
};
auto& nestedCounter = metricsService.createInt64Counter(otel::metrics::MetricNames::kTest2,
"description",
otel::metrics::MetricUnit::kSeconds,
nestedOptions);
nestedCounter.add(22);
BSONObj resultObj =
runCommand(BSON("serverStatus" << 1 << "otelMetrics" << 1 << "metrics" << 1));
ASSERT_TRUE(resultObj.hasField("otelMetrics"));
ASSERT_TRUE(resultObj.hasField("metrics"));
BSONObj otelMetricsObj = resultObj.getObjectField("otelMetrics");
ASSERT_EQ(otelMetricsObj["test_only.metric1_seconds"].Long(), 11);
BSONObj metricsObj = resultObj.getObjectField("metrics");
ASSERT_EQ(metricsObj["test"]["metric2"].Long(), 22);
}
TEST_F(ServerStatusServersTest, ExcludeWhenServerStatusOptionsAndInServerStatusNotset) {
auto& metricsService = otel::metrics::MetricsService::instance();
otel::metrics::CounterOptions options{};
ASSERT_FALSE(options.serverStatusOptions.has_value());
ASSERT_FALSE(options.inServerStatus);
auto& counter = metricsService.createInt64Counter(otel::metrics::MetricNames::kTest1,
"description",
otel::metrics::MetricUnit::kSeconds,
options);
counter.add(11);
BSONObj resultObj =
runCommand(BSON("serverStatus" << 1 << "otelMetrics" << 1 << "metrics" << 1));
ASSERT_FALSE(resultObj.hasField("otelMetrics"));
// The "metrics" section may still exist because of non-otel serverStatus metrics.
if (resultObj.hasField("metrics")) {
BSONObj metricsObj = resultObj.getObjectField("metrics");
ASSERT_FALSE(metricsObj.hasField("test_only")) << metricsObj.toString();
}
}
class ServerStatusServersRoleTestFixture : public ServiceContextTest {
public:
void setUp() override {
ServiceContextTest::setUp();
ReadWriteConcernDefaults::create(getService(), _lookupMock.getFetchDefaultsFn());
}
void tearDown() override {
otel::metrics::MetricsService::instance().clearForTests();
ServiceContextTest::tearDown();
}
protected:
otel::metrics::Counter<int64_t>& createCounter(otel::metrics::MetricsService& metricsService,
otel::metrics::MetricName metricName,
std::string dottedPath,
ClusterRole role) {
return metricsService.createInt64Counter(metricName,
"description",
otel::metrics::MetricUnit::kSeconds,
otel::metrics::CounterOptions{
.serverStatusOptions =
otel::metrics::ServerStatusOptions{
.dottedPath = std::move(dottedPath),
.role = role,
},
});
}
BSONObj getMetricsSection(StringData pathPrefix) {
Service* const service = getServiceContext()->getService();
ServiceContext::UniqueClient client =
service->makeClient("ServerStatusServersRoleTestFixture");
AlternativeClientRegion acr(client);
auto opCtx = cc().makeOperationContext();
DBDirectClient dbclient(opCtx.get());
BSONObj resultObj;
// Specify none: 1 to exclude all other sections.
dbclient.runCommand(DatabaseName::kAdmin,
BSON("serverStatus" << 1 << "none" << 1 << "metrics" << 1),
resultObj);
ASSERT_OK(getStatusFromWriteCommandReply(resultObj));
ASSERT_TRUE(resultObj.hasField("metrics"));
BSONObj metricsObj = resultObj.getObjectField("metrics");
return metricsObj.getObjectField(pathPrefix).getOwned();
}
private:
// Allows for commands to not specify a default read/write concern.
ReadWriteConcernDefaultsLookupMock _lookupMock;
};
class ServerStatusServersRoleShardTest : public virtual service_context_test::ShardRoleOverride,
public ServerStatusServersRoleTestFixture {
void setUp() override {
ServerStatusServersRoleTestFixture::setUp();
// Initialize the serviceEntryPoint so that DBDirectClient can function.
getService()->setServiceEntryPoint(std::make_unique<ServiceEntryPointShardRole>());
const auto service = getServiceContext();
auto replCoord =
std::make_unique<repl::ReplicationCoordinatorMock>(service, repl::ReplSettings{});
ASSERT_OK(replCoord->setFollowerMode(repl::MemberState::RS_PRIMARY));
repl::ReplicationCoordinator::set(service, std::move(replCoord));
}
};
TEST_F(ServerStatusServersRoleShardTest, MergesNoneAndShardMetricTreesExcludesRouter) {
auto& metricsService = otel::metrics::MetricsService::instance();
createCounter(metricsService,
otel::metrics::MetricNames::kTestShardMergeNone,
"test.noneMetric",
ClusterRole::None)
.add(11);
createCounter(metricsService,
otel::metrics::MetricNames::kTestShardMergeShard,
"test.shardMetric",
ClusterRole::ShardServer)
.add(22);
createCounter(metricsService,
otel::metrics::MetricNames::kTestShardMergeRouter,
"test.routerMetric",
ClusterRole::RouterServer)
.add(33);
BSONObj section = getMetricsSection("test");
EXPECT_EQ(section.getIntField("noneMetric"), 11);
EXPECT_EQ(section.getIntField("shardMetric"), 22);
ASSERT_FALSE(section.hasField("routerMetric")) << section.toString();
}
class ServerStatusServersRoleRouterTest : public virtual service_context_test::RouterRoleOverride,
public ServerStatusServersRoleTestFixture {
void setUp() override {
ServerStatusServersRoleTestFixture::setUp();
// Initialize the serviceEntryPoint so that DBDirectClient can function.
getService()->setServiceEntryPoint(std::make_unique<ServiceEntryPointRouterRole>());
}
};
TEST_F(ServerStatusServersRoleRouterTest, MergesNoneAndRouterMetricTreesExcludesShard) {
auto& metricsService = otel::metrics::MetricsService::instance();
createCounter(metricsService,
otel::metrics::MetricNames::kTestRouterMergeNone,
"test.noneMetric",
ClusterRole::None)
.add(11);
createCounter(metricsService,
otel::metrics::MetricNames::kTestRouterMergeShard,
"test.shardMetric",
ClusterRole::ShardServer)
.add(22);
createCounter(metricsService,
otel::metrics::MetricNames::kTestRouterMergeRouter,
"test.routerMetric",
ClusterRole::RouterServer)
.add(33);
BSONObj section = getMetricsSection("test");
EXPECT_EQ(section.getIntField("noneMetric"), 11);
ASSERT_FALSE(section.hasField("shardMetric")) << section.toString();
EXPECT_EQ(section.getIntField("routerMetric"), 33);
}
} // namespace

View File

@ -156,6 +156,12 @@ public:
static constexpr MetricName kTest4 = {"test_only.metric4"};
static constexpr MetricName kTest5 = {"test_only.metric5"};
static constexpr MetricName kTest6 = {"test_only.metric6"};
static constexpr MetricName kTestShardMergeNone = {"test_only.shard_merge_none"};
static constexpr MetricName kTestShardMergeShard = {"test_only.shard_merge_shard"};
static constexpr MetricName kTestShardMergeRouter = {"test_only.shard_merge_router"};
static constexpr MetricName kTestRouterMergeNone = {"test_only.router_merge_none"};
static constexpr MetricName kTestRouterMergeShard = {"test_only.router_merge_shard"};
static constexpr MetricName kTestRouterMergeRouter = {"test_only.router_merge_router"};
// camelCase is not allowed.
static constexpr MetricName kTestInvalid = {"test_only.Metric"};
};

View File

@ -589,4 +589,17 @@ void MetricsService::appendMetricsForServerStatus(BSONObjBuilder& bsonBuilder) c
identifierAndMetric.metric);
}
}
void MetricsService::clearForTests() {
stdx::lock_guard lock(_mutex);
#ifdef MONGO_CONFIG_OTEL
_observableInstruments.clear();
#endif
for (auto& [name, identAndMetric] : _metrics) {
auto& opts = identAndMetric.identifier.serverStatusOptions;
if (opts.has_value()) {
globalMetricTreeSet()[opts->role].removeForTests(opts->dottedPath);
}
}
_metrics.clear();
}
} // namespace mongo::otel::metrics

View File

@ -214,6 +214,12 @@ public:
*/
void appendMetricsForServerStatus(BSONObjBuilder& bsonBuilder) const;
/**
* Used in unit tests only. Removes all metrics registered by this MetricsService from the
* internal map and from the serverStatus metric trees.
*/
void clearForTests();
#ifdef MONGO_CONFIG_OTEL
/**
* Initializes the metrics service by registering metrics created before initialization with the

View File

@ -53,10 +53,13 @@ namespace {
class MetricsServiceTest : public testing::Test {
public:
void SetUp() override {
clearGlobalMetricTreeSetForTests();
metricsService = std::make_unique<MetricsService>();
}
void TearDown() override {
metricsService->clearForTests();
}
std::unique_ptr<MetricsService> metricsService;
};
@ -221,10 +224,12 @@ template <typename T>
class MetricCreationTest : public MetricsServiceTest {};
using testing::_;
using testing::AnyOf;
using testing::Contains;
using testing::ElementsAre;
using testing::ElementsAreArray;
using testing::Matcher;
using testing::Not;
using testing::UnorderedElementsAre;
using unittest::match::BSONElementEQ;
using unittest::match::BSONObjElements;
@ -398,7 +403,7 @@ TYPED_TEST(MetricCreationTest, ExceptionWhenSameNameButDifferentServerStatusOpti
MetricOptions<TypeParam> optionsRoleNone{.serverStatusOptions = ServerStatusOptions{
.dottedPath = sharedPath,
.role = ClusterRole{ClusterRole::None},
.role = ClusterRole::None,
}};
MetricCreator<TypeParam>::create(this->metricsService.get(),
MetricNames::kTest1,
@ -408,7 +413,7 @@ TYPED_TEST(MetricCreationTest, ExceptionWhenSameNameButDifferentServerStatusOpti
MetricOptions<TypeParam> optionsShardRole{.serverStatusOptions = ServerStatusOptions{
.dottedPath = sharedPath,
.role = ClusterRole{ClusterRole::ShardServer},
.role = ClusterRole::ShardServer,
}};
ASSERT_THROWS_CODE(MetricCreator<TypeParam>::create(this->metricsService.get(),
MetricNames::kTest1,
@ -649,7 +654,7 @@ using SerializeMetricsTreeTest = MetricsServiceTest;
TEST_F(SerializeMetricsTreeTest, Counter) {
CounterOptions options{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "ingress.openConnections",
.role = ClusterRole{ClusterRole::None},
.role = ClusterRole::None,
}};
auto& counter = metricsService->createInt64Counter(
MetricNames::kTest1, "description", MetricUnit::kSeconds, options);
@ -664,7 +669,7 @@ TEST_F(SerializeMetricsTreeTest, Counter) {
TEST_F(SerializeMetricsTreeTest, Histogram) {
HistogramOptions options{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "ops.latencyHistogram",
.role = ClusterRole{ClusterRole::None},
.role = ClusterRole::None,
}};
auto& histogram = metricsService->createDoubleHistogram(
MetricNames::kTest2, "description", MetricUnit::kSeconds, options);
@ -680,7 +685,7 @@ TEST_F(SerializeMetricsTreeTest, Histogram) {
TEST_F(SerializeMetricsTreeTest, RoleShard) {
CounterOptions options{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "ingress.openConnections",
.role = ClusterRole{ClusterRole::ShardServer},
.role = ClusterRole::ShardServer,
}};
auto& counter = metricsService->createInt64Counter(
MetricNames::kTest1, "description", MetricUnit::kSeconds, options);
@ -693,17 +698,29 @@ TEST_F(SerializeMetricsTreeTest, RoleShard) {
BSONObjBuilder noneBuilder;
mongo::globalMetricTreeSet()[ClusterRole::None].appendTo(noneBuilder);
ASSERT_TRUE(noneBuilder.obj().isEmpty());
BSONObj noneObj = noneBuilder.obj();
ASSERT_THAT(noneObj["metrics"],
AnyOf(IsBSONElement(_, BSONType::eoo, _),
IsBSONElement(_,
BSONType::object,
Matcher<BSONObj>(Not(BSONObjElements(
Contains(IsBSONElement("ingress", _, _))))))));
BSONObjBuilder routerBuilder;
mongo::globalMetricTreeSet()[ClusterRole::RouterServer].appendTo(routerBuilder);
ASSERT_TRUE(routerBuilder.obj().isEmpty());
BSONObj routerObj = routerBuilder.obj();
ASSERT_THAT(routerObj["metrics"],
AnyOf(IsBSONElement(_, BSONType::eoo, _),
IsBSONElement(_,
BSONType::object,
Matcher<BSONObj>(Not(BSONObjElements(
Contains(IsBSONElement("ingress", _, _))))))));
}
TEST_F(SerializeMetricsTreeTest, RoleRouter) {
CounterOptions options{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "ingress.openConnections",
.role = ClusterRole{ClusterRole::RouterServer},
.role = ClusterRole::RouterServer,
}};
auto& counter = metricsService->createInt64Counter(
MetricNames::kTest1, "description", MetricUnit::kSeconds, options);
@ -716,17 +733,29 @@ TEST_F(SerializeMetricsTreeTest, RoleRouter) {
BSONObjBuilder noneBuilder;
mongo::globalMetricTreeSet()[ClusterRole::None].appendTo(noneBuilder);
ASSERT_TRUE(noneBuilder.obj().isEmpty());
BSONObj noneObj = noneBuilder.obj();
ASSERT_THAT(noneObj["metrics"],
AnyOf(IsBSONElement(_, BSONType::eoo, _),
IsBSONElement(_,
BSONType::object,
Matcher<BSONObj>(Not(BSONObjElements(
Contains(IsBSONElement("ingress", _, _))))))));
BSONObjBuilder shardBuilder;
mongo::globalMetricTreeSet()[ClusterRole::ShardServer].appendTo(shardBuilder);
ASSERT_TRUE(shardBuilder.obj().isEmpty());
BSONObj shardObj = shardBuilder.obj();
ASSERT_THAT(shardObj["metrics"],
AnyOf(IsBSONElement(_, BSONType::eoo, _),
IsBSONElement(_,
BSONType::object,
Matcher<BSONObj>(Not(BSONObjElements(
Contains(IsBSONElement("ingress", _, _))))))));
}
TEST_F(SerializeMetricsTreeTest, RoleNone) {
CounterOptions options{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "ingress.openConnections",
.role = ClusterRole{ClusterRole::None},
.role = ClusterRole::None,
}};
auto& counter = metricsService->createInt64Counter(
MetricNames::kTest1, "description", MetricUnit::kSeconds, options);
@ -738,26 +767,36 @@ TEST_F(SerializeMetricsTreeTest, RoleNone) {
BSONObjBuilder shardBuilder;
mongo::globalMetricTreeSet()[ClusterRole::ShardServer].appendTo(shardBuilder);
ASSERT_TRUE(shardBuilder.obj().isEmpty());
BSONObj shardObj = shardBuilder.obj();
ASSERT_THAT(shardObj["metrics"],
AnyOf(IsBSONElement(_, BSONType::eoo, _),
IsBSONElement(_,
BSONType::object,
Matcher<BSONObj>(Not(BSONObjElements(
Contains(IsBSONElement("ingress", _, _))))))));
BSONObjBuilder routerBuilder;
mongo::globalMetricTreeSet()[ClusterRole::RouterServer].appendTo(routerBuilder);
ASSERT_TRUE(routerBuilder.obj().isEmpty());
BSONObj routerObj = routerBuilder.obj();
ASSERT_THAT(routerObj["metrics"],
AnyOf(IsBSONElement(_, BSONType::eoo, _),
IsBSONElement(_,
BSONType::object,
Matcher<BSONObj>(Not(BSONObjElements(
Contains(IsBSONElement("ingress", _, _))))))));
}
TEST_F(SerializeMetricsTreeTest, SamePathDifferentMetricNamesDifferentRoles) {
const std::string dottedPath = "counter";
CounterOptions shardOptions{
.serverStatusOptions = ServerStatusOptions{.dottedPath = dottedPath,
.role = ClusterRole{ClusterRole::ShardServer}}};
CounterOptions shardOptions{.serverStatusOptions = ServerStatusOptions{
.dottedPath = dottedPath, .role = ClusterRole::ShardServer}};
auto& shardCounter = metricsService->createInt64Counter(
MetricNames::kTest1, "description", MetricUnit::kSeconds, shardOptions);
shardCounter.add(7);
CounterOptions routerOptions{
.serverStatusOptions = ServerStatusOptions{.dottedPath = dottedPath,
.role = ClusterRole{ClusterRole::RouterServer}}};
CounterOptions routerOptions{.serverStatusOptions = ServerStatusOptions{
.dottedPath = dottedPath, .role = ClusterRole::RouterServer}};
auto& routerCounter = metricsService->createInt64Counter(
MetricNames::kTest2, "description", MetricUnit::kSeconds, routerOptions);
routerCounter.add(9);
@ -774,7 +813,7 @@ TEST_F(SerializeMetricsTreeTest, SamePathDifferentMetricNamesDifferentRoles) {
TEST_F(SerializeMetricsTreeTest, SharedPrefixSiblingLeaves) {
CounterOptions optionsA{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "common.metricA",
.role = ClusterRole{ClusterRole::None},
.role = ClusterRole::None,
}};
auto& counterA = metricsService->createInt64Counter(
MetricNames::kTest1, "description", MetricUnit::kSeconds, optionsA);
@ -782,7 +821,7 @@ TEST_F(SerializeMetricsTreeTest, SharedPrefixSiblingLeaves) {
CounterOptions optionsB{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "common.metricB",
.role = ClusterRole{ClusterRole::None},
.role = ClusterRole::None,
}};
auto& counterB = metricsService->createInt64Counter(
MetricNames::kTest2, "description", MetricUnit::kSeconds, optionsB);
@ -801,7 +840,7 @@ TEST_F(SerializeMetricsTreeTest, SharedPrefixSiblingLeaves) {
TEST_F(SerializeMetricsTreeTest, SharedPrefixShallowAndDeep) {
CounterOptions shallowOptions{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "common.shallowMetric",
.role = ClusterRole{ClusterRole::None},
.role = ClusterRole::None,
}};
auto& shallowCounter = metricsService->createInt64Counter(
MetricNames::kTest1, "description", MetricUnit::kSeconds, shallowOptions);
@ -809,7 +848,7 @@ TEST_F(SerializeMetricsTreeTest, SharedPrefixShallowAndDeep) {
CounterOptions deepOptions{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "common.nested.deepMetric",
.role = ClusterRole{ClusterRole::None},
.role = ClusterRole::None,
}};
auto& deepCounter = metricsService->createInt64Counter(
MetricNames::kTest2, "description", MetricUnit::kSeconds, deepOptions);
@ -1156,5 +1195,113 @@ TEST_F(CreateHistogramTest, RecordsDoubleValuesExplicitBoundaries) {
EXPECT_EQ(data2.count, 1);
}
}
using ClearForTestsTest = MetricsServiceTest;
TEST_F(ClearForTestsTest, RemovesFromBothMetricsServiceAndServerStatusTree) {
CounterOptions options{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "ingress.openConnections",
.role = ClusterRole::None,
}};
auto& counter = metricsService->createInt64Counter(
MetricNames::kTest1, "description", MetricUnit::kSeconds, options);
counter.add(11);
metricsService->clearForTests();
OtelMetricsCapturer metricsCapturer(*metricsService);
if (metricsCapturer.canReadMetrics()) {
ASSERT_THROWS_CODE(metricsCapturer.readInt64Counter(MetricNames::kTest1),
DBException,
ErrorCodes::KeyNotFound);
}
{
BSONObjBuilder builder;
mongo::globalMetricTreeSet()[ClusterRole::None].appendTo(builder);
ASSERT_THAT(builder.obj()["metrics"],
AnyOf(IsBSONElement(_, BSONType::eoo, _),
IsBSONElement(_,
BSONType::object,
Matcher<BSONObj>(Not(BSONObjElements(
Contains(IsBSONElement("ingress", _, _))))))));
}
}
TEST_F(ClearForTestsTest, RemovesFromAllServerStatusTrees) {
struct RoleAndMetricName {
ClusterRole role;
const MetricName& name;
};
for (auto [role, name] : {RoleAndMetricName{ClusterRole::None, MetricNames::kTest1},
RoleAndMetricName{ClusterRole::ShardServer, MetricNames::kTest2},
RoleAndMetricName{ClusterRole::RouterServer, MetricNames::kTest3}}) {
CounterOptions options{.serverStatusOptions = ServerStatusOptions{
.dottedPath = "ingress.openConnections",
.role = role,
}};
auto& counter = metricsService->createInt64Counter(
name, "description", MetricUnit::kOperations, options);
counter.add(11);
BSONObjBuilder builder;
mongo::globalMetricTreeSet()[ClusterRole(role)].appendTo(builder);
ASSERT_EQ(builder.obj()["metrics"]["ingress"]["openConnections"].Long(), 11);
}
metricsService->clearForTests();
for (auto role : {ClusterRole::None, ClusterRole::ShardServer, ClusterRole::RouterServer}) {
BSONObjBuilder builder;
mongo::globalMetricTreeSet()[ClusterRole(role)].appendTo(builder);
ASSERT_THAT(builder.obj()["metrics"],
AnyOf(IsBSONElement(_, BSONType::eoo, _),
IsBSONElement(_,
BSONType::object,
Matcher<BSONObj>(Not(BSONObjElements(
Contains(IsBSONElement("ingress", _, _))))))));
}
}
TEST_F(ClearForTestsTest, ClearsObservableCallbacks) {
OtelMetricsCapturer metricsCapturer(*metricsService);
if (!metricsCapturer.canReadMetrics()) {
return;
}
auto& counter = metricsService->createInt64Counter(
MetricNames::kTest1, "description", MetricUnit::kSeconds);
counter.add(11);
ASSERT_EQ(metricsCapturer.readInt64Counter(MetricNames::kTest1), 11);
metricsService->clearForTests();
// Re-register the same metric name. If the old observable callback was not cleared, triggering
// an export would invoke it with a dangling pointer (crash), or report the stale value 11.
metricsService->createInt64Counter(MetricNames::kTest1, "description", MetricUnit::kSeconds);
ASSERT_EQ(metricsCapturer.readInt64Counter(MetricNames::kTest1), 0);
}
TEST_F(ClearForTestsTest, AllowsReregistrationWithDifferentOptions) {
OtelMetricsCapturer metricsCapturer(*metricsService);
metricsService->createInt64Counter(MetricNames::kTest1, "description", MetricUnit::kSeconds);
metricsService->clearForTests();
auto& counter =
metricsService->createInt64Counter(MetricNames::kTest1, "description", MetricUnit::kBytes);
counter.add(5);
if (metricsCapturer.canReadMetrics()) {
EXPECT_EQ(metricsCapturer.readInt64Counter(MetricNames::kTest1), 5);
}
}
TEST_F(ClearForTestsTest, AllowsReregistrationWithDifferentType) {
OtelMetricsCapturer metricsCapturer(*metricsService);
metricsService->createInt64Counter(MetricNames::kTest1, "description", MetricUnit::kSeconds);
metricsService->clearForTests();
auto& counter = metricsService->createDoubleCounter(
MetricNames::kTest1, "description", MetricUnit::kSeconds);
counter.add(5.0);
if (metricsCapturer.canReadMetrics()) {
EXPECT_DOUBLE_EQ(metricsCapturer.readDoubleCounter(MetricNames::kTest1), 5.0);
}
}
} // namespace
} // namespace mongo::otel::metrics