diff options
author | Phoebe Liang <phoebeliang@google.com> | 2022-11-02 15:27:54 -0700 |
---|---|---|
committer | Copybara-Service <copybara-worker@google.com> | 2022-11-02 15:28:46 -0700 |
commit | e6044634dd7caec2d79a13aecc9e765023768757 (patch) | |
tree | 3ffe2c00f4b5e49da4f4e7d9e50c64999f318408 | |
parent | 1649c037c556bdaca7241bc0113275506bdb9638 (diff) |
Support logging of user-defined types that implement `AbslStringify()`
If a user-defined type has `AbslStringify()` defined, it will always be used for logging over `operator<<`.
`HasAbslStringify` now uses the empty class `UnimplementedSink` for its checks instead of `StringifySink` in order to make it work in cases involving other sinks.
PiperOrigin-RevId: 485710377
Change-Id: Ibdd916151c7abc3269c35fbe79b772867f3d25e1
-rw-r--r-- | CMake/AbseilDll.cmake | 1 | ||||
-rw-r--r-- | absl/log/BUILD.bazel | 1 | ||||
-rw-r--r-- | absl/log/CMakeLists.txt | 1 | ||||
-rw-r--r-- | absl/log/internal/log_message.h | 48 | ||||
-rw-r--r-- | absl/log/log.h | 37 | ||||
-rw-r--r-- | absl/log/log_format_test.cc | 131 | ||||
-rw-r--r-- | absl/strings/BUILD.bazel | 1 | ||||
-rw-r--r-- | absl/strings/CMakeLists.txt | 1 | ||||
-rw-r--r-- | absl/strings/internal/has_absl_stringify.h | 55 | ||||
-rw-r--r-- | absl/strings/internal/stringify_sink.h | 9 | ||||
-rw-r--r-- | absl/strings/str_cat.h | 1 |
11 files changed, 273 insertions, 13 deletions
diff --git a/CMake/AbseilDll.cmake b/CMake/AbseilDll.cmake index b81b0cea..831ec5fb 100644 --- a/CMake/AbseilDll.cmake +++ b/CMake/AbseilDll.cmake @@ -244,6 +244,7 @@ set(ABSL_INTERNAL_DLL_FILES "strings/internal/string_constant.h" "strings/internal/stringify_sink.h" "strings/internal/stringify_sink.cc" + "strings/internal/has_absl_stringify.h" "strings/match.cc" "strings/match.h" "strings/numbers.cc" diff --git a/absl/log/BUILD.bazel b/absl/log/BUILD.bazel index dadc8856..16788ae2 100644 --- a/absl/log/BUILD.bazel +++ b/absl/log/BUILD.bazel @@ -324,6 +324,7 @@ cc_test( "//absl/log/internal:config", "//absl/log/internal:test_matchers", "//absl/strings", + "//absl/strings:str_format", "@com_google_googletest//:gtest_main", ], ) diff --git a/absl/log/CMakeLists.txt b/absl/log/CMakeLists.txt index 09e4ca0c..28d4b519 100644 --- a/absl/log/CMakeLists.txt +++ b/absl/log/CMakeLists.txt @@ -681,6 +681,7 @@ absl_cc_test( absl::log_internal_config absl::log_internal_test_matchers absl::scoped_mock_log + absl::str_format absl::strings GTest::gmock GTest::gtest_main diff --git a/absl/log/internal/log_message.h b/absl/log/internal/log_message.h index 37a267c0..992bb630 100644 --- a/absl/log/internal/log_message.h +++ b/absl/log/internal/log_message.h @@ -41,6 +41,7 @@ #include "absl/log/internal/nullguard.h" #include "absl/log/log_entry.h" #include "absl/log/log_sink.h" +#include "absl/strings/internal/has_absl_stringify.h" #include "absl/strings/string_view.h" #include "absl/time/time.h" @@ -153,8 +154,17 @@ class LogMessage { template <int SIZE> LogMessage& operator<<(char (&buf)[SIZE]) ABSL_ATTRIBUTE_NOINLINE; - // Default: uses `ostream` logging to convert `v` to a string. - template <typename T> + // Types that support `AbslStringify()` are serialized that way. + template <typename T, + typename std::enable_if< + strings_internal::HasAbslStringify<T>::value, int>::type = 0> + LogMessage& operator<<(const T& v) ABSL_ATTRIBUTE_NOINLINE; + + // Types that don't support `AbslStringify()` but do support streaming into a + // `std::ostream&` are serialized that way. + template <typename T, + typename std::enable_if< + !strings_internal::HasAbslStringify<T>::value, int>::type = 0> LogMessage& operator<<(const T& v) ABSL_ATTRIBUTE_NOINLINE; // Note: We explicitly do not support `operator<<` for non-const references @@ -205,12 +215,44 @@ class LogMessage { std::ostream stream_; }; +// Helper class so that `AbslStringify()` can modify the LogMessage. +class StringifySink final { + public: + explicit StringifySink(LogMessage& message) : message_(message) {} + + void Append(size_t count, char ch) { message_ << std::string(count, ch); } + + void Append(absl::string_view v) { message_ << v; } + + // For types that implement `AbslStringify` using `absl::Format()`. + friend void AbslFormatFlush(StringifySink* sink, absl::string_view v) { + sink->Append(v); + } + + private: + LogMessage& message_; +}; + +// Note: the following is declared `ABSL_ATTRIBUTE_NOINLINE` +template <typename T, + typename std::enable_if<strings_internal::HasAbslStringify<T>::value, + int>::type> +LogMessage& LogMessage::operator<<(const T& v) { + StringifySink sink(*this); + // Replace with public API. + AbslStringify(sink, v); + return *this; +} + // Note: the following is declared `ABSL_ATTRIBUTE_NOINLINE` -template <typename T> +template <typename T, + typename std::enable_if<!strings_internal::HasAbslStringify<T>::value, + int>::type> LogMessage& LogMessage::operator<<(const T& v) { stream_ << log_internal::NullGuard<T>().Guard(v); return *this; } + inline LogMessage& LogMessage::operator<<( std::ostream& (*m)(std::ostream& os)) { stream_ << m; diff --git a/absl/log/log.h b/absl/log/log.h index 5331fdbf..4cd52041 100644 --- a/absl/log/log.h +++ b/absl/log/log.h @@ -132,10 +132,45 @@ // as they would be if streamed into a `std::ostream`, however it should be // noted that their actual type is unspecified. // -// To implement a custom formatting operator for a type you own, define +// To implement a custom formatting operator for a type you own, there are two +// options: `AbslStringify()` or `std::ostream& operator<<(std::ostream&, ...)`. +// It is recommended that users make their types loggable through +// `AbslStringify()` as it is a universal stringification extension that also +// enables `absl::StrFormat` and `absl::StrCat` support. If both +// `AbslStringify()` and `std::ostream& operator<<(std::ostream&, ...)` are +// defined, `AbslStringify()` will be used. +// +// To use the `AbslStringify()` API, define a friend function template in your +// type's namespace with the following signature: +// +// template <typename Sink> +// void AbslStringify(Sink& sink, const UserDefinedType& value); +// +// `Sink` has the same interface as `absl::FormatSink`, but without +// `PutPaddedString()`. +// +// Example: +// +// struct Point { +// template <typename Sink> +// friend void AbslStringify(Sink& sink, const Point& p) { +// absl::Format(&sink, "(%v, %v)", p.x, p.y); +// } +// +// int x; +// int y; +// }; +// +// To use `std::ostream& operator<<(std::ostream&, ...)`, define // `std::ostream& operator<<(std::ostream&, ...)` in your type's namespace (for // ADL) just as you would to stream it to `std::cout`. // +// Currently `AbslStringify()` ignores output manipulators but this is not +// guaranteed behavior and may be subject to change in the future. If you would +// like guaranteed behavior regarding output manipulators, please use +// `std::ostream& operator<<(std::ostream&, ...)` to make custom types loggable +// instead. +// // Those macros that support streaming honor output manipulators and `fmtflag` // changes that output data (e.g. `std::ends`) or control formatting of data // (e.g. `std::hex` and `std::fixed`), however flushing such a stream is diff --git a/absl/log/log_format_test.cc b/absl/log/log_format_test.cc index c629fce7..397c8d0c 100644 --- a/absl/log/log_format_test.cc +++ b/absl/log/log_format_test.cc @@ -32,6 +32,7 @@ #include "absl/log/scoped_mock_log.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" #include "absl/strings/string_view.h" namespace { @@ -865,6 +866,111 @@ TEST(LogFormatTest, CustomNonCopyable) { LOG(INFO) << value; } +struct Point { + template <typename Sink> + friend void AbslStringify(Sink& sink, const Point& p) { + absl::Format(&sink, "(%d, %d)", p.x, p.y); + } + + int x = 10; + int y = 20; +}; + +TEST(LogFormatTest, AbslStringifyExample) { + absl::ScopedMockLog test_sink(absl::MockLogDefault::kDisallowUnexpected); + + Point p; + + EXPECT_CALL( + test_sink, + Send(AllOf( + TextMessage(Eq("(10, 20)")), TextMessage(Eq(absl::StrCat(p))), + ENCODED_MESSAGE(EqualsProto(R"pb(value { str: "(10, 20)" })pb"))))); + + test_sink.StartCapturingLogs(); + LOG(INFO) << p; +} + +struct PointWithAbslStringifiyAndOstream { + template <typename Sink> + friend void AbslStringify(Sink& sink, + const PointWithAbslStringifiyAndOstream& p) { + absl::Format(&sink, "(%d, %d)", p.x, p.y); + } + + int x = 10; + int y = 20; +}; + +ABSL_ATTRIBUTE_UNUSED std::ostream& operator<<( + std::ostream& os, const PointWithAbslStringifiyAndOstream&) { + return os << "Default to AbslStringify()"; +} + +TEST(LogFormatTest, CustomWithAbslStringifyAndOstream) { + absl::ScopedMockLog test_sink(absl::MockLogDefault::kDisallowUnexpected); + + PointWithAbslStringifiyAndOstream p; + + EXPECT_CALL( + test_sink, + Send(AllOf( + TextMessage(Eq("(10, 20)")), TextMessage(Eq(absl::StrCat(p))), + ENCODED_MESSAGE(EqualsProto(R"pb(value { str: "(10, 20)" })pb"))))); + + test_sink.StartCapturingLogs(); + LOG(INFO) << p; +} + +struct PointStreamsNothing { + template <typename Sink> + friend void AbslStringify(Sink&, const PointStreamsNothing&) {} + + int x = 10; + int y = 20; +}; + +TEST(LogFormatTest, AbslStringifyStreamsNothing) { + absl::ScopedMockLog test_sink(absl::MockLogDefault::kDisallowUnexpected); + + PointStreamsNothing p; + + EXPECT_CALL( + test_sink, + Send(AllOf(TextMessage(Eq("77")), TextMessage(Eq(absl::StrCat(p, 77))), + ENCODED_MESSAGE(EqualsProto(R"pb(value { str: "77" })pb"))))); + + test_sink.StartCapturingLogs(); + LOG(INFO) << p << 77; +} + +struct PointMultipleAppend { + template <typename Sink> + friend void AbslStringify(Sink& sink, const PointMultipleAppend& p) { + sink.Append("("); + sink.Append(absl::StrCat(p.x, ", ", p.y, ")")); + } + + int x = 10; + int y = 20; +}; + +TEST(LogFormatTest, AbslStringifyMultipleAppend) { + absl::ScopedMockLog test_sink(absl::MockLogDefault::kDisallowUnexpected); + + PointMultipleAppend p; + + EXPECT_CALL( + test_sink, + Send(AllOf( + TextMessage(Eq("(10, 20)")), TextMessage(Eq(absl::StrCat(p))), + ENCODED_MESSAGE(EqualsProto(R"pb(value { str: "(" } + value { str: "10, 20)" })pb"))))); + + test_sink.StartCapturingLogs(); + LOG(INFO) << p; +} + TEST(ManipulatorLogFormatTest, BoolAlphaTrue) { absl::ScopedMockLog test_sink(absl::MockLogDefault::kDisallowUnexpected); @@ -1501,6 +1607,31 @@ TEST(ManipulatorLogFormatTest, CustomClassStreamsNothing) { LOG(INFO) << value << 77; } +struct PointPercentV { + template <typename Sink> + friend void AbslStringify(Sink& sink, const PointPercentV& p) { + absl::Format(&sink, "(%v, %v)", p.x, p.y); + } + + int x = 10; + int y = 20; +}; + +TEST(ManipulatorLogFormatTest, IOManipsDoNotAffectAbslStringify) { + absl::ScopedMockLog test_sink(absl::MockLogDefault::kDisallowUnexpected); + + PointPercentV p; + + EXPECT_CALL( + test_sink, + Send(AllOf( + TextMessage(Eq("(10, 20)")), TextMessage(Eq(absl::StrCat(p))), + ENCODED_MESSAGE(EqualsProto(R"pb(value { str: "(10, 20)" })pb"))))); + + test_sink.StartCapturingLogs(); + LOG(INFO) << std::hex << p; +} + // Tests that verify the behavior when more data are streamed into a `LOG` // statement than fit in the buffer. // Structured logging scenario is tested in other unit tests since the output is diff --git a/absl/strings/BUILD.bazel b/absl/strings/BUILD.bazel index 4794f4ca..12a8d155 100644 --- a/absl/strings/BUILD.bazel +++ b/absl/strings/BUILD.bazel @@ -58,6 +58,7 @@ cc_library( "charconv.h", "escaping.h", "internal/damerau_levenshtein_distance.h", + "internal/has_absl_stringify.h", "internal/string_constant.h", "match.h", "numbers.h", diff --git a/absl/strings/CMakeLists.txt b/absl/strings/CMakeLists.txt index 87b1a78f..7e91ebf2 100644 --- a/absl/strings/CMakeLists.txt +++ b/absl/strings/CMakeLists.txt @@ -23,6 +23,7 @@ absl_cc_library( "escaping.h" "internal/damerau_levenshtein_distance.h" "internal/string_constant.h" + "internal/has_absl_stringify.h" "match.h" "numbers.h" "str_cat.h" diff --git a/absl/strings/internal/has_absl_stringify.h b/absl/strings/internal/has_absl_stringify.h new file mode 100644 index 00000000..55a08508 --- /dev/null +++ b/absl/strings/internal/has_absl_stringify.h @@ -0,0 +1,55 @@ +// Copyright 2022 The Abseil Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef ABSL_STRINGS_INTERNAL_HAS_ABSL_STRINGIFY_H_ +#define ABSL_STRINGS_INTERNAL_HAS_ABSL_STRINGIFY_H_ +#include <string> +#include <type_traits> +#include <utility> + +#include "absl/strings/string_view.h" + +namespace absl { +ABSL_NAMESPACE_BEGIN + +namespace strings_internal { + +// This is an empty class not intended to be used. It exists so that +// `HasAbslStringify` can reference a universal class rather than needing to be +// copied for each new sink. +class UnimplementedSink { + public: + void Append(size_t count, char ch); + + void Append(string_view v); + + // Support `absl::Format(&sink, format, args...)`. + friend void AbslFormatFlush(UnimplementedSink* sink, absl::string_view v); +}; + +template <typename T, typename = void> +struct HasAbslStringify : std::false_type {}; + +template <typename T> +struct HasAbslStringify< + T, std::enable_if_t<std::is_void<decltype(AbslStringify( + std::declval<strings_internal::UnimplementedSink&>(), + std::declval<const T&>()))>::value>> : std::true_type {}; + +} // namespace strings_internal + +ABSL_NAMESPACE_END +} // namespace absl + +#endif // ABSL_STRINGS_INTERNAL_HAS_ABSL_STRINGIFY_H_ diff --git a/absl/strings/internal/stringify_sink.h b/absl/strings/internal/stringify_sink.h index 5e326a0b..fc3747bb 100644 --- a/absl/strings/internal/stringify_sink.h +++ b/absl/strings/internal/stringify_sink.h @@ -49,15 +49,6 @@ string_view ExtractStringification(StringifySink& sink, const T& v) { return sink.buffer_; } -template <typename T, typename = void> -struct HasAbslStringify : std::false_type {}; - -template <typename T> -struct HasAbslStringify<T, std::enable_if_t<std::is_void<decltype(AbslStringify( - std::declval<strings_internal::StringifySink&>(), - std::declval<const T&>()))>::value>> - : std::true_type {}; - } // namespace strings_internal ABSL_NAMESPACE_END diff --git a/absl/strings/str_cat.h b/absl/strings/str_cat.h index 1a37faae..00af84ea 100644 --- a/absl/strings/str_cat.h +++ b/absl/strings/str_cat.h @@ -95,6 +95,7 @@ #include <vector> #include "absl/base/port.h" +#include "absl/strings/internal/has_absl_stringify.h" #include "absl/strings/internal/stringify_sink.h" #include "absl/strings/numbers.h" #include "absl/strings/string_view.h" |