diff options
Diffstat (limited to 'library/cpp')
| -rw-r--r-- | library/cpp/yt/logging/logger-inl.h | 62 | ||||
| -rw-r--r-- | library/cpp/yt/logging/logger.h | 14 | ||||
| -rw-r--r-- | library/cpp/yt/logging/tag-inl.h | 114 | ||||
| -rw-r--r-- | library/cpp/yt/logging/tag.h | 60 | ||||
| -rw-r--r-- | library/cpp/yt/logging/tagged_payload-inl.h | 30 | ||||
| -rw-r--r-- | library/cpp/yt/logging/tagged_payload.h | 10 | ||||
| -rw-r--r-- | library/cpp/yt/logging/unittests/logger_ut.cpp | 125 |
7 files changed, 382 insertions, 33 deletions
diff --git a/library/cpp/yt/logging/logger-inl.h b/library/cpp/yt/logging/logger-inl.h index ff4d6d81bbe..d6c4e4c6775 100644 --- a/library/cpp/yt/logging/logger-inl.h +++ b/library/cpp/yt/logging/logger-inl.h @@ -5,6 +5,7 @@ #endif #include "tagged_payload.h" +#include "tag.h" #include <library/cpp/yt/yson_string/convert.h> #include <library/cpp/yt/yson_string/string.h> @@ -343,35 +344,6 @@ struct TStaticAnchorRef std::atomic<bool>* Registered; }; -//! Wraps the format spec passed to |TTaggedLoggingGuard::With| and validates at compile -//! time that it is a |%|-prefixed string literal (e.g. |"%v"|, |"%08x"|). The stored -//! spec has the leading |%| stripped, as expected by |FormatValue|. -class TLoggingTagSpec -{ -public: - template <size_t N> - consteval TLoggingTagSpec(const char (&spec)[N]) - : Spec_(spec + 1, N - 2) - { - static_assert(N >= 2, "Logging tag format spec must be a non-empty string literal"); - if (spec[0] != '%') { - TheLoggingTagFormatSpecMustStartWithPercentSign(); - } - } - - TStringBuf Get() const - { - return Spec_; - } - -private: - const TStringBuf Spec_; - - // Undefined on purpose: calling it from the |consteval| ctor turns a missing - // leading |%| into a compile error that names the violated rule. - static void TheLoggingTagFormatSpecMustStartWithPercentSign(); -}; - class TWellKnownTaggedLoggingGuard; //! Accumulates a tagged log message via a fluent |.With| chain and emits the event in @@ -408,6 +380,15 @@ public: return Enabled_; } + //! The fluent macros end their expansion in a call to this instead of naming the guard + //! directly. A tag-less |YT_TLOG_INFO("Message");| would otherwise expand to a discarded + //! id-expression, and -Wunused-value fires on those whenever the call is spelled inside + //! a macro argument (e.g. within |BIND(...)|) rather than a macro body. + TTaggedLoggingGuard& Self() & + { + return *this; + } + template <class TValue> TTaggedLoggingGuard& With(TStringBuf tag, const TValue& value) & { @@ -420,6 +401,23 @@ public: return DoWith(tag, value, spec.Get()); } + //! Attaches a keyed tag composed from several values, e.g. |.WithFormat("Method", "%v.%v", service, method)|. + template <class... TArgs> + TTaggedLoggingGuard& WithFormat(TStringBuf tag, TFormatString<TArgs...> format, TArgs&&... args) & + { + Format(Writer_.BeginTag(tag), format, std::forward<TArgs>(args)...); + Writer_.EndTag(); + return *this; + } + + //! Splices a pre-built list of keyed tags, preserving them as individual tags. Chosen + //! over the well-known single-argument |With| below by exact match. + TTaggedLoggingGuard& With(const TLoggingTagList& tags) & + { + Writer_.AppendTags(tags.GetPayload()); + return *this; + } + //! Attaches a well-known tag whose key is resolved from #value's type via the //! |GetWellKnownLoggingTag| ADL point (the type must opt in, e.g. errors). //! @@ -618,6 +616,12 @@ public: { return *this; } + + template <class... TArgs> + TNullTaggedLoggingGuard& WithFormat(TArgs&&...) + { + return *this; + } }; template <class TMessage> diff --git a/library/cpp/yt/logging/logger.h b/library/cpp/yt/logging/logger.h index 2e832995016..68529760453 100644 --- a/library/cpp/yt/logging/logger.h +++ b/library/cpp/yt/logging/logger.h @@ -468,13 +468,19 @@ void LogStructuredEvent( // Tags are supplied via a fluent |.With(name, value)| (or |.With(name, value, "%spec")|) // chain; they are carried as structured key/value pairs in the event payload. A single- // argument |.With(value)| attaches the value under a statically known key resolved by ADL -// (e.g. |.With(error)| under the "Error" key): +// (e.g. |.With(error)| under the "Error" key). |.WithFormat(name, format, args...)| composes +// one tag out of several values: // // YT_TLOG_INFO("Message") // .With("Key", value) // .With("Count", count, "%08x") +// .WithFormat("Method", "%v.%v", service, method) // .With(error); // +// |.With(tagList)| splices a #TLoggingTagList -- a set of keyed tags a component builds +// once (with the same fluent API) and attaches to many events, without collapsing them +// into a single tag. +// // If the message is not logged then the |.With| chain is not evaluated, so tag value // expressions cost nothing. @@ -498,7 +504,7 @@ void LogStructuredEvent( (message)); \ !loggingGuard__.IsEnabled()) \ { } else \ - loggingGuard__ + loggingGuard__.Self() #ifdef YT_ENABLE_TRACE_LOGGING #define YT_TLOG_TRACE(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Trace, message) @@ -545,7 +551,7 @@ void LogStructuredEvent( (message)); \ loggingGuard__.TryEnter(); \ loggingGuard__.Commit()) \ - loggingGuard__ + loggingGuard__.Self() #define YT_TLOG_FATAL_IF(condition, message) if (condition) [[unlikely]] YT_TLOG_FATAL(message) #define YT_TLOG_FATAL_UNLESS(condition, message) if (!(condition)) [[unlikely]] YT_TLOG_FATAL(message) @@ -564,7 +570,7 @@ void LogStructuredEvent( ::NYT::EErrorCode::Fatal, \ "Malformed request or incorrect state detected") \ << ::NYT::TErrorAttribute("message", loggingGuard__.Commit())) \ - loggingGuard__ + loggingGuard__.Self() #define YT_TLOG_ALERT_AND_THROW_IF(condition, message) if (condition) [[unlikely]] YT_TLOG_ALERT_AND_THROW(message) #define YT_TLOG_ALERT_AND_THROW_UNLESS(condition, message) if (!(condition)) [[unlikely]] YT_TLOG_ALERT_AND_THROW(message) diff --git a/library/cpp/yt/logging/tag-inl.h b/library/cpp/yt/logging/tag-inl.h new file mode 100644 index 00000000000..72a352ed7b8 --- /dev/null +++ b/library/cpp/yt/logging/tag-inl.h @@ -0,0 +1,114 @@ +#ifndef TAG_INL_H_ +#error "Direct inclusion of this file is not allowed, include tag.h" +// For the sake of sane code completion. +#include "tag.h" +#endif + +#include <library/cpp/yt/string/string_builder.h> + +#include <utility> + +namespace NYT::NLogging { + +//////////////////////////////////////////////////////////////////////////////// + +class TLoggingTagSpec +{ +public: + template <size_t N> + consteval TLoggingTagSpec(const char (&spec)[N]) + : Spec_(spec + 1, N - 2) + { + static_assert(N >= 2, "Logging tag format spec must be a non-empty string literal"); + if (spec[0] != '%') { + TheLoggingTagFormatSpecMustStartWithPercentSign(); + } + } + + TStringBuf Get() const + { + return Spec_; + } + +private: + const TStringBuf Spec_; + + // Undefined on purpose: calling it from the |consteval| ctor turns a missing + // leading |%| into a compile error that names the violated rule. + static void TheLoggingTagFormatSpecMustStartWithPercentSign(); +}; + +//////////////////////////////////////////////////////////////////////////////// + +template <class TValue> +TLoggingTagList& TLoggingTagList::With(TStringBuf key, const TValue& value) & +{ + DoWith(key, value, "v"_sb); + return *this; +} + +template <class TValue> +TLoggingTagList& TLoggingTagList::With(TStringBuf key, const TValue& value, TLoggingTagSpec spec) & +{ + DoWith(key, value, spec.Get()); + return *this; +} + +template <class... TArgs> +TLoggingTagList& TLoggingTagList::WithFormat(TStringBuf key, TFormatString<TArgs...> format, TArgs&&... args) & +{ + TStringBuilder builder; + Format(&builder, format, std::forward<TArgs>(args)...); + AppendTag(key, builder.GetBuffer()); + return *this; +} + +template <class TValue> +TLoggingTagList&& TLoggingTagList::With(TStringBuf key, const TValue& value) && +{ + DoWith(key, value, "v"_sb); + return std::move(*this); +} + +template <class TValue> +TLoggingTagList&& TLoggingTagList::With(TStringBuf key, const TValue& value, TLoggingTagSpec spec) && +{ + DoWith(key, value, spec.Get()); + return std::move(*this); +} + +template <class... TArgs> +TLoggingTagList&& TLoggingTagList::WithFormat(TStringBuf key, TFormatString<TArgs...> format, TArgs&&... args) && +{ + TStringBuilder builder; + Format(&builder, format, std::forward<TArgs>(args)...); + AppendTag(key, builder.GetBuffer()); + return std::move(*this); +} + +inline bool TLoggingTagList::IsEmpty() const +{ + return Payload_.empty(); +} + +inline TStringBuf TLoggingTagList::GetPayload() const +{ + return Payload_; +} + +template <class TValue> +void TLoggingTagList::DoWith(TStringBuf key, const TValue& value, TStringBuf spec) +{ + TStringBuilder builder; + FormatValue(&builder, value, spec); + AppendTag(key, builder.GetBuffer()); +} + +inline void TLoggingTagList::AppendTag(TStringBuf key, TStringBuf value) +{ + TTaggedPayloadWriter::AppendTag(&Payload_, key, value); +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/tag.h b/library/cpp/yt/logging/tag.h new file mode 100644 index 00000000000..9c49e9b17d0 --- /dev/null +++ b/library/cpp/yt/logging/tag.h @@ -0,0 +1,60 @@ +#pragma once + +#include "tagged_payload.h" + +#include <library/cpp/yt/string/format.h> + +#include <util/generic/strbuf.h> + +#include <string> + +namespace NYT::NLogging { + +//////////////////////////////////////////////////////////////////////////////// + +//! Wraps the format spec passed to a tag-appending |With| and validates at compile time +//! that it is a |%|-prefixed string literal (e.g. |"%v"|, |"%08x"|). +class TLoggingTagSpec; + +//////////////////////////////////////////////////////////////////////////////// + +//! An opaque, pre-serialized list of logging tags. +class TLoggingTagList +{ +public: + TLoggingTagList() = default; + + template <class TValue> + TLoggingTagList& With(TStringBuf key, const TValue& value) &; + template <class TValue> + TLoggingTagList& With(TStringBuf key, const TValue& value, TLoggingTagSpec spec) &; + template <class... TArgs> + TLoggingTagList& WithFormat(TStringBuf key, TFormatString<TArgs...> format, TArgs&&... args) &; + + template <class TValue> + TLoggingTagList&& With(TStringBuf key, const TValue& value) &&; + template <class TValue> + TLoggingTagList&& With(TStringBuf key, const TValue& value, TLoggingTagSpec spec) &&; + template <class... TArgs> + TLoggingTagList&& WithFormat(TStringBuf key, TFormatString<TArgs...> format, TArgs&&... args) &&; + + bool IsEmpty() const; + + //! The serialized tag section, spliced verbatim by #TTaggedPayloadWriter::AppendTags. + TStringBuf GetPayload() const; + +private: + std::string Payload_; + + template <class TValue> + void DoWith(TStringBuf key, const TValue& value, TStringBuf spec); + void AppendTag(TStringBuf key, TStringBuf value); +}; + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NLogging + +#define TAG_INL_H_ +#include "tag-inl.h" +#undef TAG_INL_H_ diff --git a/library/cpp/yt/logging/tagged_payload-inl.h b/library/cpp/yt/logging/tagged_payload-inl.h index 4b71a5bebef..5cb7f1ded8c 100644 --- a/library/cpp/yt/logging/tagged_payload-inl.h +++ b/library/cpp/yt/logging/tagged_payload-inl.h @@ -85,6 +85,36 @@ inline TTaggedPayloadWriter& TTaggedPayloadWriter::EndTag() & return *this; } +inline TTaggedPayloadWriter& TTaggedPayloadWriter::AppendTags(TStringBuf tags) & +{ + YT_ASSERT(MessageEnded_ && !InTag_); + Builder_.AppendString(tags); + return *this; +} + +inline void TTaggedPayloadWriter::AppendTag(std::string* payload, TStringBuf key, TStringBuf value) +{ + // High bit reserved for the well-known flag. + YT_ASSERT(key.size() < WellKnownTagFlag); + + auto keySize = static_cast<ui32>(key.size()); + auto valueSize = static_cast<ui32>(value.size()); + + // Every appended byte is overwritten below, so skip zero-filling them. + auto offset = payload->size(); + ResizeUninitialized(*payload, offset + 2 * sizeof(ui32) + key.size() + value.size()); + + char* ptr = payload->data() + offset; + auto write = [&] (const void* data, size_t size) { + ::memcpy(ptr, data, size); + ptr += size; + }; + write(&keySize, sizeof(keySize)); + write(key.data(), key.size()); + write(&valueSize, sizeof(valueSize)); + write(value.data(), value.size()); +} + inline TTaggedLogEventPayload TTaggedPayloadWriter::Finish() & { YT_ASSERT(MessageEnded_ && !InTag_); diff --git a/library/cpp/yt/logging/tagged_payload.h b/library/cpp/yt/logging/tagged_payload.h index da46f30f42a..67c8dc1f854 100644 --- a/library/cpp/yt/logging/tagged_payload.h +++ b/library/cpp/yt/logging/tagged_payload.h @@ -114,6 +114,16 @@ public: //! Ends the current tag, filling in the value's reserved length prefix. TTaggedPayloadWriter& EndTag() &; + //! Splices an already-serialized tag section (see #TLoggingTagList::GetPayload) + //! verbatim. Must follow #EndMessage, must not interrupt a tag, and must precede + //! any well-known tag, which the layout requires to come last. + TTaggedPayloadWriter& AppendTags(TStringBuf tags) &; + + //! Appends a single framed keyed tag to #payload, for producers that hold an + //! already-formatted value and accumulate a tag section of their own (see + //! #TLoggingTagList). + static void AppendTag(std::string* payload, TStringBuf key, TStringBuf value); + //! Returns the serialized payload. Must follow #EndMessage. TTaggedLogEventPayload Finish() &; diff --git a/library/cpp/yt/logging/unittests/logger_ut.cpp b/library/cpp/yt/logging/unittests/logger_ut.cpp index db176ca5394..3c194a5d4b8 100644 --- a/library/cpp/yt/logging/unittests/logger_ut.cpp +++ b/library/cpp/yt/logging/unittests/logger_ut.cpp @@ -192,6 +192,131 @@ TEST(TTaggedApiTest, CustomSpec) EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("100"))); } +TEST(TTaggedApiTest, WithFormat) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + YT_TLOG_INFO("Message") + .WithFormat("Method", "%v.%v", "MyService", "MyMethod") + .With("Arg1", 123); + + auto decoded = DecodeSingleEvent(manager); + EXPECT_EQ(decoded.Message, "Message"); + ASSERT_EQ(decoded.Tags.size(), 2u); + EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Method"), std::string("MyService.MyMethod"))); + EXPECT_EQ(decoded.Tags[1], std::pair(std::string("Arg1"), std::string("123"))); +} + +TEST(TTaggedApiTest, TagList) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + + auto tags = TLoggingTagList() + .With("Address", "localhost:1234") + .With("ConnectionId", 42) + .With("Flags", 256, "%x") + .WithFormat("Method", "%v.%v", "MyService", "MyMethod"); + + YT_TLOG_INFO("Message") + .With("Before", 1) + .With(tags) + .With("After", 2); + + auto decoded = DecodeSingleEvent(manager); + EXPECT_EQ(decoded.Message, "Message"); + ASSERT_EQ(decoded.Tags.size(), 6u); + EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Before"), std::string("1"))); + EXPECT_EQ(decoded.Tags[1], std::pair(std::string("Address"), std::string("localhost:1234"))); + EXPECT_EQ(decoded.Tags[2], std::pair(std::string("ConnectionId"), std::string("42"))); + EXPECT_EQ(decoded.Tags[3], std::pair(std::string("Flags"), std::string("100"))); + EXPECT_EQ(decoded.Tags[4], std::pair(std::string("Method"), std::string("MyService.MyMethod"))); + EXPECT_EQ(decoded.Tags[5], std::pair(std::string("After"), std::string("2"))); +} + +TEST(TTaggedApiTest, EmptyTagList) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + + TLoggingTagList tags; + EXPECT_TRUE(tags.IsEmpty()); + + YT_TLOG_INFO("Message") + .With(tags); + + auto decoded = DecodeSingleEvent(manager); + EXPECT_EQ(decoded.Message, "Message"); + EXPECT_TRUE(decoded.Tags.empty()); +} + +TEST(TTaggedApiTest, TagListReusedAcrossEvents) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + + TLoggingTagList tags; + tags.With("Shared", "yes"); + EXPECT_FALSE(tags.IsEmpty()); + + YT_TLOG_INFO("First").With(tags); + YT_TLOG_INFO("Second").With(tags); + + ASSERT_EQ(manager.GetEvents().size(), 2u); + for (const auto& event : manager.GetEvents()) { + auto decoded = DecodeEvent(event); + ASSERT_EQ(decoded.Tags.size(), 1u); + EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Shared"), std::string("yes"))); + } +} + +TEST(TTaggedApiTest, TagListBeforeWellKnownTag) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + + TLoggingTagList tags; + tags.With("Shared", "yes"); + auto error = TError("boom"); + + YT_TLOG_INFO("Message") + .With(tags) + .With(error); + + ASSERT_EQ(manager.GetEvents().size(), 1u); + TTaggedPayloadReader reader(std::get<TTaggedLogEventPayload>(manager.GetEvents()[0].Payload)); + EXPECT_EQ(reader.ReadMessage(), "Message"); + + auto shared = reader.TryReadTag(); + ASSERT_TRUE(shared); + EXPECT_EQ(shared->Key, "Shared"); + EXPECT_FALSE(shared->IsWellKnown); + + auto errorTag = reader.TryReadTag(); + ASSERT_TRUE(errorTag); + EXPECT_EQ(errorTag->Key, "Error"); + EXPECT_TRUE(errorTag->IsWellKnown); + + EXPECT_FALSE(reader.TryReadTag()); +} + +TEST(TTaggedApiTest, WithFormatDisabledDoesNotEvaluateArgs) +{ + TMockLogManager manager(/*minLevel*/ ELogLevel::Warning); + TLogger Logger(&manager, "Test"); + + int evaluated = 0; + auto evaluate = [&] { + ++evaluated; + return 123; + }; + YT_TLOG_INFO("Message") + .WithFormat("Arg1", "%v-%v", evaluate(), evaluate()); + + EXPECT_EQ(evaluated, 0); + EXPECT_TRUE(manager.GetEvents().empty()); +} + TEST(TTaggedApiTest, LoggerTagFoldedIntoMessage) { TMockLogManager manager; |
