summaryrefslogtreecommitdiff
path: root/src/mongo
diff options
context:
space:
mode:
authorHenrik Edin <henrik.edin@mongodb.com>2020-01-06 18:56:47 +0000
committerevergreen <evergreen@mongodb.com>2020-01-06 18:56:47 +0000
commit5e270d8ee59dc0757167b2720aacc9e63e05212e (patch)
tree0ce1452e45031f23d8338a6084f5b46d51effa4a /src/mongo
parent7e1394a188d756ce9a7e74c75f6ada5bb4b7949f (diff)
downloadmongo-5e270d8ee59dc0757167b2720aacc9e63e05212e.tar.gz
SERVER-45125 Add support for logging standard containers and ranges in logv2
Sequential containers are formatted as arrays in JSON Associative containers are formatted as object where keys and values are elements.
Diffstat (limited to 'src/mongo')
-rw-r--r--src/mongo/logv2/attribute_storage.h416
-rw-r--r--src/mongo/logv2/log_test_v2.cpp201
2 files changed, 554 insertions, 63 deletions
diff --git a/src/mongo/logv2/attribute_storage.h b/src/mongo/logv2/attribute_storage.h
index 18904d65899..6b64f3d9622 100644
--- a/src/mongo/logv2/attribute_storage.h
+++ b/src/mongo/logv2/attribute_storage.h
@@ -53,10 +53,48 @@ struct CustomAttributeValue {
std::function<std::string()> toString;
};
+template <typename T>
+auto seqLog(const T& container);
+
+template <typename It>
+auto seqLog(It begin, It end);
+
+template <typename T>
+auto mapLog(const T& container);
+
+template <typename It>
+auto mapLog(It begin, It end);
+
namespace detail {
namespace {
// Helper traits to figure out capabilities on custom types
+template <class T>
+struct IsOptional : std::false_type {};
+
+template <class T>
+struct IsOptional<boost::optional<T>> : std::true_type {};
+
+template <class T, typename = void>
+struct IsContainer : std::false_type {};
+
+template <class T, typename = void>
+struct HasMappedType : std::false_type {};
+
+template <typename T>
+struct HasMappedType<T, std::void_t<typename T::mapped_type>> : std::true_type {};
+
+// Trait to detect container, common interface for both std::array and std::forward_list
+template <typename T>
+struct IsContainer<T,
+ std::void_t<typename T::value_type,
+ typename T::size_type,
+ typename T::iterator,
+ typename T::const_iterator,
+ decltype(std::declval<T>().empty()),
+ decltype(std::declval<T>().begin()),
+ decltype(std::declval<T>().end())>> : std::true_type {};
+
template <class T, class = void>
struct HasToBSON : std::false_type {};
@@ -105,88 +143,315 @@ struct HasToString<T, std::void_t<decltype(std::declval<T>().toString())>> : std
} // namespace
-// Named attribute, storage for a name-value attribute.
-class NamedAttribute {
-public:
- NamedAttribute() = default;
- NamedAttribute(StringData n, int val) : name(n), value(val) {}
- NamedAttribute(StringData n, unsigned int val) : name(n), value(val) {}
- // long is 32bit on Windows and 64bit on posix. To avoid ambiguity where different platforms we
- // treat long as 64bit always
- NamedAttribute(StringData n, long val) : name(n), value(static_cast<long long>(val)) {}
- NamedAttribute(StringData n, unsigned long val)
- : name(n), value(static_cast<unsigned long long>(val)) {}
- NamedAttribute(StringData n, long long val) : name(n), value(val) {}
- NamedAttribute(StringData n, unsigned long long val) : name(n), value(val) {}
- NamedAttribute(StringData n, double val) : name(n), value(val) {}
- NamedAttribute(StringData n, bool val) : name(n), value(val) {}
- NamedAttribute(StringData n, StringData val) : name(n), value(val) {}
- NamedAttribute(StringData n, BSONObj const& val) : name(n), value(&val) {}
- NamedAttribute(StringData n, BSONArray const& val) : name(n), value(&val) {}
- NamedAttribute(StringData n, const char* val) : NamedAttribute(n, StringData(val)) {}
- NamedAttribute(StringData n, char* val) : NamedAttribute(n, static_cast<const char*>(val)) {}
- NamedAttribute(StringData n, float val) : NamedAttribute(n, static_cast<double>(val)) {}
- NamedAttribute(StringData n, std::string const& val) : NamedAttribute(n, StringData(val)) {}
- NamedAttribute(StringData n, long double val) = delete;
+// Mapping functions on how to map a logged value to how it is stored in variant (reused by
+// container support)
+inline bool mapValue(bool value) {
+ return value;
+}
+inline int mapValue(int value) {
+ return value;
+}
+inline unsigned int mapValue(unsigned int value) {
+ return value;
+}
+inline long long mapValue(long value) {
+ return value;
+}
+inline unsigned long long mapValue(unsigned long value) {
+ return value;
+}
+inline long long mapValue(long long value) {
+ return value;
+}
+inline unsigned long long mapValue(unsigned long long value) {
+ return value;
+}
+inline double mapValue(float value) {
+ return value;
+}
+inline double mapValue(double value) {
+ return value;
+}
+inline StringData mapValue(StringData value) {
+ return value;
+}
+inline StringData mapValue(std::string const& value) {
+ return value;
+}
+inline StringData mapValue(char* value) {
+ return value;
+}
+inline StringData mapValue(const char* value) {
+ return value;
+}
+inline const BSONObj* mapValue(BSONObj const& value) {
+ return &value;
+}
+inline const BSONArray* mapValue(BSONArray const& value) {
+ return &value;
+}
+inline CustomAttributeValue mapValue(BSONElement const& val) {
+ CustomAttributeValue custom;
+ custom.BSONSerialize = [&val](BSONObjBuilder& builder) { builder.appendElements(val.wrap()); };
+ custom.toString = [&val]() { return val.toString(); };
+ return custom;
+}
+inline CustomAttributeValue mapValue(boost::none_t val) {
+ CustomAttributeValue custom;
+ // Use BSONAppend instead of toBSON because we just want the null value and not a whole
+ // object with a field name
+ custom.BSONAppend = [](BSONObjBuilder& builder, StringData fieldName) {
+ builder.appendNull(fieldName);
+ };
+ custom.toString = [&val]() { return constants::kNullOptionalString.toString(); };
+ return custom;
+}
+
+template <typename T, std::enable_if_t<IsContainer<T>::value && !HasMappedType<T>::value, int> = 0>
+CustomAttributeValue mapValue(const T& val) {
+ CustomAttributeValue custom;
+ custom.toBSONArray = [&val]() { return seqLog(val).toBSONArray(); };
+ custom.stringSerialize = [&val](fmt::memory_buffer& buffer) { seqLog(val).serialize(buffer); };
+ return custom;
+}
+
+template <typename T, std::enable_if_t<IsContainer<T>::value && HasMappedType<T>::value, int> = 0>
+CustomAttributeValue mapValue(const T& val) {
+ CustomAttributeValue custom;
+ custom.BSONSerialize = [&val](BSONObjBuilder& builder) { mapLog(val).serialize(&builder); };
+ custom.stringSerialize = [&val](fmt::memory_buffer& buffer) { mapLog(val).serialize(buffer); };
+ return custom;
+}
- NamedAttribute(StringData n, BSONElement const& val) : name(n) {
- CustomAttributeValue custom;
+template <typename T,
+ std::enable_if_t<!std::is_integral_v<T> && !std::is_floating_point_v<T> &&
+ !IsContainer<T>::value,
+ int> = 0>
+CustomAttributeValue mapValue(const T& val) {
+ static_assert(HasToString<T>::value || HasStringSerialize<T>::value,
+ "custom type needs toString() or serialize(fmt::memory_buffer&) implementation");
+
+ CustomAttributeValue custom;
+ if constexpr (HasBSONBuilderAppend<T>::value) {
+ custom.BSONAppend = [&val](BSONObjBuilder& builder, StringData fieldName) {
+ builder.append(fieldName, val);
+ };
+ }
+ if constexpr (HasBSONSerialize<T>::value) {
+ custom.BSONSerialize = [&val](BSONObjBuilder& builder) { val.serialize(&builder); };
+ } else if constexpr (HasToBSON<T>::value) {
custom.BSONSerialize = [&val](BSONObjBuilder& builder) {
- builder.appendElements(val.wrap());
+ builder.appendElements(val.toBSON());
};
+ } else if constexpr (HasToBSONArray<T>::value) {
+ custom.toBSONArray = [&val]() { return val.toBSONArray(); };
+ }
+ if constexpr (HasStringSerialize<T>::value) {
+ custom.stringSerialize = [&val](fmt::memory_buffer& buffer) { val.serialize(buffer); };
+ } else if constexpr (HasToString<T>::value) {
custom.toString = [&val]() { return val.toString(); };
- value = std::move(custom);
}
- template <typename T>
- NamedAttribute(StringData n, const boost::optional<T>& val)
- : NamedAttribute(val ? NamedAttribute(n, *val) : NamedAttribute()) {
- if (!val) {
- CustomAttributeValue custom;
- // Use BSONAppend instead of toBSON because we just want the null value and not a whole
- // object with a field name
- custom.BSONAppend = [](BSONObjBuilder& builder, StringData fieldName) {
- builder.appendNull(fieldName);
+ return custom;
+}
+
+template <typename It>
+class SequenceContainerLogger {
+public:
+ SequenceContainerLogger(It begin, It end) : _begin(begin), _end(end) {}
+
+ // JSON Format: [elem1, elem2, ..., elemN]
+ BSONArray toBSONArray() const {
+ BSONArrayBuilder builder;
+ for (auto it = _begin; it != _end; ++it) {
+ const auto& item = *it;
+ auto append = [&builder](auto&& val) {
+ if constexpr (std::is_same_v<decltype(val), CustomAttributeValue&&>) {
+ if (val.BSONAppend) {
+ BSONObjBuilder objBuilder;
+ val.BSONAppend(objBuilder, ""_sd);
+ builder.append(objBuilder.done().getField(""_sd));
+ } else if (val.BSONSerialize) {
+ BSONObjBuilder objBuilder;
+ val.BSONSerialize(objBuilder);
+ builder.append(objBuilder.done());
+ } else if (val.toBSONArray) {
+ builder.append(val.toBSONArray());
+ } else if (val.stringSerialize) {
+ fmt::memory_buffer buffer;
+ val.stringSerialize(buffer);
+ builder.append(fmt::to_string(buffer));
+ } else {
+ builder.append(val.toString());
+ }
+ } else {
+ builder.append(val);
+ }
};
- custom.toString = [&val]() { return constants::kNullOptionalString.toString(); };
- name = n;
- value = std::move(custom);
+
+ using item_t = std::decay_t<decltype(item)>;
+ if constexpr (IsOptional<item_t>::value) {
+ if (item) {
+ append(mapValue(*item));
+ } else {
+ append(mapValue(boost::none));
+ }
+ } else {
+ append(mapValue(item));
+ }
}
+ return builder.arr();
}
- template <typename T,
- typename = std::enable_if_t<!std::is_integral_v<T> && !std::is_floating_point_v<T>>>
- NamedAttribute(StringData n, const T& val) : name(n) {
- static_assert(
- HasToString<T>::value || HasStringSerialize<T>::value,
- "custom type needs toString() or serialize(fmt::memory_buffer&) implementation");
-
- CustomAttributeValue custom;
- if constexpr (HasBSONBuilderAppend<T>::value) {
- custom.BSONAppend = [&val](BSONObjBuilder& builder, StringData fieldName) {
- builder.append(fieldName, val);
+ // Text Format: (elem1, elem2, ..., elemN)
+ void serialize(fmt::memory_buffer& buffer) const {
+ StringData separator = ""_sd;
+ buffer.push_back('(');
+ for (auto it = _begin; it != _end; ++it) {
+ const auto& item = *it;
+ buffer.append(separator.begin(), separator.end());
+
+ auto append = [&buffer](auto&& val) {
+ if constexpr (std::is_same_v<decltype(val), CustomAttributeValue&&>) {
+ if (val.stringSerialize) {
+ val.stringSerialize(buffer);
+ } else {
+ fmt::format_to(buffer, "{}", val.toString());
+ }
+ } else {
+ fmt::format_to(buffer, "{}", val);
+ }
};
+
+ using item_t = std::decay_t<decltype(item)>;
+ if constexpr (IsOptional<item_t>::value) {
+ if (item) {
+ append(mapValue(*item));
+ } else {
+ append(mapValue(boost::none));
+ }
+ } else {
+ append(mapValue(item));
+ }
+
+ separator = ", "_sd;
}
- if constexpr (HasBSONSerialize<T>::value) {
- custom.BSONSerialize = [&val](BSONObjBuilder& builder) { val.serialize(&builder); };
- } else if constexpr (HasToBSON<T>::value) {
- custom.BSONSerialize = [&val](BSONObjBuilder& builder) {
- builder.appendElements(val.toBSON());
+ buffer.push_back(')');
+ }
+
+private:
+ It _begin;
+ It _end;
+};
+
+template <typename It>
+class AssociativeContainerLogger {
+public:
+ static_assert(std::is_same_v<decltype(mapValue(std::declval<It>()->first)), StringData>,
+ "key in associative container needs to be a string");
+
+ AssociativeContainerLogger(It begin, It end) : _begin(begin), _end(end) {}
+
+ // JSON Format: {"elem1": val1, "elem2": val2, ..., "elemN": valN}
+ void serialize(BSONObjBuilder* builder) const {
+ for (auto it = _begin; it != _end; ++it) {
+ const auto& item = *it;
+ auto append = [builder](StringData key, auto&& val) {
+ if constexpr (std::is_same_v<decltype(val), CustomAttributeValue&&>) {
+ if (val.BSONAppend) {
+ val.BSONAppend(*builder, key);
+ } else if (val.BSONSerialize) {
+ BSONObjBuilder subBuilder = builder->subobjStart(key);
+ val.BSONSerialize(subBuilder);
+ subBuilder.done();
+ } else if (val.toBSONArray) {
+ builder->append(key, val.toBSONArray());
+ } else if (val.stringSerialize) {
+ fmt::memory_buffer buffer;
+ val.stringSerialize(buffer);
+ builder->append(key, fmt::to_string(buffer));
+ } else {
+ builder->append(key, val.toString());
+ }
+ } else {
+ builder->append(key, val);
+ }
};
- } else if constexpr (HasToBSONArray<T>::value) {
- custom.toBSONArray = [&val]() { return val.toBSONArray(); };
+ auto key = mapValue(item.first);
+ using value_t = std::decay_t<decltype(item.second)>;
+ if constexpr (IsOptional<value_t>::value) {
+ if (item.second) {
+ append(key, mapValue(*item.second));
+ } else {
+ append(key, mapValue(boost::none));
+ }
+ } else {
+ append(key, mapValue(item.second));
+ }
}
- if constexpr (HasStringSerialize<T>::value) {
- custom.stringSerialize = [&val](fmt::memory_buffer& buffer) {
- return val.serialize(buffer);
+ }
+
+ // Text Format: (elem1: val1, elem2: val2, ..., elemN: valN)
+ void serialize(fmt::memory_buffer& buffer) const {
+ StringData separator = ""_sd;
+ buffer.push_back('(');
+ for (auto it = _begin; it != _end; ++it) {
+ const auto& item = *it;
+ buffer.append(separator.begin(), separator.end());
+
+ auto append = [&buffer](StringData key, auto&& val) {
+ if constexpr (std::is_same_v<decltype(val), CustomAttributeValue&&>) {
+ if (val.stringSerialize) {
+ fmt::format_to(buffer, "{}: ", key);
+ val.stringSerialize(buffer);
+ } else {
+ fmt::format_to(buffer, "{}: {}", key, val.toString());
+ }
+ } else {
+ fmt::format_to(buffer, "{}: {}", key, val);
+ }
};
- } else if constexpr (HasToString<T>::value) {
- custom.toString = [&val]() { return val.toString(); };
+
+ auto key = mapValue(item.first);
+ using value_t = std::decay_t<decltype(item.second)>;
+ if constexpr (IsOptional<value_t>::value) {
+ if (item.second) {
+ append(key, mapValue(*item.second));
+ } else {
+ append(key, mapValue(boost::none));
+ }
+ } else {
+ append(key, mapValue(item.second));
+ }
+
+ separator = ", "_sd;
}
+ buffer.push_back(')');
+ }
- value = std::move(custom);
+private:
+ It _begin;
+ It _end;
+};
+
+// Named attribute, storage for a name-value attribute.
+class NamedAttribute {
+public:
+ NamedAttribute() = default;
+ NamedAttribute(StringData n, long double val) = delete;
+
+ template <typename T>
+ NamedAttribute(StringData n, const boost::optional<T>& val)
+ : NamedAttribute(val ? NamedAttribute(n, *val) : NamedAttribute()) {
+ if (!val) {
+ name = n;
+ value = mapValue(boost::none);
+ }
}
+ template <typename T>
+ NamedAttribute(StringData n, const T& val) : name(n), value(mapValue(val)) {}
+
StringData name;
stdx::variant<int,
unsigned int,
@@ -260,5 +525,30 @@ private:
size_t _size;
};
+// Helpers for logging containers, optional to use. Allowes logging of ranges.
+template <typename T>
+auto seqLog(const T& container) {
+ using std::begin;
+ using std::end;
+ return detail::SequenceContainerLogger(begin(container), end(container));
+}
+
+template <typename It>
+auto seqLog(It begin, It end) {
+ return detail::SequenceContainerLogger(begin, end);
+}
+
+template <typename T>
+auto mapLog(const T& container) {
+ using std::begin;
+ using std::end;
+ return detail::AssociativeContainerLogger(begin(container), end(container));
+}
+
+template <typename It>
+auto mapLog(It begin, It end) {
+ return detail::AssociativeContainerLogger(begin, end);
+}
+
} // namespace logv2
} // namespace mongo
diff --git a/src/mongo/logv2/log_test_v2.cpp b/src/mongo/logv2/log_test_v2.cpp
index d0899129074..473d26028cc 100644
--- a/src/mongo/logv2/log_test_v2.cpp
+++ b/src/mongo/logv2/log_test_v2.cpp
@@ -754,6 +754,207 @@ TEST_F(LogTestV2, JsonBsonFormat) {
validateCustomAttrBSONArray(BSONObj(linesBson.back().data()));
}
+TEST_F(LogTestV2, Containers) {
+ using namespace constants;
+
+ std::vector<std::string> text;
+ auto text_sink = LogTestBackend::create(text);
+ text_sink->set_filter(ComponentSettingsFilter(LogManager::global().getGlobalDomain(),
+ LogManager::global().getGlobalSettings()));
+ text_sink->set_formatter(PlainFormatter());
+ attach(text_sink);
+
+ std::vector<std::string> json;
+ auto json_sink = LogTestBackend::create(json);
+ json_sink->set_filter(ComponentSettingsFilter(LogManager::global().getGlobalDomain(),
+ LogManager::global().getGlobalSettings()));
+ json_sink->set_formatter(JSONFormatter());
+ attach(json_sink);
+
+ std::vector<std::string> bson;
+ auto bson_sink = LogTestBackend::create(bson);
+ bson_sink->set_filter(ComponentSettingsFilter(LogManager::global().getGlobalDomain(),
+ LogManager::global().getGlobalSettings()));
+ bson_sink->set_formatter(BSONFormatter());
+ attach(bson_sink);
+
+ // Helper to create a comma separated list of a container, stringify is function on how to
+ // transform element into a string.
+ auto text_join = [](auto begin, auto end, auto stringify) -> std::string {
+ if (begin == end)
+ return "()";
+
+ auto second = begin;
+ ++second;
+ if (second == end)
+ return fmt::format("({})", stringify(*begin));
+
+ return fmt::format(
+ "({})",
+ std::accumulate(
+ second, end, stringify(*begin), [&stringify](std::string result, auto&& item) {
+ return result + ", " + stringify(item);
+ }));
+ };
+
+ // All standard sequential containers are supported
+ std::vector<std::string> vectorStrings = {"str1", "str2", "str3"};
+ LOGV2("{}", "name"_attr = vectorStrings);
+ ASSERT_EQUALS(text.back(),
+ text_join(vectorStrings.begin(), vectorStrings.end(), [](const std::string& str) {
+ return str;
+ }));
+ auto validateStringVector = [&vectorStrings](const BSONObj& obj) {
+ std::vector<BSONElement> jsonVector =
+ obj.getField(kAttributesFieldName).Obj().getField("name").Array();
+ ASSERT_EQUALS(vectorStrings.size(), jsonVector.size());
+ for (std::size_t i = 0; i < vectorStrings.size(); ++i)
+ ASSERT_EQUALS(jsonVector[i].String(), vectorStrings[i]);
+ };
+ validateStringVector(mongo::fromjson(json.back()));
+ validateStringVector(BSONObj(bson.back().data()));
+
+ // Elements can require custom formatting
+ std::list<TypeWithBSON> listCustom = {
+ TypeWithBSON(0.0, 1.0), TypeWithBSON(2.0, 3.0), TypeWithBSON(4.0, 5.0)};
+ LOGV2("{}", "name"_attr = listCustom);
+ ASSERT_EQUALS(text.back(),
+ text_join(listCustom.begin(), listCustom.end(), [](const auto& item) {
+ return item.toString();
+ }));
+ auto validateBSONObjList = [&listCustom](const BSONObj& obj) {
+ std::vector<BSONElement> jsonVector =
+ obj.getField(kAttributesFieldName).Obj().getField("name").Array();
+ ASSERT_EQUALS(listCustom.size(), jsonVector.size());
+ auto in = listCustom.begin();
+ auto out = jsonVector.begin();
+ for (; in != listCustom.end(); ++in, ++out) {
+ ASSERT(in->toBSON().woCompare(out->Obj()) == 0);
+ }
+ };
+ validateBSONObjList(mongo::fromjson(json.back()));
+ validateBSONObjList(BSONObj(bson.back().data()));
+
+ // Optionals are also allowed as elements
+ std::forward_list<boost::optional<bool>> listOptionalBool = {true, boost::none, false};
+ LOGV2("{}", "name"_attr = listOptionalBool);
+ ASSERT_EQUALS(text.back(),
+ text_join(listOptionalBool.begin(),
+ listOptionalBool.end(),
+ [](const auto& item) -> std::string {
+ if (!item)
+ return constants::kNullOptionalString.toString();
+ else if (*item)
+ return "true";
+ else
+ return "false";
+ }));
+ auto validateOptionalBool = [&listOptionalBool](const BSONObj& obj) {
+ std::vector<BSONElement> jsonVector =
+ obj.getField(kAttributesFieldName).Obj().getField("name").Array();
+ auto in = listOptionalBool.begin();
+ auto out = jsonVector.begin();
+ for (; in != listOptionalBool.end() && out != jsonVector.end(); ++in, ++out) {
+ if (*in)
+ ASSERT_EQUALS(**in, out->Bool());
+ else
+ ASSERT(out->isNull());
+ }
+ ASSERT(in == listOptionalBool.end());
+ ASSERT(out == jsonVector.end());
+ };
+ validateOptionalBool(mongo::fromjson(json.back()));
+ validateOptionalBool(BSONObj(bson.back().data()));
+
+ // Containers can be nested
+ std::array<std::deque<int>, 4> arrayOfDeques = {{{0, 1}, {2, 3}, {4, 5}, {6, 7}}};
+ LOGV2("{}", "name"_attr = arrayOfDeques);
+ ASSERT_EQUALS(text.back(),
+ text_join(arrayOfDeques.begin(),
+ arrayOfDeques.end(),
+ [text_join](const std::deque<int>& deque) {
+ return text_join(deque.begin(), deque.end(), [](int val) {
+ return fmt::format("{}", val);
+ });
+ }));
+ auto validateArrayOfDeques = [&arrayOfDeques](const BSONObj& obj) {
+ std::vector<BSONElement> jsonVector =
+ obj.getField(kAttributesFieldName).Obj().getField("name").Array();
+ ASSERT_EQUALS(arrayOfDeques.size(), jsonVector.size());
+ auto in = arrayOfDeques.begin();
+ auto out = jsonVector.begin();
+ for (; in != arrayOfDeques.end(); ++in, ++out) {
+ std::vector<BSONElement> inner_array = out->Array();
+ ASSERT_EQUALS(in->size(), inner_array.size());
+ auto inner_begin = in->begin();
+ auto inner_end = in->end();
+
+ auto inner_out = inner_array.begin();
+ for (; inner_begin != inner_end; ++inner_begin, ++inner_out) {
+ ASSERT_EQUALS(*inner_begin, inner_out->Int());
+ }
+ }
+ };
+ validateArrayOfDeques(mongo::fromjson(json.back()));
+ validateArrayOfDeques(BSONObj(bson.back().data()));
+
+ // Associative containers are also supported
+ std::map<std::string, std::string> mapStrStr = {{"key1", "val1"}, {"key2", "val2"}};
+ LOGV2("{}", "name"_attr = mapStrStr);
+ ASSERT_EQUALS(text.back(), text_join(mapStrStr.begin(), mapStrStr.end(), [](const auto& item) {
+ return fmt::format("{}: {}", item.first, item.second);
+ }));
+ auto validateMapOfStrings = [&mapStrStr](const BSONObj& obj) {
+ BSONObj mappedValues = obj.getField(kAttributesFieldName).Obj().getField("name").Obj();
+ auto in = mapStrStr.begin();
+ for (; in != mapStrStr.end(); ++in) {
+ ASSERT_EQUALS(mappedValues.getField(in->first).String(), in->second);
+ }
+ };
+ validateMapOfStrings(mongo::fromjson(json.back()));
+ validateMapOfStrings(BSONObj(bson.back().data()));
+
+ // Associative containers with optional sequential container is ok too
+ stdx::unordered_map<std::string, boost::optional<std::vector<int>>> mapOptionalVector = {
+ {"key1", boost::optional<std::vector<int>>{{1, 2, 3}}},
+ {"key2", boost::optional<std::vector<int>>{boost::none}}};
+
+ LOGV2("{}", "name"_attr = mapOptionalVector);
+ ASSERT_EQUALS(
+ text.back(),
+ text_join(mapOptionalVector.begin(),
+ mapOptionalVector.end(),
+ [text_join](const std::pair<std::string, boost::optional<std::vector<int>>>&
+ optionalVectorItem) {
+ if (!optionalVectorItem.second)
+ return optionalVectorItem.first + ": " +
+ constants::kNullOptionalString.toString();
+ else
+ return optionalVectorItem.first + ": " +
+ text_join(optionalVectorItem.second->begin(),
+ optionalVectorItem.second->end(),
+ [](int val) { return fmt::format("{}", val); });
+ }));
+ auto validateMapOfOptionalVectors = [&mapOptionalVector](const BSONObj& obj) {
+ BSONObj mappedValues = obj.getField(kAttributesFieldName).Obj().getField("name").Obj();
+ auto in = mapOptionalVector.begin();
+ for (; in != mapOptionalVector.end(); ++in) {
+ BSONElement mapElement = mappedValues.getField(in->first);
+ if (!in->second)
+ ASSERT(mapElement.isNull());
+ else {
+ const std::vector<int>& intVec = *(in->second);
+ std::vector<BSONElement> jsonVector = mapElement.Array();
+ ASSERT_EQUALS(jsonVector.size(), intVec.size());
+ for (std::size_t i = 0; i < intVec.size(); ++i)
+ ASSERT_EQUALS(jsonVector[i].Int(), intVec[i]);
+ }
+ }
+ };
+ validateMapOfOptionalVectors(mongo::fromjson(json.back()));
+ validateMapOfOptionalVectors(BSONObj(bson.back().data()));
+}
+
TEST_F(LogTestV2, Unicode) {
std::vector<std::string> lines;
auto sink = LogTestBackend::create(lines);