summaryrefslogtreecommitdiff
path: root/src/mongo/logv2
diff options
context:
space:
mode:
authorHenrik Edin <henrik.edin@mongodb.com>2020-04-16 16:45:55 -0400
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-05-05 14:08:38 +0000
commitc65076829b29f977d6412f5309a3b8cf56ce9657 (patch)
tree388116bdbd0319e2ae1dea178c9f2b169924f17a /src/mongo/logv2
parent20df781cb8b801634389efa9b4c5568bc6b21d57 (diff)
downloadmongo-c65076829b29f977d6412f5309a3b8cf56ce9657.tar.gz
SERVER-47594 Add helpers to Structured Logging to abstract attribute names
* Users can implement logAttrs() for their types to provide common attribute names * multipleAttrs can be used to provide multiple attributes for an object
Diffstat (limited to 'src/mongo/logv2')
-rw-r--r--src/mongo/logv2/README.md84
-rw-r--r--src/mongo/logv2/log_attr.h128
-rw-r--r--src/mongo/logv2/log_detail.h64
-rw-r--r--src/mongo/logv2/logv2_test.cpp126
4 files changed, 382 insertions, 20 deletions
diff --git a/src/mongo/logv2/README.md b/src/mongo/logv2/README.md
index 97bc9bdad9d..42cd65969cc 100644
--- a/src/mongo/logv2/README.md
+++ b/src/mongo/logv2/README.md
@@ -393,6 +393,90 @@ JSON format:
"samples": [{"durationNanos": 200}, {"durationNanos": 400}]
```
+# Attribute naming abstraction
+
+The style guide contains recommendations for attribute naming in certain cases. To make abstraction of attribute naming possible a `logAttrs` function can be implemented as a friend function in a class with the following signature:
+
+```
+class AnyUserType {
+public:
+ friend auto logAttrs(const AnyUserType& instance) {
+ return "name"_attr=instance;
+ }
+
+ BSONObj toBSON() const; // Type needs to be loggable
+};
+```
+
+##### Examples
+```
+const AnyUserType& t = ...;
+LOGV2(2000, "log of user type", logAttr(t));
+```
+
+## Multiple attributes
+
+In some cases a loggable type might be composed as a hierarchy in the C++ type system which would lead to a very verbose structured log output as every level in the hierarcy needs a name when outputted as JSON. The attribute naming abstraction system can also be used to collapse such hierarchies. Instead of making a type loggable it can instead return one or more attributes from its members by using `multipleAttrs` in `logAttrs` functions.
+
+`multipleAttrs(...)` accepts attributes or instances of types with `logAttrs` functions implemented.
+
+##### Examples
+
+```
+class NotALoggableType {
+ std::string name;
+ BSONObj data;
+
+ friend auto logAttrs(const NotALoggableType& instance) {
+ return logv2::multipleAttrs("name"_attr=instance.name, "data"_attr=instance.data);
+ }
+};
+
+NotALoggableType t = ...;
+
+// These two log statements would produce the same output (apart from different id)
+
+LOGV2(2001, "Log of non-loggable type's members", logAttrs(t));
+
+LOGV2(2001, "Log of non-loggable type's members", "name"_attr=t.name, "data"_attr=t.data);
+
+```
+
+## Handling temporary lifetime with multiple attributes
+
+To avoid lifetime issues (log attributes bind their values by reference) it is recommended to **not** create attributes when using `multipleAttrs` unless attributes are created for members directly. If `logAttrs` or `""_attr=` is used inside a `logAttrs` function on the return of a function returning by value it will result in a dangling reference. The following example illustrates the problem
+
+```
+class SomeSubType {
+public:
+ BSONObj toBSON() const {...};
+
+ friend auto logAttrs(const SomeSubType& sub) {
+ return "subAttr"_attr=sub;
+ }
+};
+
+class SomeType {
+public:
+ const std::string& name() const { return name_; }
+ SomeSubType sub() const { return sub_; } // Returning by value!
+
+ friend auto logAttrs(const SomeType& type) {
+ // logAttrs(type.sub()) below will contain a danling reference!
+ return logv2::multipleAttrs("name"_attr=type.name(), logAttrs(type.sub()));
+ }
+private:
+ SomeSubType sub_;
+ std::string name_;
+};
+```
+The better implementation would be to let the log system control the lifetime by passing the instance to `multipleAttrs` without creating the attribute. The log system will detect that it is not an attribute and will attempt to create attributes by calling `logAttrs`:
+```
+friend auto logAttrs(const SomeType& type) {
+ return logv2::multipleAttrs("name"_attr=type.name(), type.sub());
+}
+```
+
# Additional features
## Combining uassert with log statement
diff --git a/src/mongo/logv2/log_attr.h b/src/mongo/logv2/log_attr.h
new file mode 100644
index 00000000000..2434b1bd3ce
--- /dev/null
+++ b/src/mongo/logv2/log_attr.h
@@ -0,0 +1,128 @@
+/**
+ * Copyright (C) 2020-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 <fmt/format.h>
+#include <tuple>
+#include <type_traits>
+
+namespace mongo {
+namespace logv2 {
+namespace detail {
+/**
+ * Helper to be used inside logAttrs() functions, captures rvalues by value so they don't go out of
+ * scope Can create a tuple of loggable named attributes for the logger
+ */
+template <typename... Ts>
+class ComposedAttr {
+public:
+ ComposedAttr(Ts&&... args) : _values(std::move(args)...) {}
+
+ /**
+ * Creates a flattend tuple of loggable named attributes
+ */
+ auto attributes() const;
+
+private:
+ std::tuple<Ts...> _values;
+};
+
+template <typename>
+struct IsComposedAttr : std::false_type {};
+
+template <typename... Ts>
+struct IsComposedAttr<ComposedAttr<Ts...>> : std::true_type {};
+
+/**
+ * Helper to make regular attributes composable with combine()
+ */
+template <typename T>
+auto logAttrs(const fmt::internal::named_arg<T, char>& a) {
+ return a;
+}
+
+/**
+ * Flattens the input into a single tuple (no tuples of tuples). Passes through the input by
+ * reference by possible. May only be used at the call side of the log system to avoid dangling
+ * references.
+ */
+template <typename T>
+std::tuple<const T&> toFlatAttributesTupleRef(const T& arg) {
+ return {arg};
+}
+
+template <typename... Ts>
+auto toFlatAttributesTupleRef(const ComposedAttr<Ts...>& arg) {
+ return arg.attributes();
+}
+
+/**
+ * Same as above but does not pass by reference. Needs to be used when building composed hierarchies
+ * in helper functions
+ */
+template <typename T>
+std::tuple<T> toFlatAttributesTuple(const T& arg) {
+ return {arg};
+}
+template <typename... Ts>
+auto toFlatAttributesTuple(const ComposedAttr<Ts...>& arg) {
+ return arg.attributes();
+}
+
+template <typename... Ts>
+auto ComposedAttr<Ts...>::attributes() const {
+ // logAttrs() converts the input to a loggable named attribute, user implementation
+ return std::apply(
+ [this](auto&&... args) { return std::tuple_cat(toFlatAttributesTuple(logAttrs(args))...); },
+ _values);
+}
+
+} // namespace detail
+
+/**
+ * Combines multiple attributes to be returned in user defined logAttrs() functions
+ */
+template <typename... Ts>
+auto multipleAttrs(Ts&&... attrs) {
+ // We can capture lvalue references as reference otherwise this is a temporary object that needs
+ // to be stored during the lifetime of the log statement.
+ return detail::ComposedAttr<
+ std::conditional_t<std::is_lvalue_reference_v<Ts>, Ts, std::remove_reference_t<Ts>>...>(
+ std::forward<Ts>(attrs)...);
+}
+
+} // namespace logv2
+
+inline namespace literals {
+inline fmt::internal::udl_arg<char> operator"" _attr(const char* s, std::size_t n) {
+ return fmt::operator""_a(s, n);
+}
+} // namespace literals
+} // namespace mongo
diff --git a/src/mongo/logv2/log_detail.h b/src/mongo/logv2/log_detail.h
index 5dbe0ec83b2..aab2d9faf09 100644
--- a/src/mongo/logv2/log_detail.h
+++ b/src/mongo/logv2/log_detail.h
@@ -32,6 +32,7 @@
#include "mongo/base/status.h"
#include "mongo/bson/util/builder.h"
#include "mongo/logv2/attribute_storage.h"
+#include "mongo/logv2/log_attr.h"
#include "mongo/logv2/log_component.h"
#include "mongo/logv2/log_domain.h"
#include "mongo/logv2/log_options.h"
@@ -53,12 +54,13 @@ void doUnstructuredLogImpl(LogSeverity const& severity, // NOLINT
TypeErasedAttributeStorage const& attrs);
+// doLogUnpacked overloads require the arguments to be flattened attributes
template <typename S, typename... Args>
-void doLog(int32_t id,
- LogSeverity const& severity,
- LogOptions const& options,
- S const& message,
- const fmt::internal::named_arg<Args, char>&... args) {
+void doLogUnpacked(int32_t id,
+ LogSeverity const& severity,
+ LogOptions const& options,
+ const S& message,
+ const fmt::internal::named_arg<Args, char>&... args) {
auto attributes = makeAttributeStorage(args...);
fmt::string_view msg{message};
@@ -66,31 +68,53 @@ void doLog(int32_t id,
}
template <typename S, size_t N, typename... Args>
+void doLogUnpacked(int32_t id,
+ LogSeverity const& severity,
+ LogOptions const& options,
+ const S&, // formatMsg not used
+ const char (&msg)[N],
+ const fmt::internal::named_arg<Args, char>&... args) {
+ doLogUnpacked(id, severity, options, msg, args...);
+}
+
+template <typename S>
+void doLogUnpacked(int32_t id,
+ LogSeverity const& severity,
+ LogOptions const& options,
+ const S& message,
+ const DynamicAttributes& dynamicAttrs) {
+ fmt::string_view msg{message};
+ doLogImpl(id, severity, options, StringData(msg.data(), msg.size()), dynamicAttrs);
+}
+
+// Args may be raw attributes or CombinedAttr's here. We need to flatten any combined attributes
+// into just raw attributes for doLogUnpacked. We do this building flat tuples for every argument,
+// concatenating them into a single tuple that we can expand again using apply.
+template <typename S, typename... Args>
void doLog(int32_t id,
LogSeverity const& severity,
LogOptions const& options,
- S const& fmtmsg,
- const char (&msg)[N],
- const fmt::internal::named_arg<Args, char>&... args) {
- doLog(id, severity, options, msg, args...);
+ const S& formatMsg,
+ const Args&... args) {
+ std::apply([id, &severity, &options, &formatMsg](
+ auto&&... args) { doLogUnpacked(id, severity, options, formatMsg, args...); },
+ std::tuple_cat(toFlatAttributesTupleRef(args)...));
}
-template <typename S>
+template <typename S, size_t N, typename... Args>
void doLog(int32_t id,
LogSeverity const& severity,
LogOptions const& options,
- S const& message,
- const DynamicAttributes& dynamicAttrs) {
- fmt::string_view msg{message};
- doLogImpl(id, severity, options, StringData(msg.data(), msg.size()), dynamicAttrs);
+ const S& formatMsg,
+ const char (&msg)[N],
+ const Args&... args) {
+ std::apply(
+ [id, &severity, &options, &formatMsg, &msg](auto&&... unpackedArgs) {
+ doLogUnpacked(id, severity, options, formatMsg, msg, unpackedArgs...);
+ },
+ std::tuple_cat(toFlatAttributesTupleRef(args)...));
}
} // namespace logv2::detail
-inline namespace literals {
-inline fmt::internal::udl_arg<char> operator"" _attr(const char* s, std::size_t) {
- return {s};
-}
-} // namespace literals
-
} // namespace mongo
diff --git a/src/mongo/logv2/logv2_test.cpp b/src/mongo/logv2/logv2_test.cpp
index 9028605deb9..e5c702a1f7c 100644
--- a/src/mongo/logv2/logv2_test.cpp
+++ b/src/mongo/logv2/logv2_test.cpp
@@ -956,6 +956,132 @@ TEST_F(LogV2JsonBsonTest, DynamicAttributes) {
});
}
+
+struct A {
+ std::string toString() const {
+ return "A";
+ }
+
+ friend auto logAttrs(const A& a) {
+ return "a"_attr = a;
+ }
+};
+
+TEST_F(LogV2JsonBsonTest, AttrWrapperOne) {
+ A a;
+ LOGV2(4759400, "{}", logAttrs(a));
+ validate([&a](const BSONObj& obj) {
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("a").String(),
+ a.toString());
+ });
+}
+
+struct B {
+ std::string toString() const {
+ return "B";
+ }
+ friend auto logAttrs(const B& b) {
+ return "b"_attr = b;
+ }
+};
+
+TEST_F(LogV2JsonBsonTest, AttrWrapperTwo) {
+ A a;
+ B b;
+ LOGV2(4759401, "{}", logAttrs(a), logAttrs(b));
+ validate([&a, &b](const BSONObj& obj) {
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("a").String(),
+ a.toString());
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("b").String(),
+ b.toString());
+ });
+}
+
+struct C {
+ std::string toString() const {
+ return "C";
+ }
+ friend auto logAttrs(const C& c) {
+ return "c"_attr = c;
+ }
+};
+
+TEST_F(LogV2JsonBsonTest, AttrWrapperRvalue) {
+ A a;
+ B b;
+ LOGV2(4759402, "{}", logAttrs(a), logAttrs(b), logAttrs(C()));
+ validate([&a, &b](const BSONObj& obj) {
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("a").String(),
+ a.toString());
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("b").String(),
+ b.toString());
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("c").String(),
+ C().toString());
+ });
+}
+
+struct D {
+ std::string toString() const {
+ return "D";
+ }
+
+ A a() const {
+ return A();
+ }
+ const B& b() const {
+ return _b;
+ }
+
+ friend auto logAttrs(const D& d) {
+ return multipleAttrs("d"_attr = d, d.a(), d.b());
+ }
+
+ B _b;
+};
+
+TEST_F(LogV2JsonBsonTest, AttrWrapperComplex) {
+ D d;
+ LOGV2(4759403, "{}", logAttrs(d));
+ validate([&d](const BSONObj& obj) {
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("a").String(),
+ d.a().toString());
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("b").String(),
+ d.b().toString());
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("d").String(),
+ d.toString());
+ });
+}
+
+struct E {
+ D d() const {
+ return D();
+ }
+ const C& c() const {
+ return _c;
+ }
+
+ friend auto logAttrs(const E& e) {
+ return multipleAttrs(e.d(), e.c());
+ }
+
+ C _c;
+};
+
+TEST_F(LogV2JsonBsonTest, AttrWrapperComplexHierarchy) {
+ E e;
+ LOGV2(4759404, "{}", logAttrs(e));
+ validate([&e](const BSONObj& obj) {
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("a").String(),
+ e.d().a().toString());
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("b").String(),
+ e.d().b().toString());
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("c").String(),
+ e.c().toString());
+ ASSERT_EQUALS(obj.getField(kAttributesFieldName).Obj().getField("d").String(),
+ e.d().toString());
+ });
+}
+
class LogV2ContainerTest : public LogV2TypesTest {
public:
using LogV2TypesTest::LogV2TypesTest;