diff options
author | Henrik Edin <henrik.edin@mongodb.com> | 2020-04-16 16:45:55 -0400 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-05-05 14:08:38 +0000 |
commit | c65076829b29f977d6412f5309a3b8cf56ce9657 (patch) | |
tree | 388116bdbd0319e2ae1dea178c9f2b169924f17a /src/mongo/logv2 | |
parent | 20df781cb8b801634389efa9b4c5568bc6b21d57 (diff) | |
download | mongo-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.md | 84 | ||||
-rw-r--r-- | src/mongo/logv2/log_attr.h | 128 | ||||
-rw-r--r-- | src/mongo/logv2/log_detail.h | 64 | ||||
-rw-r--r-- | src/mongo/logv2/logv2_test.cpp | 126 |
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; |