diff options
Diffstat (limited to 'library/cpp')
27 files changed, 2029 insertions, 283 deletions
diff --git a/library/cpp/getopt/small/last_getopt_opts.h b/library/cpp/getopt/small/last_getopt_opts.h index fd0ef978c1f..868477ec79e 100644 --- a/library/cpp/getopt/small/last_getopt_opts.h +++ b/library/cpp/getopt/small/last_getopt_opts.h @@ -463,6 +463,24 @@ namespace NLastGetopt { } /** + * Add section with examples. + * + * @param examples text of this section + */ + void SetExamples(std::string_view examples) { + SetExamples(TString(examples)); + } + + /** + * Add section with examples. + * + * @param examples text of this section + */ + void SetExamples(const char* examples) { + SetExamples(TString(examples)); + } + + /** * Set minimal number of free args * * @param min new value diff --git a/library/cpp/yt/error/error-inl.h b/library/cpp/yt/error/error-inl.h index 1542d008f58..85751e8a3cf 100644 --- a/library/cpp/yt/error/error-inl.h +++ b/library/cpp/yt/error/error-inl.h @@ -604,4 +604,24 @@ void FormatValue(TStringBuilderBase* builder, const TErrorOr<T>& error, TStringB //////////////////////////////////////////////////////////////////////////////// +template <class F, class... As> +auto RunNoExcept(F&& functor, As&&... args) noexcept -> decltype(functor(std::forward<As>(args)...)) +{ + return functor(std::forward<As>(args)...); +} + +//////////////////////////////////////////////////////////////////////////////// + +inline TStringBuf GetWellKnownLoggingTag(const std::exception&) +{ + return "Error"_sb; +} + +inline TStringBuf GetWellKnownLoggingTag(const TError&) +{ + return "Error"_sb; +} + +//////////////////////////////////////////////////////////////////////////////// + } // namespace NYT diff --git a/library/cpp/yt/error/error.h b/library/cpp/yt/error/error.h index 13223af8d6c..1e5c2147d82 100644 --- a/library/cpp/yt/error/error.h +++ b/library/cpp/yt/error/error.h @@ -475,10 +475,15 @@ void FormatValue(TStringBuilderBase* builder, const TErrorOr<T>& error, TStringB //////////////////////////////////////////////////////////////////////////////// template <class F, class... As> -auto RunNoExcept(F&& functor, As&&... args) noexcept -> decltype(functor(std::forward<As>(args)...)) -{ - return functor(std::forward<As>(args)...); -} +auto RunNoExcept(F&& functor, As&&... args) noexcept -> decltype(functor(std::forward<As>(args)...)); + +//////////////////////////////////////////////////////////////////////////////// + +//! Registers errors as a well-known logging tag (ADL customization point for +//! library/cpp/yt/logging), so that |YT_TLOG_*(...).With(error)| attaches them under +//! the "Error" key, rendered after the message in plain text. +TStringBuf GetWellKnownLoggingTag(const std::exception&); +TStringBuf GetWellKnownLoggingTag(const TError&); //////////////////////////////////////////////////////////////////////////////// diff --git a/library/cpp/yt/logging/backends/arcadia/backend.cpp b/library/cpp/yt/logging/backends/arcadia/backend.cpp index 3c6ff9f5f58..5878ba52be3 100644 --- a/library/cpp/yt/logging/backends/arcadia/backend.cpp +++ b/library/cpp/yt/logging/backends/arcadia/backend.cpp @@ -57,7 +57,7 @@ public: // Use low-level api, because it is more convinient here. auto loggingContext = GetLoggingContext(); auto event = NDetail::CreateLogEvent(loggingContext, Logger_, logLevel); - event.MessageRef = NDetail::BuildLogMessage(loggingContext, Logger_, message).MessageRef; + event.Payload = NDetail::BuildLogMessage(loggingContext, Logger_, message).Payload; event.Family = ELogFamily::PlainText; Logger_.Write(std::move(event)); } diff --git a/library/cpp/yt/logging/benchmark/logging.cpp b/library/cpp/yt/logging/benchmark/logging.cpp new file mode 100644 index 00000000000..8745511f2bd --- /dev/null +++ b/library/cpp/yt/logging/benchmark/logging.cpp @@ -0,0 +1,167 @@ +#include <benchmark/benchmark.h> + +#include <library/cpp/yt/logging/logger.h> + +#include <atomic> + +namespace NYT::NLogging { +namespace { + +//////////////////////////////////////////////////////////////////////////////// + +// A log manager whose Enqueue is essentially free, so that the benchmark measures +// only the producer-side cost (building the event payload), not the logging thread. +class TBenchmarkLogManager + : public ILogManager +{ +public: + void RegisterStaticAnchor(TLoggingAnchor* anchor, ::TSourceLocation /*sourceLocation*/, TStringBuf /*message*/) override + { + anchor->Registered.store(true); + } + + void UpdateAnchor(TLoggingAnchor* anchor) override + { + anchor->CurrentVersion.store(ActualVersion_.load()); + } + + void Enqueue(TLogEvent&& event) override + { + // Keep the produced payload observable so the build is not optimized away; + // the event is then destroyed (freeing its chunk slice) -- same for both APIs. + benchmark::DoNotOptimize(std::get<TTaggedLogEventPayload>(event.Payload).Underlying().Begin()); + } + + const TLoggingCategory* GetCategory(TStringBuf /*categoryName*/) override + { + return &Category_; + } + + void UpdateCategory(TLoggingCategory* category) override + { + category->CurrentVersion.store(ActualVersion_.load()); + } + + bool GetAbortOnAlert() const override + { + return false; + } + +private: + std::atomic<int> ActualVersion_ = 1; + TLoggingCategory Category_ = { + .Name = "Bench", + .MinPlainTextLevel = ELogLevel::Minimum, + .CurrentVersion = 0, + .ActualVersion = &ActualVersion_, + }; +}; + +//////////////////////////////////////////////////////////////////////////////// +// No tags. + +void BM_Log_NoTags(benchmark::State& state) +{ + TBenchmarkLogManager manager; + TLogger Logger(&manager, "Bench"); + for (auto _ : state) { + YT_LOG_INFO("This is a log message of a typical length without tags"); + } +} +BENCHMARK(BM_Log_NoTags); + +void BM_TLog_NoTags(benchmark::State& state) +{ + TBenchmarkLogManager manager; + TLogger Logger(&manager, "Bench"); + for (auto _ : state) { + YT_TLOG_INFO("This is a log message of a typical length without tags"); + } +} +BENCHMARK(BM_TLog_NoTags); + +//////////////////////////////////////////////////////////////////////////////// +// One int tag. + +void BM_Log_OneTag(benchmark::State& state) +{ + TBenchmarkLogManager manager; + TLogger Logger(&manager, "Bench"); + for (auto _ : state) { + YT_LOG_INFO("This is a log message of a typical length (TagName: %v)", + 123); + } +} +BENCHMARK(BM_Log_OneTag); + +void BM_TLog_OneTag(benchmark::State& state) +{ + TBenchmarkLogManager manager; + TLogger Logger(&manager, "Bench"); + for (auto _ : state) { + YT_TLOG_INFO("This is a log message of a typical length") + .With("TagName", 123); + } +} +BENCHMARK(BM_TLog_OneTag); + +//////////////////////////////////////////////////////////////////////////////// +// Two tags (int + string). + +void BM_Log_TwoTags(benchmark::State& state) +{ + TBenchmarkLogManager manager; + TLogger Logger(&manager, "Bench"); + for (auto _ : state) { + YT_LOG_INFO("This is a log message of a typical length (TagName1: %v, TagName2: %v)", + 123, + "this-is-a-string"); + } +} +BENCHMARK(BM_Log_TwoTags); + +void BM_TLog_TwoTags(benchmark::State& state) +{ + TBenchmarkLogManager manager; + TLogger Logger(&manager, "Bench"); + for (auto _ : state) { + YT_TLOG_INFO("This is a log message of a typical length") + .With("TagName1", 123) + .With("TagName2", "this-is-a-string"); + } +} +BENCHMARK(BM_TLog_TwoTags); + +//////////////////////////////////////////////////////////////////////////////// +// Three tags (int + string + double). + +void BM_Log_ThreeTags(benchmark::State& state) +{ + TBenchmarkLogManager manager; + TLogger Logger(&manager, "Bench"); + for (auto _ : state) { + YT_LOG_INFO("This is a log message of a typical length (TagName1: %v, TagName2: %v, TagName3: %v)", + 123, + "this-is-a-string", + 3.14); + } +} +BENCHMARK(BM_Log_ThreeTags); + +void BM_TLog_ThreeTags(benchmark::State& state) +{ + TBenchmarkLogManager manager; + TLogger Logger(&manager, "Bench"); + for (auto _ : state) { + YT_TLOG_INFO("This is a log message of a typical length") + .With("TagName1", 123) + .With("TagName2", "this-is-a-string") + .With("TagName3", 3.14); + } +} +BENCHMARK(BM_TLog_ThreeTags); + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace +} // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/benchmark/ya.make b/library/cpp/yt/logging/benchmark/ya.make index 61fd0b49276..a7a23128cdf 100644 --- a/library/cpp/yt/logging/benchmark/ya.make +++ b/library/cpp/yt/logging/benchmark/ya.make @@ -4,6 +4,7 @@ INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc) SRCS( logger_tag.cpp + logging.cpp ) PEERDIR( diff --git a/library/cpp/yt/logging/logger-inl.h b/library/cpp/yt/logging/logger-inl.h index fa20fb06489..ff4d6d81bbe 100644 --- a/library/cpp/yt/logging/logger-inl.h +++ b/library/cpp/yt/logging/logger-inl.h @@ -4,6 +4,8 @@ #include "logger.h" #endif +#include "tagged_payload.h" + #include <library/cpp/yt/yson_string/convert.h> #include <library/cpp/yt/yson_string/string.h> @@ -107,33 +109,6 @@ Y_FORCE_INLINE const TLogger& TLogger::operator()() const namespace NDetail { -struct TMessageStringBuilderContext -{ - TSharedMutableRef Chunk; -}; - -struct TMessageBufferTag -{ }; - -class TMessageStringBuilder - : public TStringBuilderBase -{ -public: - TSharedRef Flush(); - - // For testing only. - static void DisablePerThreadCache(); - -protected: - void DoReset() override; - void DoReserve(size_t newLength) override; - -private: - TSharedMutableRef Buffer_; - - static constexpr size_t ChunkSize = 128_KB - 64; -}; - inline bool HasMessageTags( const TLoggingContext& loggingContext, const TLogger& logger) @@ -162,14 +137,14 @@ inline void AppendMessageTags( } if (const auto& traceLoggingTag = loggingContext.TraceLoggingTag; !traceLoggingTag.empty()) { if (printComma) { - builder->AppendString(TStringBuf(", ")); + builder->AppendString(", "_sb); } builder->AppendString(traceLoggingTag); printComma = true; } if (const auto& threadMessageTag = GetThreadMessageTag(); !threadMessageTag.empty()) { if (printComma) { - builder->AppendString(TStringBuf(", ")); + builder->AppendString(", "_sb); } builder->AppendString(threadMessageTag); printComma = true; @@ -185,10 +160,10 @@ inline void AppendLogMessage( if (HasMessageTags(loggingContext, logger)) { if (message.Size() >= 1 && message[message.Size() - 1] == ')') { builder->AppendString(TStringBuf(message.Begin(), message.Size() - 1)); - builder->AppendString(TStringBuf(", ")); + builder->AppendString(", "_sb); } else { builder->AppendString(TStringBuf(message.Begin(), message.Size())); - builder->AppendString(TStringBuf(" (")); + builder->AppendString(" ("_sb); } AppendMessageTags(builder, loggingContext, logger); builder->AppendChar(')'); @@ -208,10 +183,10 @@ void AppendLogMessageWithFormat( if (HasMessageTags(loggingContext, logger)) { if (format.size() >= 2 && format[format.size() - 1] == ')') { builder->AppendFormat(TRuntimeFormat{format.substr(0, format.size() - 1)}, std::forward<TArgs>(args)...); - builder->AppendString(TStringBuf(", ")); + builder->AppendString(", "_sb); } else { builder->AppendFormat(TRuntimeFormat{format}, std::forward<TArgs>(args)...); - builder->AppendString(TStringBuf(" (")); + builder->AppendString(" ("_sb); } AppendMessageTags(builder, loggingContext, logger); builder->AppendChar(')'); @@ -222,7 +197,7 @@ void AppendLogMessageWithFormat( struct TLogMessage { - TSharedRef MessageRef; + TTaggedLogEventPayload Payload; TStringBuf Anchor; }; @@ -233,9 +208,10 @@ TLogMessage BuildLogMessage( TFormatString<TArgs...> format, TArgs&&... args) { - TMessageStringBuilder builder; - AppendLogMessageWithFormat(&builder, loggingContext, logger, format.Get(), std::forward<TArgs>(args)...); - return {builder.Flush(), format.Get()}; + TTaggedPayloadWriter writer; + AppendLogMessageWithFormat(writer.BeginMessage(), loggingContext, logger, format.Get(), std::forward<TArgs>(args)...); + writer.EndMessage(); + return {writer.Finish(), format.Get()}; } template <CFormattable T> @@ -245,13 +221,15 @@ TLogMessage BuildLogMessage( const TLogger& logger, const T& obj) { - TMessageStringBuilder builder; - FormatValue(&builder, obj, TStringBuf("v")); + TTaggedPayloadWriter writer; + auto* builder = writer.BeginMessage(); + FormatValue(builder, obj, "v"_sb); if (HasMessageTags(loggingContext, logger)) { - builder.AppendString(TStringBuf(" (")); - AppendMessageTags(&builder, loggingContext, logger); - builder.AppendChar(')'); + builder->AppendString(" ("_sb); + AppendMessageTags(builder, loggingContext, logger); + builder->AppendChar(')'); } + writer.EndMessage(); if constexpr (std::same_as<TStringBuf, std::remove_cvref_t<T>>) { // NB(arkady-e1ppa): This is the overload where TStringBuf @@ -262,9 +240,9 @@ TLogMessage BuildLogMessage( // us having overload for TStringBuf (both have implicit ctors from // string literals) thus we have to accommodate TStringBuf specifics // in this if constexpr part. - return {builder.Flush(), obj}; + return {writer.Finish(), obj}; } else { - return {builder.Flush(), TStringBuf()}; + return {writer.Finish(), TStringBuf()}; } } @@ -295,13 +273,10 @@ inline TLogMessage BuildLogMessage( const TLogger& logger, TSharedRef&& message) { - if (HasMessageTags(loggingContext, logger)) { - TMessageStringBuilder builder; - AppendLogMessage(&builder, loggingContext, logger, message); - return {builder.Flush(), TStringBuf()}; - } else { - return {std::move(message), TStringBuf()}; - } + TTaggedPayloadWriter writer; + AppendLogMessage(writer.BeginMessage(), loggingContext, logger, message); + writer.EndMessage(); + return {writer.Finish(), TStringBuf()}; } inline TLogEvent CreateLogEvent( @@ -310,10 +285,10 @@ inline TLogEvent CreateLogEvent( ELogLevel level) { TLogEvent event; - event.Instant = loggingContext.Instant; event.Category = logger.GetCategory(); - event.Essential = logger.IsEssential(); event.Level = level; + event.Essential = logger.IsEssential(); + event.Instant = loggingContext.Instant; event.ThreadId = loggingContext.ThreadId; event.ThreadName = loggingContext.ThreadName; event.FiberId = loggingContext.FiberId; @@ -332,15 +307,24 @@ inline void LogEventImpl( ELogLevel level, ::TSourceLocation sourceLocation, TLoggingAnchor* anchor, - TSharedRef message) + TTaggedLogEventPayload payload) { - auto event = CreateLogEvent(loggingContext, logger, level); - event.MessageKind = ELogMessageKind::Unstructured; - event.MessageRef = std::move(message); - event.Family = ELogFamily::PlainText; - event.SourceFile = sourceLocation.File; - event.SourceLine = sourceLocation.Line; - event.Anchor = anchor; + auto event = TLogEvent{ + .Category = logger.GetCategory(), + .Level = level, + .Family = ELogFamily::PlainText, + .Essential = logger.IsEssential(), + .Payload = std::move(payload), + .Instant = loggingContext.Instant, + .ThreadId = loggingContext.ThreadId, + .ThreadName = loggingContext.ThreadName, + .FiberId = loggingContext.FiberId, + .TraceId = loggingContext.TraceId, + .RequestId = loggingContext.RequestId, + .SourceFile = sourceLocation.File, + .SourceLine = sourceLocation.Line, + .Anchor = anchor, + }; if (Y_UNLIKELY(event.Level >= ELogLevel::Alert)) { logger.Write(TLogEvent(event)); OnCriticalLogEvent(logger, event); @@ -349,6 +333,299 @@ inline void LogEventImpl( } } +//////////////////////////////////////////////////////////////////////////////// + +//! References the per-call-site static anchor and its one-shot registration flag. +//! Produced by the lambda embedded in the fluent |YT_TLOG_*| macros. +struct TStaticAnchorRef +{ + TLoggingAnchor* Anchor; + 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 +//! its destructor. Instantiated by the fluent |YT_TLOG_*| macros, which guarantee that +//! the chain is reached only when the level is enabled (so tag value expressions are +//! not evaluated otherwise). +//! +//! The user message -- with the logger's contextual (logger/trace/thread) tags folded +//! in -- goes to the payload message field; each |.With(key, value)| becomes a +//! structured payload tag (see #TTaggedPayloadWriter). +class TTaggedLoggingGuard +{ +public: + TTaggedLoggingGuard( + const TLogger& logger, + ELogLevel level, + ::TSourceLocation sourceLocation, + TStaticAnchorRef anchorRef, + TStringBuf message) + : TTaggedLoggingGuard( + logger, + level, + sourceLocation, + anchorRef, + message, + /*alwaysBuildMessage*/ false) + { } + + TTaggedLoggingGuard(const TTaggedLoggingGuard&) = delete; + TTaggedLoggingGuard& operator=(const TTaggedLoggingGuard&) = delete; + + bool IsEnabled() const + { + return Enabled_; + } + + template <class TValue> + TTaggedLoggingGuard& With(TStringBuf tag, const TValue& value) & + { + return DoWith(tag, value, "v"_sb); + } + + template <class TValue> + TTaggedLoggingGuard& With(TStringBuf tag, const TValue& value, TLoggingTagSpec spec) & + { + return DoWith(tag, value, spec.Get()); + } + + //! 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). + //! + //! Returns a #TWellKnownTaggedLoggingGuard, which exposes only further well-known + //! tags: the payload contract requires well-known tags to come last (so + //! #FormatTaggedPayload can stay single-pass), so a keyed |.With(key, value)| after a + //! well-known tag must not compile. + template <class TValue> + TWellKnownTaggedLoggingGuard With(const TValue& value) &; + + ~TTaggedLoggingGuard() + { + if (!Enabled_) { + return; + } + LogEventImpl( + LoggingContext_, + Logger_, + EffectiveLevel_, + SourceLocation_, + Anchor_, + Writer_.Finish()); + } + +protected: + const TLogger& Logger_; + const ::TSourceLocation SourceLocation_; + TLoggingAnchor* const Anchor_; + + bool Enabled_ = false; + ELogLevel EffectiveLevel_ = ELogLevel::Minimum; + TLoggingContext LoggingContext_; + TTaggedPayloadWriter Writer_; + + //! Shared constructor. When #alwaysBuildMessage is set the payload message is built + //! even if the level is disabled (so a terminal guard can still recover it); #Enabled_ + //! continues to gate whether the destructor emits the event. + TTaggedLoggingGuard( + const TLogger& logger, + ELogLevel level, + ::TSourceLocation sourceLocation, + TStaticAnchorRef anchorRef, + TStringBuf message, + bool alwaysBuildMessage) + : Logger_(logger) + , SourceLocation_(sourceLocation) + , Anchor_(anchorRef.Anchor) + { + if (!Logger_.IsAnchorUpToDate(*Anchor_)) [[unlikely]] { + Logger_.UpdateStaticAnchor(Anchor_, anchorRef.Registered, sourceLocation, message); + } + + EffectiveLevel_ = TLogger::GetEffectiveLoggingLevel(level, *Anchor_); + Enabled_ = Logger_.IsLevelEnabled(EffectiveLevel_); + if (!Enabled_ && !alwaysBuildMessage) { + return; + } + + LoggingContext_ = GetLoggingContext(); + + auto* builder = Writer_.BeginMessage(); + builder->AppendString(message); + if (HasMessageTags(LoggingContext_, Logger_)) { + builder->AppendString(" ("_sb); + AppendMessageTags(builder, LoggingContext_, Logger_); + builder->AppendChar(')'); + } + Writer_.EndMessage(); + } + +private: + template <class TValue> + TTaggedLoggingGuard& DoWith(TStringBuf tag, const TValue& value, TStringBuf spec) & + { + // Format the value straight into the payload buffer; no temporary. + FormatValue(Writer_.BeginTag(tag), value, spec); + Writer_.EndTag(); + return *this; + } +}; + +//! Restricts the fluent |.With| chain once a well-known tag has been attached. The +//! payload contract requires well-known tags to come last (#FormatTaggedPayload is +//! single-pass), so only further well-known |.With(value)| calls are exposed -- a keyed +//! |.With(key, value)| after a well-known tag fails to compile. +class TWellKnownTaggedLoggingGuard +{ +public: + explicit TWellKnownTaggedLoggingGuard(TTaggedLoggingGuard& guard) + : Guard_(guard) + { } + + template <class TValue> + TWellKnownTaggedLoggingGuard With(const TValue& value) && + { + return Guard_.With(value); + } + +private: + TTaggedLoggingGuard& Guard_; +}; + +template <class TValue> +TWellKnownTaggedLoggingGuard TTaggedLoggingGuard::With(const TValue& value) & +{ + FormatValue(Writer_.BeginWellKnownTag(GetWellKnownLoggingTag(value)), value, "v"_sb); + Writer_.EndTag(); + return TWellKnownTaggedLoggingGuard(*this); +} + +//! Terminal guard for the fluent |YT_TLOG_FATAL| macros. Builds the message +//! unconditionally and, once the |.With| chain completes, emits the event at |Fatal| +//! level -- which aborts the process. The enclosing |for| invokes #Commit in its step. +class TTaggedFatalLoggingGuard + : public TTaggedLoggingGuard +{ +public: + TTaggedFatalLoggingGuard( + const TLogger& logger, + ::TSourceLocation sourceLocation, + TStaticAnchorRef anchorRef, + TStringBuf message) + : TTaggedLoggingGuard(logger, ELogLevel::Fatal, sourceLocation, anchorRef, message, /*alwaysBuildMessage*/ true) + { } + + //! Returns true exactly once, so the enclosing |for| runs the |.With| chain a single + //! time before its step expression commits the event. + bool TryEnter() + { + bool pending = Pending_; + Pending_ = false; + return pending; + } + + //! Emits the event at |Fatal| level; the log manager aborts the process. + [[noreturn]] void Commit() & + { + Enabled_ = false; // The event is emitted here, not from the base destructor. + LogEventImpl(LoggingContext_, Logger_, ELogLevel::Fatal, SourceLocation_, Anchor_, Writer_.Finish()); + Y_UNREACHABLE(); + } + +private: + bool Pending_ = true; +}; + +//! Terminal guard for the fluent |YT_TLOG_ALERT_AND_THROW| macros. Builds the message +//! unconditionally; once the |.With| chain completes, #Commit emits the event at |Alert| +//! level (when enabled) and returns the message so the macro can attach it to the thrown +//! error. The throw lives in the macro -- the logging library must not depend on the +//! error library, and a destructor must not throw. +class TTaggedThrowingLoggingGuard + : public TTaggedLoggingGuard +{ +public: + TTaggedThrowingLoggingGuard( + const TLogger& logger, + ::TSourceLocation sourceLocation, + TStaticAnchorRef anchorRef, + TStringBuf message) + : TTaggedLoggingGuard(logger, ELogLevel::Alert, sourceLocation, anchorRef, message, /*alwaysBuildMessage*/ true) + { } + + //! Returns true exactly once, so the enclosing |for| runs the |.With| chain a single + //! time before its step expression commits the event and throws. + bool TryEnter() + { + bool pending = Pending_; + Pending_ = false; + return pending; + } + + //! Emits the alert event (when enabled) and returns its message for the error payload. + std::string Commit() & + { + auto payload = Writer_.Finish(); + std::string message(GetMessageFromTaggedPayload(payload)); + if (Enabled_) { + Enabled_ = false; // The event is emitted here, not from the base destructor. + LogEventImpl(LoggingContext_, Logger_, EffectiveLevel_, SourceLocation_, Anchor_, std::move(payload)); + } + return message; + } + +private: + bool Pending_ = true; +}; + +//! A no-op stand-in for #TTaggedLoggingGuard used by compile-time-disabled trace logging: +//! it swallows the |.With| chain without evaluating it. +class TNullTaggedLoggingGuard +{ +public: + template <class... TArgs> + TNullTaggedLoggingGuard& With(TArgs&&...) + { + return *this; + } +}; + +template <class TMessage> +TNullTaggedLoggingGuard MakeNullTaggedLoggingGuard(const TMessage&) +{ + return {}; +} + } // namespace NDetail //////////////////////////////////////////////////////////////////////////////// diff --git a/library/cpp/yt/logging/logger.cpp b/library/cpp/yt/logging/logger.cpp index 64d8b06fa7e..5359c1cd141 100644 --- a/library/cpp/yt/logging/logger.cpp +++ b/library/cpp/yt/logging/logger.cpp @@ -1,9 +1,13 @@ #include "logger.h" +#include "structured_payload.h" + #include <library/cpp/yt/assert/assert.h> #include <library/cpp/yt/cpu_clock/clock.h> +#include <library/cpp/yt/memory/leaky_singleton.h> + #include <library/cpp/yt/system/thread_name.h> #include <util/system/compiler.h> @@ -23,98 +27,13 @@ void OnCriticalLogEvent( event.Level == ELogLevel::Alert && logger.GetAbortOnAlert()) { fprintf(stderr, "*** Aborting on critical log event\n"); - fwrite(event.MessageRef.begin(), 1, event.MessageRef.size(), stderr); + auto message = FormatTaggedPayload(std::get<TTaggedLogEventPayload>(event.Payload)); + fwrite(message.data(), 1, message.size(), stderr); fprintf(stderr, "\n"); YT_ABORT(); } } -TSharedRef TMessageStringBuilder::Flush() -{ - return Buffer_.Slice(0, GetLength()); -} - -void TMessageStringBuilder::DoReset() -{ - Buffer_.Reset(); -} - -struct TPerThreadCache; - -YT_DEFINE_THREAD_LOCAL(TPerThreadCache*, Cache); -YT_DEFINE_THREAD_LOCAL(bool, CacheDestroyed); - -struct TPerThreadCache -{ - TSharedMutableRef Chunk; - size_t ChunkOffset = 0; - - ~TPerThreadCache() - { - TMessageStringBuilder::DisablePerThreadCache(); - } - - static YT_PREVENT_TLS_CACHING TPerThreadCache* GetCache() - { - auto& cache = Cache(); - if (Y_LIKELY(cache)) { - return cache; - } - if (CacheDestroyed()) { - return nullptr; - } - static thread_local TPerThreadCache CacheData; - cache = &CacheData; - return cache; - } -}; - -void TMessageStringBuilder::DisablePerThreadCache() -{ - Cache() = nullptr; - CacheDestroyed() = true; -} - -void TMessageStringBuilder::DoReserve(size_t newCapacity) -{ - auto oldLength = GetLength(); - newCapacity = FastClp2(newCapacity); - - auto newChunkSize = std::max(ChunkSize, newCapacity); - // Hold the old buffer until the data is copied. - auto oldBuffer = std::move(Buffer_); - auto* cache = TPerThreadCache::GetCache(); - if (Y_LIKELY(cache)) { - auto oldCapacity = End_ - Begin_; - auto deltaCapacity = newCapacity - oldCapacity; - if (End_ == cache->Chunk.Begin() + cache->ChunkOffset && - cache->ChunkOffset + deltaCapacity <= cache->Chunk.Size()) - { - // Resize inplace. - Buffer_ = cache->Chunk.Slice(cache->ChunkOffset - oldCapacity, cache->ChunkOffset + deltaCapacity); - cache->ChunkOffset += deltaCapacity; - End_ = Begin_ + newCapacity; - return; - } - - if (Y_UNLIKELY(cache->ChunkOffset + newCapacity > cache->Chunk.Size())) { - cache->Chunk = TSharedMutableRef::Allocate<TMessageBufferTag>(newChunkSize, {.InitializeStorage = false}); - cache->ChunkOffset = 0; - } - - Buffer_ = cache->Chunk.Slice(cache->ChunkOffset, cache->ChunkOffset + newCapacity); - cache->ChunkOffset += newCapacity; - } else { - Buffer_ = TSharedMutableRef::Allocate<TMessageBufferTag>(newChunkSize, {.InitializeStorage = false}); - newCapacity = newChunkSize; - } - if (oldLength > 0) { - ::memcpy(Buffer_.Begin(), Begin_, oldLength); - } - Begin_ = Buffer_.Begin(); - End_ = Begin_ + newCapacity; -} - } // namespace NDetail //////////////////////////////////////////////////////////////////////////////// @@ -149,16 +68,34 @@ ELogLevel GetThreadMinLogLevel() //////////////////////////////////////////////////////////////////////////////// -YT_DEFINE_THREAD_LOCAL(std::string, ThreadMessageTag); +YT_DEFINE_THREAD_LOCAL(bool, ThreadMessageTagDestroyed, false); + +struct TThreadMessageTagStorage +{ + std::string Tag; + + ~TThreadMessageTagStorage() + { + ThreadMessageTagDestroyed() = true; + } +}; + +YT_DEFINE_THREAD_LOCAL(TThreadMessageTagStorage, ThreadMessageTag); void SetThreadMessageTag(std::string messageTag) { - ThreadMessageTag() = std::move(messageTag); + if (Y_UNLIKELY(ThreadMessageTagDestroyed())) { + return; + } + ThreadMessageTag().Tag = std::move(messageTag); } std::string& GetThreadMessageTag() { - return ThreadMessageTag(); + if (Y_UNLIKELY(ThreadMessageTagDestroyed())) { + return *LeakySingleton<std::string>(); + } + return ThreadMessageTag().Tag; } //////////////////////////////////////////////////////////////////////////////// @@ -350,8 +287,7 @@ void LogStructuredEvent( loggingContext, logger, level); - event.MessageKind = ELogMessageKind::Structured; - event.MessageRef = message.ToSharedRef(); + event.Payload = MakeStructuredPayloadFromYson(message); event.Family = ELogFamily::Structured; logger.Write(std::move(event)); } diff --git a/library/cpp/yt/logging/logger.h b/library/cpp/yt/logging/logger.h index d781326e88d..0f29b6f7376 100644 --- a/library/cpp/yt/logging/logger.h +++ b/library/cpp/yt/logging/logger.h @@ -80,11 +80,6 @@ using TRequestId = TGuid; //////////////////////////////////////////////////////////////////////////////// -DEFINE_ENUM(ELogMessageKind, - (Unstructured) - (Structured) -); - struct TLogEvent { const TLoggingCategory* Category = nullptr; @@ -92,8 +87,9 @@ struct TLogEvent ELogFamily Family = ELogFamily::PlainText; bool Essential = false; - ELogMessageKind MessageKind = ELogMessageKind::Unstructured; - TSharedRef MessageRef; + //! The active alternative identifies the event kind (tagged plain-text vs + //! structured) and determines how the bytes are decoded. + TLogEventPayload Payload; TCpuInstant Instant = 0; @@ -327,10 +323,10 @@ void LogStructuredEvent( #define YT_LOG_ALERT_IF(condition, ...) if (condition) YT_LOG_ALERT(__VA_ARGS__) #define YT_LOG_ALERT_UNLESS(condition, ...) if (!(condition)) YT_LOG_ALERT(__VA_ARGS__) -#define YT_LOG_FATAL(...) \ - do { \ +#define YT_LOG_FATAL(...) \ + do { \ YT_LOG_EVENT(Logger, ::NYT::NLogging::ELogLevel::Fatal, __VA_ARGS__); \ - Y_UNREACHABLE(); \ + Y_UNREACHABLE(); \ } while(false) #define YT_LOG_FATAL_IF(condition, ...) if (Y_UNLIKELY(condition)) YT_LOG_FATAL(__VA_ARGS__) #define YT_LOG_FATAL_UNLESS(condition, ...) if (!Y_LIKELY(condition)) YT_LOG_FATAL(__VA_ARGS__) @@ -346,126 +342,233 @@ void LogStructuredEvent( * 4. The exception is raised regardless of the logging level configured for the |Logger| and * and the currently active message suppressions. */ -#define YT_LOG_ALERT_AND_THROW(...) \ - do { \ - /* NOLINTBEGIN(bugprone-reserved-identifier, readability-identifier-naming) */ \ - const auto& logger__ = Logger(); \ - auto level__ = ::NYT::NLogging::ELogLevel::Alert; \ - auto location__ = __LOCATION__; \ - \ - auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \ - auto message__ = ::NYT::NLogging::NDetail::BuildLogMessage(loggingContext__, logger__, __VA_ARGS__); \ - auto messageStr__ = ::std::string(message__.MessageRef.ToStringBuf()); \ - \ - static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> anchorStorage__; \ - auto* anchor__ = anchorStorage__.Get(); \ - \ - bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \ - if (!anchorUpToDate__) [[unlikely]] { \ - static std::atomic<bool> anchorRegistered__; \ - logger__.UpdateStaticAnchor(anchor__, &anchorRegistered__, location__, message__.Anchor); \ - } \ - \ - auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \ - if (logger__.IsLevelEnabled(effectiveLevel__)) { \ - ::NYT::NLogging::NDetail::LogEventImpl( \ - loggingContext__, \ - logger__, \ - effectiveLevel__, \ - location__, \ - anchor__, \ - std::move(message__.MessageRef)); \ - } \ - \ - THROW_ERROR_EXCEPTION( \ - ::NYT::EErrorCode::Fatal, \ - "Malformed request or incorrect state detected") \ - << ::NYT::TErrorAttribute("message", std::move(messageStr__)); \ - /* NOLINTEND(bugprone-reserved-identifier, readability-identifier-naming) */ \ +#define YT_LOG_ALERT_AND_THROW(...) \ + do { \ + /* NOLINTBEGIN(bugprone-reserved-identifier, readability-identifier-naming) */ \ + const auto& logger__ = Logger(); \ + auto level__ = ::NYT::NLogging::ELogLevel::Alert; \ + auto location__ = __LOCATION__; \ + \ + auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \ + auto message__ = ::NYT::NLogging::NDetail::BuildLogMessage(loggingContext__, logger__, __VA_ARGS__); \ + /* Copy the message out before the payload is moved into the log event below. */ \ + auto messageStr__ = ::std::string(::NYT::NLogging::GetMessageFromTaggedPayload(message__.Payload)); \ + \ + static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> anchorStorage__; \ + auto* anchor__ = anchorStorage__.Get(); \ + \ + bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \ + if (!anchorUpToDate__) [[unlikely]] { \ + static std::atomic<bool> anchorRegistered__; \ + logger__.UpdateStaticAnchor(anchor__, &anchorRegistered__, location__, message__.Anchor); \ + } \ + \ + auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \ + if (logger__.IsLevelEnabled(effectiveLevel__)) { \ + ::NYT::NLogging::NDetail::LogEventImpl( \ + loggingContext__, \ + logger__, \ + effectiveLevel__, \ + location__, \ + anchor__, \ + std::move(message__.Payload)); \ + } \ + \ + THROW_ERROR_EXCEPTION( \ + ::NYT::EErrorCode::Fatal, \ + "Malformed request or incorrect state detected") \ + << ::NYT::TErrorAttribute("message", std::move(messageStr__)); \ + /* NOLINTEND(bugprone-reserved-identifier, readability-identifier-naming) */ \ } while (false) #define YT_LOG_ALERT_AND_THROW_IF(condition, ...) \ - if (Y_UNLIKELY(condition)) { \ - YT_LOG_ALERT_AND_THROW(__VA_ARGS__); \ - } \ + if (Y_UNLIKELY(condition)) { \ + YT_LOG_ALERT_AND_THROW(__VA_ARGS__); \ + } \ static_assert(true) #define YT_LOG_ALERT_AND_THROW_UNLESS(condition, ...) \ - if (!Y_UNLIKELY(condition)) { \ - YT_LOG_ALERT_AND_THROW(__VA_ARGS__); \ - } \ + if (!Y_UNLIKELY(condition)) { \ + YT_LOG_ALERT_AND_THROW(__VA_ARGS__); \ + } \ static_assert(true) -#define YT_LOG_EVENT(logger, level, ...) \ - do { \ - /* NOLINTBEGIN(bugprone-reserved-identifier, readability-identifier-naming) */ \ - const auto& logger__ = (logger)(); \ - auto level__ = (level); \ - auto location__ = __LOCATION__; \ - static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> anchorStorage__; \ - auto* anchor__ = anchorStorage__.Get(); \ - \ - bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \ - if (anchorUpToDate__) [[likely]] { \ - auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \ - if (!logger__.IsLevelEnabled(effectiveLevel__)) { \ - break; \ - } \ - } \ - \ - auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \ +#define YT_LOG_EVENT(logger, level, ...) \ + do { \ + /* NOLINTBEGIN(bugprone-reserved-identifier, readability-identifier-naming) */ \ + const auto& logger__ = (logger)(); \ + auto level__ = (level); \ + auto location__ = __LOCATION__; \ + static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> anchorStorage__; \ + auto* anchor__ = anchorStorage__.Get(); \ + \ + bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \ + if (anchorUpToDate__) [[likely]] { \ + auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \ + if (!logger__.IsLevelEnabled(effectiveLevel__)) { \ + break; \ + } \ + } \ + \ + auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \ auto message__ = ::NYT::NLogging::NDetail::BuildLogMessage(loggingContext__, logger__, __VA_ARGS__); \ - \ - if (!anchorUpToDate__) [[unlikely]] { \ - static std::atomic<bool> anchorRegistered__; \ - logger__.UpdateStaticAnchor(anchor__, &anchorRegistered__, location__, message__.Anchor); \ - } \ - \ - auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \ - if (!logger__.IsLevelEnabled(effectiveLevel__)) { \ - break; \ - } \ - \ - ::NYT::NLogging::NDetail::LogEventImpl( \ - loggingContext__, \ - logger__, \ - effectiveLevel__, \ - location__, \ - anchor__, \ - std::move(message__.MessageRef)); \ - /* NOLINTEND(bugprone-reserved-identifier, readability-identifier-naming) */ \ + \ + if (!anchorUpToDate__) [[unlikely]] { \ + static std::atomic<bool> anchorRegistered__; \ + logger__.UpdateStaticAnchor(anchor__, &anchorRegistered__, location__, message__.Anchor); \ + } \ + \ + auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \ + if (!logger__.IsLevelEnabled(effectiveLevel__)) { \ + break; \ + } \ + \ + ::NYT::NLogging::NDetail::LogEventImpl( \ + loggingContext__, \ + logger__, \ + effectiveLevel__, \ + location__, \ + anchor__, \ + std::move(message__.Payload)); \ + /* NOLINTEND(bugprone-reserved-identifier, readability-identifier-naming) */ \ } while (false) -#define YT_LOG_EVENT_WITH_DYNAMIC_ANCHOR(logger, level, anchor, ...) \ - do { \ - const auto& logger__ = (logger)(); \ - auto level__ = (level); \ - auto location__ = __LOCATION__; \ - auto* anchor__ = (anchor); \ - \ - bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \ - if (!anchorUpToDate__) [[unlikely]] { \ - logger__.UpdateDynamicAnchor(anchor__); \ - } \ - \ - auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \ - if (!logger__.IsLevelEnabled(effectiveLevel__)) { \ - break; \ - } \ - \ - auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \ +#define YT_LOG_EVENT_WITH_DYNAMIC_ANCHOR(logger, level, anchor, ...) \ + do { \ + const auto& logger__ = (logger)(); \ + auto level__ = (level); \ + auto location__ = __LOCATION__; \ + auto* anchor__ = (anchor); \ + \ + bool anchorUpToDate__ = logger__.IsAnchorUpToDate(*anchor__); \ + if (!anchorUpToDate__) [[unlikely]] { \ + logger__.UpdateDynamicAnchor(anchor__); \ + } \ + \ + auto effectiveLevel__ = ::NYT::NLogging::TLogger::GetEffectiveLoggingLevel(level__, *anchor__); \ + if (!logger__.IsLevelEnabled(effectiveLevel__)) { \ + break; \ + } \ + \ + auto loggingContext__ = ::NYT::NLogging::GetLoggingContext(); \ auto message__ = ::NYT::NLogging::NDetail::BuildLogMessage(loggingContext__, logger__, __VA_ARGS__); \ - \ - ::NYT::NLogging::NDetail::LogEventImpl( \ - loggingContext__, \ - logger__, \ - effectiveLevel__, \ - location__, \ - anchor__, \ - std::move(message__.MessageRef)); \ + \ + ::NYT::NLogging::NDetail::LogEventImpl( \ + loggingContext__, \ + logger__, \ + effectiveLevel__, \ + location__, \ + anchor__, \ + std::move(message__.Payload)); \ } while (false) //////////////////////////////////////////////////////////////////////////////// +// Tagged logging +// +// 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): +// +// YT_TLOG_INFO("Message") +// .With("Key", value) +// .With("Count", count, "%08x") +// .With(error); +// +// If the message is not logged then the |.With| chain is not evaluated, so tag value +// expressions cost nothing. + +//! Yields a #TStaticAnchorRef for the expansion site: a per-call-site leaky anchor and +//! its one-shot registration flag, produced via an immediately-invoked lambda. +#define YT_TLOG_STATIC_ANCHOR_REF() \ + [] { \ + /* NOLINTBEGIN(bugprone-reserved-identifier, readability-identifier-naming) */ \ + static ::NYT::TLeakyStorage<::NYT::NLogging::TLoggingAnchor> anchorStorage__; \ + static std::atomic<bool> anchorRegistered__; \ + return ::NYT::NLogging::NDetail::TStaticAnchorRef{anchorStorage__.Get(), &anchorRegistered__}; \ + /* NOLINTEND(bugprone-reserved-identifier, readability-identifier-naming) */ \ + }() + +#define YT_TLOG_EVENT_FLUENT(logger, level, message) \ + if (::NYT::NLogging::NDetail::TTaggedLoggingGuard loggingGuard__( \ + (logger)(), \ + (level), \ + __LOCATION__, \ + YT_TLOG_STATIC_ANCHOR_REF(), \ + (message)); \ + !loggingGuard__.IsEnabled()) \ + { } else \ + loggingGuard__ + +#ifdef YT_ENABLE_TRACE_LOGGING +#define YT_TLOG_TRACE(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Trace, message) +#define YT_TLOG_TRACE_IF(condition, message) if (condition) YT_TLOG_TRACE(message) +#define YT_TLOG_TRACE_UNLESS(condition, message) if (!(condition)) YT_TLOG_TRACE(message) +#else +#define YT_TLOG_UNUSED(message) if (true) { } else ::NYT::NLogging::NDetail::MakeNullTaggedLoggingGuard(message) +#define YT_TLOG_TRACE(message) YT_TLOG_UNUSED(message) +#define YT_TLOG_TRACE_IF(condition, message) YT_TLOG_UNUSED(message) +#define YT_TLOG_TRACE_UNLESS(condition, message) YT_TLOG_UNUSED(message) +#endif + +#define YT_TLOG_DEBUG(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Debug, message) +#define YT_TLOG_DEBUG_IF(condition, message) if (condition) YT_TLOG_DEBUG(message) +#define YT_TLOG_DEBUG_UNLESS(condition, message) if (!(condition)) YT_TLOG_DEBUG(message) + +#define YT_TLOG_INFO(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Info, message) +#define YT_TLOG_INFO_IF(condition, message) if (condition) YT_TLOG_INFO(message) +#define YT_TLOG_INFO_UNLESS(condition, message) if (!(condition)) YT_TLOG_INFO(message) + +#define YT_TLOG_WARNING(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Warning, message) +#define YT_TLOG_WARNING_IF(condition, message) if (condition) YT_TLOG_WARNING(message) +#define YT_TLOG_WARNING_UNLESS(condition, message) if (!(condition)) YT_TLOG_WARNING(message) + +#define YT_TLOG_ERROR(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Error, message) +#define YT_TLOG_ERROR_IF(condition, message) if (condition) YT_TLOG_ERROR(message) +#define YT_TLOG_ERROR_UNLESS(condition, message) if (!(condition)) YT_TLOG_ERROR(message) + +#define YT_TLOG_ALERT(message) YT_TLOG_EVENT_FLUENT(Logger, ::NYT::NLogging::ELogLevel::Alert, message) +#define YT_TLOG_ALERT_IF(condition, message) if (condition) YT_TLOG_ALERT(message) +#define YT_TLOG_ALERT_UNLESS(condition, message) if (!(condition)) YT_TLOG_ALERT(message) + +// The terminal action of |YT_TLOG_FATAL|/|YT_TLOG_ALERT_AND_THROW| (aborting or throwing) +// must run *after* the |.With| chain, which the user appends to the macro. A guard cannot +// do it in its destructor -- that would run mid-chain on a temporary, and a throwing +// destructor terminates. So both expand to a single-iteration |for| whose step expression +// fires once the chain (the loop body) has completed. + +#define YT_TLOG_FATAL(message) \ + for (::NYT::NLogging::NDetail::TTaggedFatalLoggingGuard loggingGuard__( \ + Logger(), \ + __LOCATION__, \ + YT_TLOG_STATIC_ANCHOR_REF(), \ + (message)); \ + loggingGuard__.TryEnter(); \ + loggingGuard__.Commit()) \ + loggingGuard__ +#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) + +// See #YT_LOG_ALERT_AND_THROW for the rationale. The throw lives here -- not in the guard +// -- because the logging library must not depend on the error library. The guard's +// |Commit| logs the alert (when enabled) and returns the message for the |"message"| +// attribute. +#define YT_TLOG_ALERT_AND_THROW(message) \ + for (::NYT::NLogging::NDetail::TTaggedThrowingLoggingGuard loggingGuard__( \ + Logger(), \ + __LOCATION__, \ + YT_TLOG_STATIC_ANCHOR_REF(), \ + (message)); \ + loggingGuard__.TryEnter(); \ + THROW_ERROR_EXCEPTION( \ + ::NYT::EErrorCode::Fatal, \ + "Malformed request or incorrect state detected") \ + << ::NYT::TErrorAttribute("message", loggingGuard__.Commit())) \ + loggingGuard__ +#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) + +//////////////////////////////////////////////////////////////////////////////// } // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/plain_text_formatter/formatter.cpp b/library/cpp/yt/logging/plain_text_formatter/formatter.cpp index ab2181113dd..85f0889fa9b 100644 --- a/library/cpp/yt/logging/plain_text_formatter/formatter.cpp +++ b/library/cpp/yt/logging/plain_text_formatter/formatter.cpp @@ -1,9 +1,13 @@ #include "formatter.h" +#include <library/cpp/yt/logging/structured_payload.h> + #include <library/cpp/yt/cpu_clock/clock.h> #include <library/cpp/yt/misc/port.h> +#include <variant> + #ifdef YT_USE_SSE42 #include <emmintrin.h> #include <pmmintrin.h> @@ -146,6 +150,37 @@ void FormatMessage(TBaseFormatter* out, TStringBuf message) } } +// Formats |Message (Key: Value, ...)|, with well-known tags (e.g. an error) appended +// after the |(...)| group. Well-known tags are always written last, so a single pass +// suffices. Every piece -- message, tag keys/values, and the newline separating a +// well-known tag -- goes through FormatMessage and is escaped, so the rendered payload +// stays on a single physical line (a newline is emitted as the literal "\n"). +void FormatPayload(TBaseFormatter* out, const TTaggedLogEventPayload& payload) +{ + TTaggedPayloadReader reader(payload); + FormatMessage(out, reader.ReadMessage()); + bool parenOpen = false; + while (auto tag = reader.TryReadTag()) { + if (tag->IsWellKnown) { + if (parenOpen) { + out->AppendChar(')'); + parenOpen = false; + } + FormatMessage(out, "\n"_sb); + FormatMessage(out, tag->Value); + } else { + out->AppendString(parenOpen ? ", "_sb : " ("_sb); + parenOpen = true; + FormatMessage(out, tag->Key); + out->AppendString(": "_sb); + FormatMessage(out, tag->Value); + } + } + if (parenOpen) { + out->AppendChar(')'); + } +} + //////////////////////////////////////////////////////////////////////////////// void TCachingDateFormatter::Format(TBaseFormatter* buffer, TInstant dateTime, bool printMicroseconds) @@ -186,7 +221,13 @@ void TPlainTextEventFormatter::Format(TBaseFormatter* buffer, const TLogEvent& e buffer->AppendChar('\t'); - FormatMessage(buffer, event.MessageRef.ToStringBuf()); + if (const auto* tagged = std::get_if<TTaggedLogEventPayload>(&event.Payload)) { + FormatPayload(buffer, *tagged); + } else { + // A structured event routed to a plain-text writer: emit its raw YSON fragment + // (escaped, so the record stays a single physical line). + FormatMessage(buffer, GetYsonFromStructuredPayload(std::get<TStructuredLogEventPayload>(event.Payload)).AsStringBuf()); + } buffer->AppendChar('\t'); diff --git a/library/cpp/yt/logging/private.h b/library/cpp/yt/logging/private.h new file mode 100644 index 00000000000..b80f0488048 --- /dev/null +++ b/library/cpp/yt/logging/private.h @@ -0,0 +1,15 @@ +#pragma once + +#include "public.h" + +namespace NYT::NLogging::NDetail { + +//////////////////////////////////////////////////////////////////////////////// + +//! Ref-counted memory tag for payload buffers; used for memory accounting. +struct TMessageBufferTag +{ }; + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NLogging::NDetail diff --git a/library/cpp/yt/logging/public.h b/library/cpp/yt/logging/public.h index 1e2b59ca0d3..17f8998f1af 100644 --- a/library/cpp/yt/logging/public.h +++ b/library/cpp/yt/logging/public.h @@ -1,6 +1,11 @@ #pragma once #include <library/cpp/yt/misc/enum.h> +#include <library/cpp/yt/misc/strong_typedef.h> + +#include <library/cpp/yt/memory/ref.h> + +#include <variant> namespace NYT::NLogging { @@ -36,4 +41,18 @@ struct ILogManager; //////////////////////////////////////////////////////////////////////////////// +//! Opaque payload of a plain-text log event: a message plus optional key/value tags, +//! framed by #TTaggedPayloadWriter. Produced by the YT_LOG_*/YT_TLOG_* macros. +YT_DEFINE_STRONG_TYPEDEF(TTaggedLogEventPayload, TSharedRef); + +//! Opaque payload of a structured log event: a raw YSON map fragment. +//! Produced by #LogStructuredEvent. +YT_DEFINE_STRONG_TYPEDEF(TStructuredLogEventPayload, TSharedRef); + +//! The payload carried by a #TLogEvent: exactly one of the encodings above. The active +//! alternative both identifies the event kind and determines how the payload is decoded. +using TLogEventPayload = std::variant<TTaggedLogEventPayload, TStructuredLogEventPayload>; + +//////////////////////////////////////////////////////////////////////////////// + } // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/structured_payload.cpp b/library/cpp/yt/logging/structured_payload.cpp new file mode 100644 index 00000000000..1a6ce5ea370 --- /dev/null +++ b/library/cpp/yt/logging/structured_payload.cpp @@ -0,0 +1,19 @@ +#include "structured_payload.h" + +namespace NYT::NLogging { + +//////////////////////////////////////////////////////////////////////////////// + +TStructuredLogEventPayload MakeStructuredPayloadFromYson(const NYson::TYsonString& message) +{ + return TStructuredLogEventPayload(message.ToSharedRef()); +} + +NYson::TYsonStringBuf GetYsonFromStructuredPayload(const TStructuredLogEventPayload& payload) +{ + return NYson::TYsonStringBuf(payload.Underlying().ToStringBuf(), NYson::EYsonType::MapFragment); +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/structured_payload.h b/library/cpp/yt/logging/structured_payload.h new file mode 100644 index 00000000000..81464f4aa92 --- /dev/null +++ b/library/cpp/yt/logging/structured_payload.h @@ -0,0 +1,25 @@ +#pragma once + +#include "public.h" + +#include <library/cpp/yt/memory/ref.h> + +#include <library/cpp/yt/yson_string/string.h> + +namespace NYT::NLogging { + +//////////////////////////////////////////////////////////////////////////////// + +// Structured events carry the raw YSON map fragment as their opaque payload, with no +// framing. These helpers isolate that representation from callers. + +//! Producer: wraps #message into a payload (zero-copy). +TStructuredLogEventPayload MakeStructuredPayloadFromYson(const NYson::TYsonString& message); + +//! Consumer: views the payload as the YSON map fragment it carries. The result views +//! into #payload, which must outlive it. +NYson::TYsonStringBuf GetYsonFromStructuredPayload(const TStructuredLogEventPayload& payload); + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/tagged_payload-inl.h b/library/cpp/yt/logging/tagged_payload-inl.h new file mode 100644 index 00000000000..4b71a5bebef --- /dev/null +++ b/library/cpp/yt/logging/tagged_payload-inl.h @@ -0,0 +1,108 @@ +#ifndef TAGGED_PAYLOAD_INL_H_ +#error "Direct inclusion of this file is not allowed, include tagged_payload.h" +// For the sake of sane code completion. +#include "tagged_payload.h" +#endif + +#include <library/cpp/yt/assert/assert.h> + +#include <cstring> + +namespace NYT::NLogging { + +//////////////////////////////////////////////////////////////////////////////// + +inline TSharedRef TTaggedPayloadBuilder::Flush() +{ + return Buffer_.Slice(0, GetLength()); +} + +template <class T> +void TTaggedPayloadBuilder::AppendPod(const T& value) +{ + ::memcpy(Preallocate(sizeof(value)), &value, sizeof(value)); + Advance(sizeof(value)); +} + +template <class T> +void TTaggedPayloadBuilder::WritePodAt(size_t offset, const T& value) +{ + YT_ASSERT(offset + sizeof(value) <= GetLength()); + ::memcpy(Begin_ + offset, &value, sizeof(value)); +} + +//////////////////////////////////////////////////////////////////////////////// + +inline TStringBuilderBase* TTaggedPayloadWriter::BeginMessage() & +{ + YT_ASSERT(!MessageStarted_); + MessageStarted_ = true; + ReserveLengthPrefix(); + return &Builder_; +} + +inline TTaggedPayloadWriter& TTaggedPayloadWriter::EndMessage() & +{ + YT_ASSERT(MessageStarted_ && !MessageEnded_); + MessageEnded_ = true; + BackpatchLengthPrefix(); + return *this; +} + +inline TStringBuilderBase* TTaggedPayloadWriter::BeginTag(TStringBuf key) & +{ + return DoBeginTag(key, /*wellKnown*/ false); +} + +inline TStringBuilderBase* TTaggedPayloadWriter::BeginWellKnownTag(TStringBuf key) & +{ + return DoBeginTag(key, /*wellKnown*/ true); +} + +inline TStringBuilderBase* TTaggedPayloadWriter::DoBeginTag(TStringBuf key, bool wellKnown) +{ + YT_ASSERT(MessageEnded_ && !InTag_); + // High bit reserved for the well-known flag. + YT_ASSERT(key.size() < WellKnownTagFlag); + InTag_ = true; + // Reserve the key-size prefix, the key bytes, and the value-size prefix in a single + // capacity check; the value is then formatted in place and EndTag backpatches its size. + auto keySize = static_cast<ui32>(key.size()) | (wellKnown ? WellKnownTagFlag : 0); + char* ptr = Builder_.Preallocate(2 * sizeof(ui32) + key.size()); + PrefixOffset_ = Builder_.GetLength() + sizeof(ui32) + key.size(); + ::memcpy(ptr, &keySize, sizeof(keySize)); + ::memcpy(ptr + sizeof(keySize), key.data(), key.size()); + // The value-size prefix is left uninitialized; EndTag overwrites it. + Builder_.Advance(2 * sizeof(ui32) + key.size()); + return &Builder_; +} + +inline TTaggedPayloadWriter& TTaggedPayloadWriter::EndTag() & +{ + YT_ASSERT(InTag_); + InTag_ = false; + BackpatchLengthPrefix(); + return *this; +} + +inline TTaggedLogEventPayload TTaggedPayloadWriter::Finish() & +{ + YT_ASSERT(MessageEnded_ && !InTag_); + return TTaggedLogEventPayload(Builder_.Flush()); +} + +inline void TTaggedPayloadWriter::ReserveLengthPrefix() +{ + PrefixOffset_ = Builder_.GetLength(); + // Reserve a zeroed ui32; backpatched by BackpatchLengthPrefix. + Builder_.AppendPod<ui32>(0); +} + +inline void TTaggedPayloadWriter::BackpatchLengthPrefix() +{ + Builder_.WritePodAt<ui32>(PrefixOffset_, Builder_.GetLength() - PrefixOffset_ - sizeof(ui32)); +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/tagged_payload.cpp b/library/cpp/yt/logging/tagged_payload.cpp new file mode 100644 index 00000000000..4133cc0bd55 --- /dev/null +++ b/library/cpp/yt/logging/tagged_payload.cpp @@ -0,0 +1,214 @@ +#include "tagged_payload.h" +#include "private.h" + +#include <library/cpp/yt/assert/assert.h> + +#include <library/cpp/yt/misc/tls.h> + +#include <util/system/compiler.h> +#include <util/system/types.h> + +#include <util/generic/bitops.h> + +#include <algorithm> + +namespace NYT::NLogging { + +//////////////////////////////////////////////////////////////////////////////// + +namespace { + +struct TPerThreadCache; + +YT_DEFINE_THREAD_LOCAL(TPerThreadCache*, Cache); +YT_DEFINE_THREAD_LOCAL(bool, CacheDestroyed); + +struct TPerThreadCache +{ + TSharedMutableRef Chunk; + size_t ChunkOffset = 0; + + ~TPerThreadCache() + { + TTaggedPayloadBuilder::DisablePerThreadCache(); + } + + static YT_PREVENT_TLS_CACHING TPerThreadCache* GetCache() + { + auto& cache = Cache(); + [[likely]] if (cache) { + return cache; + } + if (CacheDestroyed()) { + return nullptr; + } + static thread_local TPerThreadCache CacheData; + cache = &CacheData; + return cache; + } +}; + +TSharedMutableRef AllocateChunk(size_t size) +{ + return TSharedMutableRef::Allocate<::NYT::NLogging::NDetail::TMessageBufferTag>(size, {.InitializeStorage = false}); +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// + +void TTaggedPayloadBuilder::DisablePerThreadCache() +{ + Cache() = nullptr; + CacheDestroyed() = true; +} + +void TTaggedPayloadBuilder::DoReset() +{ + Buffer_.Reset(); +} + +void TTaggedPayloadBuilder::DoReserve(size_t newCapacity) +{ + auto oldLength = GetLength(); + newCapacity = FastClp2(newCapacity); + + auto newChunkSize = std::max(ChunkSize, newCapacity); + // Hold the old buffer until the data is copied. + auto oldBuffer = std::move(Buffer_); + auto* cache = TPerThreadCache::GetCache(); + [[likely]] if (cache) { + auto oldCapacity = End_ - Begin_; + auto deltaCapacity = newCapacity - oldCapacity; + if (End_ == cache->Chunk.Begin() + cache->ChunkOffset && + cache->ChunkOffset + deltaCapacity <= cache->Chunk.Size()) + { + // Resize inplace. + Buffer_ = cache->Chunk.Slice(cache->ChunkOffset - oldCapacity, cache->ChunkOffset + deltaCapacity); + cache->ChunkOffset += deltaCapacity; + End_ = Begin_ + newCapacity; + return; + } + + [[unlikely]] if (cache->ChunkOffset + newCapacity > cache->Chunk.Size()) { + cache->Chunk = AllocateChunk(newChunkSize); + cache->ChunkOffset = 0; + } + + Buffer_ = cache->Chunk.Slice(cache->ChunkOffset, cache->ChunkOffset + newCapacity); + cache->ChunkOffset += newCapacity; + } else { + Buffer_ = AllocateChunk(newChunkSize); + newCapacity = newChunkSize; + } + if (oldLength > 0) { + ::memcpy(Buffer_.Begin(), Begin_, oldLength); + } + Begin_ = Buffer_.Begin(); + End_ = Begin_ + newCapacity; +} + +//////////////////////////////////////////////////////////////////////////////// + +void TTaggedPayloadWriter::DisablePerThreadCache() +{ + TTaggedPayloadBuilder::DisablePerThreadCache(); +} + +//////////////////////////////////////////////////////////////////////////////// + +TTaggedPayloadReader::TTaggedPayloadReader(const TTaggedLogEventPayload& payload) + : Current_(payload.Underlying().Begin()) + , End_(payload.Underlying().End()) +{ } + +TStringBuf TTaggedPayloadReader::ReadMessage() +{ + YT_ASSERT(!MessageRead_); + MessageRead_ = true; + return ReadString(); +} + +std::optional<TTaggedPayloadReader::TTag> TTaggedPayloadReader::TryReadTag() +{ + YT_ASSERT(MessageRead_); + if (Current_ == End_) { + return std::nullopt; + } + auto keySize = ReadLength(); + bool wellKnown = (keySize & WellKnownTagFlag) != 0; + auto key = ReadBytes(keySize & ~WellKnownTagFlag); + auto value = ReadString(); + return TTag{.IsWellKnown = wellKnown, .Key = key, .Value = value}; +} + +ui32 TTaggedPayloadReader::ReadLength() +{ + YT_ASSERT(Current_ + sizeof(ui32) <= End_); + ui32 size; + ::memcpy(&size, Current_, sizeof(size)); + Current_ += sizeof(size); + return size; +} + +TStringBuf TTaggedPayloadReader::ReadBytes(ui32 size) +{ + YT_ASSERT(Current_ + size <= End_); + TStringBuf result(Current_, size); + Current_ += size; + return result; +} + +TStringBuf TTaggedPayloadReader::ReadString() +{ + return ReadBytes(ReadLength()); +} + +//////////////////////////////////////////////////////////////////////////////// + +TTaggedLogEventPayload MakeTaggedPayloadFromMessage(TStringBuf message) +{ + TTaggedPayloadWriter writer; + writer.BeginMessage()->AppendString(message); + writer.EndMessage(); + return writer.Finish(); +} + +TStringBuf GetMessageFromTaggedPayload(const TTaggedLogEventPayload& payload) +{ + return TTaggedPayloadReader(payload).ReadMessage(); +} + +std::string FormatTaggedPayload(const TTaggedLogEventPayload& payload) +{ + TTaggedPayloadReader reader(payload); + std::string result(reader.ReadMessage()); + // Well-known tags (e.g. an error) are always written last -- the fluent |.With| chain + // enforces this at the type level (see #TWellKnownTaggedLoggingGuard) -- so they render + // after the |(...)| group on trailing lines in a single pass. + bool parenOpen = false; + while (auto tag = reader.TryReadTag()) { + if (tag->IsWellKnown) { + if (parenOpen) { + result += ')'; + parenOpen = false; + } + result += '\n'; + result += tag->Value; + } else { + result += parenOpen ? ", " : " ("; + parenOpen = true; + result += tag->Key; + result += ": "; + result += tag->Value; + } + } + if (parenOpen) { + result += ')'; + } + return result; +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/tagged_payload.h b/library/cpp/yt/logging/tagged_payload.h new file mode 100644 index 00000000000..da46f30f42a --- /dev/null +++ b/library/cpp/yt/logging/tagged_payload.h @@ -0,0 +1,201 @@ +#pragma once + +#include "public.h" + +#include <library/cpp/yt/string/string_builder.h> + +#include <library/cpp/yt/memory/ref.h> + +#include <util/generic/strbuf.h> +#include <util/generic/size_literals.h> + +#include <util/system/types.h> + +#include <optional> +#include <string> + +namespace NYT::NLogging { + +//////////////////////////////////////////////////////////////////////////////// + +// Plain-text events carry their message together with structured key/value tags in a +// flat, length-prefixed payload, deferring tag rendering to the consumer (the logging +// thread). These helpers own the wire format and isolate it from callers. + +//! A per-thread chunk-cached string builder backing payload buffers. +/*! + * Generic: it knows nothing about the payload layout (see #TTaggedPayloadWriter). + */ +class TTaggedPayloadBuilder + : public TStringBuilderBase +{ +public: + TSharedRef Flush(); + + //! Appends a trivially-copyable value as raw bytes (the ui32 length prefix). + template <class T> + void AppendPod(const T& value); + + //! Overwrites a trivially-copyable value at #offset. + //! #offset + sizeof(value) must not exceed the current length. + template <class T> + void WritePodAt(size_t offset, const T& value); + + //! For testing only. + static void DisablePerThreadCache(); + +private: + TSharedMutableRef Buffer_; + + static constexpr size_t ChunkSize = 128_KB - 64; + + void DoReset() override; + void DoReserve(size_t newCapacity) override; +}; + +//////////////////////////////////////////////////////////////////////////////// + +//! A log message together with its tag key/value pairs, serialized into a flat, +//! opaque byte payload that #TLogEvent hands to the logging thread. +/*! + * Layout (all sizes are host-endian |ui32|; the transport is in-process): + * \code + * [message size][message bytes] + * [key size][key bytes][value size][value bytes] x N + * \endcode + * + * A "well-known" tag sets the high bit of its key-size field; the low 31 bits stay the + * key length and the key bytes are still written. Consumers may render such tags + * specially (see #GetWellKnownLoggingTag). + * + * Both the producer (#TTaggedPayloadWriter) and the consumer + * (#TTaggedPayloadReader) own this single definition of the layout. + */ + +//! The high bit of a tag's key-size field, marking a well-known tag. +constexpr ui32 WellKnownTagFlag = 1u << 31; + +//////////////////////////////////////////////////////////////////////////////// + +//! Producer side: serializes the message and its tags into a payload. +/*! + * Owns a per-thread chunk-cached buffer; both the message text (#BeginMessage) and + * tag values (#BeginTag) are appended through the returned #TStringBuilderBase, so a + * formatted value is built directly into the payload without an intermediate + * allocation/copy. The length-prefix framing lives entirely here -- the underlying + * builder is format-agnostic. + */ +class TTaggedPayloadWriter +{ +public: + //! Constructs an empty writer; the message is built incrementally through the + //! builder returned by #BeginMessage. + TTaggedPayloadWriter() = default; + + TTaggedPayloadWriter(const TTaggedPayloadWriter&) = delete; + TTaggedPayloadWriter& operator=(const TTaggedPayloadWriter&) = delete; + + //! Begins the message field and returns a builder for appending its text. + //! Must be called exactly once, first. + TStringBuilderBase* BeginMessage() &; + + //! Ends the message field, filling in its reserved length prefix. Must be + //! called exactly once, after the message text and before any tag/#Finish. + TTaggedPayloadWriter& EndMessage() &; + + //! Begins a tag: writes #key, then reserves the value's length prefix and returns + //! a builder for appending the value. Lets the value be formatted directly into the + //! payload buffer. Pair with #EndTag. Must follow #EndMessage. + TStringBuilderBase* BeginTag(TStringBuf key) &; + + //! Like #BeginTag, but marks the tag well-known (sets #WellKnownTagFlag). + TStringBuilderBase* BeginWellKnownTag(TStringBuf key) &; + + //! Ends the current tag, filling in the value's reserved length prefix. + TTaggedPayloadWriter& EndTag() &; + + //! Returns the serialized payload. Must follow #EndMessage. + TTaggedLogEventPayload Finish() &; + + //! For testing only. + static void DisablePerThreadCache(); + +private: + TTaggedPayloadBuilder Builder_; + bool MessageStarted_ = false; + bool MessageEnded_ = false; + bool InTag_ = false; + //! Offset of the length prefix currently being filled (message or tag value). + size_t PrefixOffset_ = 0; + + //! Shared implementation of #BeginTag and #BeginWellKnownTag. + TStringBuilderBase* DoBeginTag(TStringBuf key, bool wellKnown); + //! Reserves a length prefix at the current position, remembering its offset. + void ReserveLengthPrefix(); + //! Backpatches the reserved prefix with the length appended after it. + void BackpatchLengthPrefix(); +}; + +//////////////////////////////////////////////////////////////////////////////// + +//! Consumer side: parses a payload produced by #TTaggedPayloadWriter. +/*! + * A single-pass cursor; the returned views point into #payload, which must outlive + * the reader. + * + * Call order: #ReadMessage must be called exactly once, first; then #TryReadTag + * must be called repeatedly until it returns |std::nullopt|, yielding the tags in + * write order. + */ +class TTaggedPayloadReader +{ +public: + struct TTag + { + bool IsWellKnown = false; + TStringBuf Key; + TStringBuf Value; + }; + + explicit TTaggedPayloadReader(const TTaggedLogEventPayload& payload); + + //! Reads the message field. Must be called exactly once, before any #TryReadTag. + TStringBuf ReadMessage(); + + //! Reads the next tag; returns |std::nullopt| once the tags are exhausted. + //! Must follow #ReadMessage. + std::optional<TTag> TryReadTag(); + +private: + const char* Current_; + const char* const End_; + bool MessageRead_ = false; + + //! Reads a ui32 length word. + ui32 ReadLength(); + //! Reads #size bytes as a view. + TStringBuf ReadBytes(ui32 size); + //! Reads a length-prefixed string (a length word followed by its bytes). + TStringBuf ReadString(); +}; + +//////////////////////////////////////////////////////////////////////////////// + +//! Producer convenience: builds a payload carrying only #message and no tags. +TTaggedLogEventPayload MakeTaggedPayloadFromMessage(TStringBuf message); + +//! Consumer convenience: returns the message, disregarding any tags. The result +//! views into #payload, which must outlive it. +TStringBuf GetMessageFromTaggedPayload(const TTaggedLogEventPayload& payload); + +//! Consumer convenience: renders the payload as |Message (Key: Value, ...)|, or just +//! the message if it carries no tags. +std::string FormatTaggedPayload(const TTaggedLogEventPayload& payload); + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NLogging + +#define TAGGED_PAYLOAD_INL_H_ +#include "tagged_payload-inl.h" +#undef TAGGED_PAYLOAD_INL_H_ diff --git a/library/cpp/yt/logging/unittests/helpers.cpp b/library/cpp/yt/logging/unittests/helpers.cpp new file mode 100644 index 00000000000..2a2b20120cc --- /dev/null +++ b/library/cpp/yt/logging/unittests/helpers.cpp @@ -0,0 +1,27 @@ +#include "helpers.h" + +namespace NYT::NLogging { + +//////////////////////////////////////////////////////////////////////////////// + +void WriteMessage(TTaggedPayloadWriter* writer, TStringBuf message) +{ + writer->BeginMessage()->AppendString(message); + writer->EndMessage(); +} + +void WriteTag(TTaggedPayloadWriter* writer, TStringBuf key, TStringBuf value) +{ + writer->BeginTag(key)->AppendString(value); + writer->EndTag(); +} + +void WriteWellKnownTag(TTaggedPayloadWriter* writer, TStringBuf key, TStringBuf value) +{ + writer->BeginWellKnownTag(key)->AppendString(value); + writer->EndTag(); +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/unittests/helpers.h b/library/cpp/yt/logging/unittests/helpers.h new file mode 100644 index 00000000000..6785a65f99b --- /dev/null +++ b/library/cpp/yt/logging/unittests/helpers.h @@ -0,0 +1,22 @@ +#pragma once + +#include <library/cpp/yt/logging/tagged_payload.h> + +#include <util/generic/strbuf.h> + +namespace NYT::NLogging { + +//////////////////////////////////////////////////////////////////////////////// + +//! Writes the message (equivalent to BeginMessage, an append, and EndMessage). +void WriteMessage(TTaggedPayloadWriter* writer, TStringBuf message); + +//! Writes a tag with a ready string value (equivalent to BeginTag, an append, and EndTag). +void WriteTag(TTaggedPayloadWriter* writer, TStringBuf key, TStringBuf value); + +//! Writes a well-known tag with a ready string value (see BeginWellKnownTag). +void WriteWellKnownTag(TTaggedPayloadWriter* writer, TStringBuf key, TStringBuf value); + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/unittests/logger_ut.cpp b/library/cpp/yt/logging/unittests/logger_ut.cpp index 410a56fc309..db176ca5394 100644 --- a/library/cpp/yt/logging/unittests/logger_ut.cpp +++ b/library/cpp/yt/logging/unittests/logger_ut.cpp @@ -1,15 +1,117 @@ #include <library/cpp/testing/gtest/gtest.h> #include <library/cpp/yt/logging/logger.h> +#include <library/cpp/yt/logging/tagged_payload.h> +#include <library/cpp/yt/logging/structured_payload.h> #include <library/cpp/yt/error/error.h> +#include <library/cpp/yt/yson_string/string.h> + +#include <atomic> +#include <string> +#include <utility> +#include <vector> + namespace NYT::NLogging { namespace { +using namespace NDetail; + +//////////////////////////////////////////////////////////////////////////////// + +//! A log manager that captures enqueued events for inspection in tests. +class TMockLogManager + : public ILogManager +{ +public: + explicit TMockLogManager(ELogLevel minLevel = ELogLevel::Minimum) + { + Category_.MinPlainTextLevel.store(minLevel); + } + + void RegisterStaticAnchor(TLoggingAnchor* anchor, ::TSourceLocation /*sourceLocation*/, TStringBuf /*message*/) override + { + anchor->Registered.store(true); + } + + void UpdateAnchor(TLoggingAnchor* anchor) override + { + anchor->CurrentVersion.store(ActualVersion_.load()); + } + + void Enqueue(TLogEvent&& event) override + { + Events_.push_back(std::move(event)); + } + + const TLoggingCategory* GetCategory(TStringBuf /*categoryName*/) override + { + return &Category_; + } + + void UpdateCategory(TLoggingCategory* category) override + { + category->CurrentVersion.store(ActualVersion_.load()); + } + + bool GetAbortOnAlert() const override + { + return false; + } + + const std::vector<TLogEvent>& GetEvents() const + { + return Events_; + } + +private: + std::vector<TLogEvent> Events_; + std::atomic<int> ActualVersion_ = 1; + TLoggingCategory Category_ = { + .Name = "Test", + .MinPlainTextLevel = ELogLevel::Minimum, + .CurrentVersion = 0, + .ActualVersion = &ActualVersion_, + }; +}; + +struct TDecodedEvent +{ + std::string Message; + std::vector<std::pair<std::string, std::string>> Tags; +}; + +TDecodedEvent DecodeEvent(const TLogEvent& event) +{ + TTaggedPayloadReader reader(std::get<TTaggedLogEventPayload>(event.Payload)); + TDecodedEvent result; + result.Message = reader.ReadMessage(); + while (auto tag = reader.TryReadTag()) { + result.Tags.emplace_back(tag->Key, tag->Value); + } + return result; +} + +TDecodedEvent DecodeSingleEvent(const TMockLogManager& manager) +{ + EXPECT_EQ(manager.GetEvents().size(), 1u); + return DecodeEvent(manager.GetEvents()[0]); +} + +NYson::TYsonString MakeMapFragment(TStringBuf yson) +{ + return NYson::TYsonString(yson, NYson::EYsonType::MapFragment); +} + +TStringBuf GetStructuredYson(const TLogEvent& event) +{ + return GetYsonFromStructuredPayload(std::get<TStructuredLogEventPayload>(event.Payload)).AsStringBuf(); +} + //////////////////////////////////////////////////////////////////////////////// -TEST(TLogger, NullByDefault) +TEST(TLoggerTest, NullByDefault) { { TLogger logger; @@ -23,7 +125,7 @@ TEST(TLogger, NullByDefault) } } -TEST(TLogger, CopyOfNullLogger) +TEST(TLoggerTest, CopyOfNullLogger) { TLogger nullLogger{/*logManager*/ nullptr, "Category"}; ASSERT_FALSE(nullLogger); @@ -34,7 +136,7 @@ TEST(TLogger, CopyOfNullLogger) EXPECT_FALSE(logger.IsLevelEnabled(ELogLevel::Fatal)); } -TEST(TLogger, LogAlertAndThrowMessage) +TEST(TLoggerTest, LogAlertAndThrowMessage) { try { TLogger Logger; @@ -52,5 +154,228 @@ TEST(TLogger, LogAlertAndThrowMessage) //////////////////////////////////////////////////////////////////////////////// +TEST(TTaggedApiTest, MessageOnly) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + YT_TLOG_INFO("Message"); + + auto decoded = DecodeSingleEvent(manager); + EXPECT_EQ(decoded.Message, "Message"); + EXPECT_TRUE(decoded.Tags.empty()); +} + +TEST(TTaggedApiTest, Tags) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + YT_TLOG_INFO("Message") + .With("Arg1", 123) + .With("Arg2", "test"); + + auto decoded = DecodeSingleEvent(manager); + EXPECT_EQ(decoded.Message, "Message"); + ASSERT_EQ(decoded.Tags.size(), 2u); + EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("123"))); + EXPECT_EQ(decoded.Tags[1], std::pair(std::string("Arg2"), std::string("test"))); +} + +TEST(TTaggedApiTest, CustomSpec) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + YT_TLOG_INFO("Message") + .With("Arg1", 256, "%x"); + + auto decoded = DecodeSingleEvent(manager); + ASSERT_EQ(decoded.Tags.size(), 1u); + EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("100"))); +} + +TEST(TTaggedApiTest, LoggerTagFoldedIntoMessage) +{ + TMockLogManager manager; + auto Logger = TLogger(&manager, "Test") + .WithTag("LoggingTag: %v", 555); + YT_TLOG_INFO("Message") + .With("Arg1", 123); + + auto decoded = DecodeSingleEvent(manager); + EXPECT_EQ(decoded.Message, "Message (LoggingTag: 555)"); + ASSERT_EQ(decoded.Tags.size(), 1u); + EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("123"))); +} + +TEST(TTaggedApiTest, DisabledDoesNotEvaluateTags) +{ + TMockLogManager manager(/*minLevel*/ ELogLevel::Warning); + TLogger Logger(&manager, "Test"); + + int evaluated = 0; + auto evaluate = [&] { + ++evaluated; + return 123; + }; + YT_TLOG_INFO("Message") + .With("Arg1", evaluate()); + + EXPECT_EQ(evaluated, 0); + EXPECT_TRUE(manager.GetEvents().empty()); +} + +TEST(TTaggedApiTest, WellKnownErrorTag) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + auto error = TError("boom"); + YT_TLOG_INFO("Message") + .With("Tag", 1) + .With(error); + + ASSERT_EQ(manager.GetEvents().size(), 1u); + TTaggedPayloadReader reader(std::get<TTaggedLogEventPayload>(manager.GetEvents()[0].Payload)); + EXPECT_EQ(reader.ReadMessage(), "Message"); + + auto regular = reader.TryReadTag(); + ASSERT_TRUE(regular); + EXPECT_EQ(regular->Key, "Tag"); + EXPECT_EQ(regular->Value, "1"); + EXPECT_FALSE(regular->IsWellKnown); + + // |.With(error)| attaches the error under the well-known "Error" key (resolved via + // GetWellKnownLoggingTag), with the formatted error as the value. + auto errorTag = reader.TryReadTag(); + ASSERT_TRUE(errorTag); + EXPECT_EQ(errorTag->Key, "Error"); + EXPECT_TRUE(errorTag->IsWellKnown); + EXPECT_NE(errorTag->Value.find("boom"), TStringBuf::npos); + + EXPECT_FALSE(reader.TryReadTag()); +} + +// The single-pass formatter assumes well-known tags come last, so the fluent API must +// forbid a keyed tag after a well-known one (e.g. |.With(error).With("Key", 1)|) at +// compile time. +template <class TGuard> +concept CAllowsKeyedTagAfterWellKnown = requires (TGuard guard, TError error) { + guard.With(error).With("Key", 1); +}; + +// A further well-known tag after a well-known one stays allowed. +template <class TGuard> +concept CAllowsWellKnownTagAfterWellKnown = requires (TGuard guard, TError error) { + guard.With(error).With(error); +}; + +static_assert(!CAllowsKeyedTagAfterWellKnown<TTaggedLoggingGuard>); +static_assert(CAllowsWellKnownTagAfterWellKnown<TTaggedLoggingGuard>); + +TEST(TTaggedApiTest, AlertAndThrow) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + + try { + YT_TLOG_ALERT_AND_THROW("Alert message") + .With("Arg1", 1) + .With("Arg2", 2); + EXPECT_TRUE(false); + } catch (const TErrorException& ex) { + const auto& error = ex.Error(); + EXPECT_EQ(error.GetCode(), NYT::EErrorCode::Fatal); + EXPECT_EQ(error.GetMessage(), "Malformed request or incorrect state detected"); + EXPECT_EQ(error.Attributes().Get<std::string>("message"), "Alert message"); + } + + // The alert is also logged, carrying the structured tags. + auto decoded = DecodeSingleEvent(manager); + EXPECT_EQ(decoded.Message, "Alert message"); + ASSERT_EQ(decoded.Tags.size(), 2u); + EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("1"))); + EXPECT_EQ(decoded.Tags[1], std::pair(std::string("Arg2"), std::string("2"))); +} + +TEST(TTaggedApiTest, AlertAndThrowDisabledStillThrows) +{ + TMockLogManager manager(/*minLevel*/ ELogLevel::Fatal); + TLogger Logger(&manager, "Test"); + + // The level is disabled, so nothing is logged, but the exception (with its message) + // is still raised. + EXPECT_THROW( + YT_TLOG_ALERT_AND_THROW("Alert message") + .With("Arg1", 1), + TErrorException); + EXPECT_TRUE(manager.GetEvents().empty()); +} + +TEST(TTaggedApiDeathTest, Fatal) +{ + EXPECT_DEATH({ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + YT_TLOG_FATAL("Fatal message") + .With("Arg1", 1); + }, "Fatal message \\(Arg1: 1\\)"); +} + +//////////////////////////////////////////////////////////////////////////////// + +TEST(TStructuredApiTest, StructuredEvent) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + + LogStructuredEvent(Logger, MakeMapFragment("\"key\"=\"value\""), ELogLevel::Info); + + ASSERT_EQ(manager.GetEvents().size(), 1u); + const auto& event = manager.GetEvents()[0]; + EXPECT_TRUE(std::holds_alternative<TStructuredLogEventPayload>(event.Payload)); + EXPECT_EQ(event.Family, ELogFamily::Structured); + EXPECT_EQ(event.Level, ELogLevel::Info); + // The payload is the raw YSON map fragment. + EXPECT_EQ(GetStructuredYson(event), "\"key\"=\"value\""); +} + +TEST(TStructuredApiTest, PreservesLevel) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + + LogStructuredEvent(Logger, MakeMapFragment("\"a\"=1;\"b\"=2"), ELogLevel::Warning); + + ASSERT_EQ(manager.GetEvents().size(), 1u); + EXPECT_EQ(manager.GetEvents()[0].Level, ELogLevel::Warning); + EXPECT_EQ(GetStructuredYson(manager.GetEvents()[0]), "\"a\"=1;\"b\"=2"); +} + +TEST(TStructuredApiTest, EmptyFragment) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + + LogStructuredEvent(Logger, MakeMapFragment(""), ELogLevel::Info); + + ASSERT_EQ(manager.GetEvents().size(), 1u); + EXPECT_EQ(GetStructuredYson(manager.GetEvents()[0]), ""); +} + +TEST(TStructuredApiTest, MultipleEvents) +{ + TMockLogManager manager; + TLogger Logger(&manager, "Test"); + + LogStructuredEvent(Logger, MakeMapFragment("\"i\"=0"), ELogLevel::Debug); + LogStructuredEvent(Logger, MakeMapFragment("\"i\"=1"), ELogLevel::Info); + + ASSERT_EQ(manager.GetEvents().size(), 2u); + EXPECT_EQ(manager.GetEvents()[0].Level, ELogLevel::Debug); + EXPECT_EQ(GetStructuredYson(manager.GetEvents()[0]), "\"i\"=0"); + EXPECT_EQ(manager.GetEvents()[1].Level, ELogLevel::Info); + EXPECT_EQ(GetStructuredYson(manager.GetEvents()[1]), "\"i\"=1"); +} + +//////////////////////////////////////////////////////////////////////////////// + } // namespace } // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/unittests/structured_payload_ut.cpp b/library/cpp/yt/logging/unittests/structured_payload_ut.cpp new file mode 100644 index 00000000000..f15970dcb23 --- /dev/null +++ b/library/cpp/yt/logging/unittests/structured_payload_ut.cpp @@ -0,0 +1,36 @@ +#include <library/cpp/testing/gtest/gtest.h> + +#include <library/cpp/yt/logging/structured_payload.h> + +#include <library/cpp/yt/yson_string/string.h> + +namespace NYT::NLogging { +namespace { + +using namespace NYson; + +//////////////////////////////////////////////////////////////////////////////// + +TYsonString MakeMapFragment(TStringBuf yson) +{ + return TYsonString(yson, EYsonType::MapFragment); +} + +TEST(TStructuredPayloadTest, RoundTrip) +{ + auto payload = MakeStructuredPayloadFromYson(MakeMapFragment("\"key\"=\"value\"")); + auto view = GetYsonFromStructuredPayload(payload); + EXPECT_EQ(view.GetType(), EYsonType::MapFragment); + EXPECT_EQ(view.AsStringBuf(), "\"key\"=\"value\""); +} + +TEST(TStructuredPayloadTest, EmptyFragment) +{ + auto payload = MakeStructuredPayloadFromYson(MakeMapFragment("")); + EXPECT_EQ(GetYsonFromStructuredPayload(payload).AsStringBuf(), ""); +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace +} // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/unittests/tagged_payload_ut.cpp b/library/cpp/yt/logging/unittests/tagged_payload_ut.cpp new file mode 100644 index 00000000000..57d9ac168f4 --- /dev/null +++ b/library/cpp/yt/logging/unittests/tagged_payload_ut.cpp @@ -0,0 +1,146 @@ +#include "helpers.h" + +#include <library/cpp/testing/gtest/gtest.h> + +#include <library/cpp/yt/logging/tagged_payload.h> + +#include <string> +#include <vector> + +namespace NYT::NLogging { +namespace { + +using namespace NDetail; + +//////////////////////////////////////////////////////////////////////////////// + +struct TDecodedPayload +{ + std::string Message; + std::vector<std::pair<std::string, std::string>> Tags; +}; + +TTaggedLogEventPayload Encode(TStringBuf message, const std::vector<std::pair<TStringBuf, TStringBuf>>& tags = {}) +{ + TTaggedPayloadWriter writer; + WriteMessage(&writer, message); + for (auto [key, value] : tags) { + WriteTag(&writer, key, value); + } + return writer.Finish(); +} + +TDecodedPayload Decode(const TTaggedLogEventPayload& payload) +{ + TTaggedPayloadReader reader(payload); + TDecodedPayload result; + result.Message = reader.ReadMessage(); + while (auto tag = reader.TryReadTag()) { + result.Tags.emplace_back(tag->Key, tag->Value); + } + return result; +} + +TEST(TTaggedPayloadTest, MessageOnly) +{ + auto decoded = Decode(Encode("Hello")); + EXPECT_EQ(decoded.Message, "Hello"); + EXPECT_TRUE(decoded.Tags.empty()); +} + +TEST(TTaggedPayloadTest, EmptyMessage) +{ + auto decoded = Decode(Encode("")); + EXPECT_EQ(decoded.Message, ""); + EXPECT_TRUE(decoded.Tags.empty()); +} + +TEST(TTaggedPayloadTest, OneTag) +{ + auto decoded = Decode(Encode("Message", {{"Key", "Value"}})); + EXPECT_EQ(decoded.Message, "Message"); + ASSERT_EQ(decoded.Tags.size(), 1u); + EXPECT_EQ(decoded.Tags[0].first, "Key"); + EXPECT_EQ(decoded.Tags[0].second, "Value"); +} + +TEST(TTaggedPayloadTest, ManyTags) +{ + auto decoded = Decode(Encode("Message", {{"Arg1", "123"}, {"Arg2", "test"}, {"Arg3", ""}})); + EXPECT_EQ(decoded.Message, "Message"); + ASSERT_EQ(decoded.Tags.size(), 3u); + EXPECT_EQ(decoded.Tags[0], std::pair(std::string("Arg1"), std::string("123"))); + EXPECT_EQ(decoded.Tags[1], std::pair(std::string("Arg2"), std::string("test"))); + EXPECT_EQ(decoded.Tags[2], std::pair(std::string("Arg3"), std::string(""))); +} + +TEST(TTaggedPayloadTest, BinarySafeValues) +{ + // Keys/values may carry arbitrary bytes, including embedded NULs and delimiters. + std::string message("a\0b ()", 6); + std::string key("k\0:", 3); + std::string value("v\0, ", 4); + + auto decoded = Decode(Encode(message, {{key, value}})); + EXPECT_EQ(decoded.Message, message); + ASSERT_EQ(decoded.Tags.size(), 1u); + EXPECT_EQ(decoded.Tags[0].first, key); + EXPECT_EQ(decoded.Tags[0].second, value); +} + +TEST(TTaggedPayloadTest, ReaderViewsPointIntoPayload) +{ + auto payload = Encode("Message", {{"Key", "Value"}}); + + auto pointsInto = [&] (TStringBuf view) { + return view.data() >= payload.Underlying().Begin() && view.data() + view.size() <= payload.Underlying().End(); + }; + + TTaggedPayloadReader reader(payload); + EXPECT_TRUE(pointsInto(reader.ReadMessage())); + auto tag = reader.TryReadTag(); + ASSERT_TRUE(tag.has_value()); + EXPECT_TRUE(pointsInto(tag->Key)); + EXPECT_TRUE(pointsInto(tag->Value)); +} + +TEST(TTaggedPayloadTest, WellKnownTag) +{ + TTaggedPayloadWriter writer; + WriteMessage(&writer, "Message"); + WriteTag(&writer, "Key", "Value"); + WriteWellKnownTag(&writer, "Error", "boom"); + auto payload = writer.Finish(); + + TTaggedPayloadReader reader(payload); + EXPECT_EQ(reader.ReadMessage(), "Message"); + + auto regular = reader.TryReadTag(); + ASSERT_TRUE(regular); + EXPECT_EQ(regular->Key, "Key"); + EXPECT_EQ(regular->Value, "Value"); + EXPECT_FALSE(regular->IsWellKnown); + + auto wellKnown = reader.TryReadTag(); + ASSERT_TRUE(wellKnown); + EXPECT_EQ(wellKnown->Key, "Error"); + EXPECT_EQ(wellKnown->Value, "boom"); + EXPECT_TRUE(wellKnown->IsWellKnown); + + EXPECT_FALSE(reader.TryReadTag()); +} + +TEST(TTaggedPayloadTest, FormatWellKnownTagTrailing) +{ + TTaggedPayloadWriter writer; + WriteMessage(&writer, "Message"); + WriteTag(&writer, "Key", "Value"); + WriteWellKnownTag(&writer, "Error", "boom"); + // Regular tags stay inline; the well-known tag is appended after the |(...)| group. + EXPECT_EQ(FormatTaggedPayload(writer.Finish()), "Message (Key: Value)\nboom"); +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace +} // namespace NYT::NLogging diff --git a/library/cpp/yt/logging/unittests/ya.make b/library/cpp/yt/logging/unittests/ya.make index 01946699bb2..cc068fa24c6 100644 --- a/library/cpp/yt/logging/unittests/ya.make +++ b/library/cpp/yt/logging/unittests/ya.make @@ -3,7 +3,10 @@ GTEST(unittester-library-logging) INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc) SRCS( + helpers.cpp logger_ut.cpp + tagged_payload_ut.cpp + structured_payload_ut.cpp static_analysis_ut.cpp ) diff --git a/library/cpp/yt/logging/ya.make b/library/cpp/yt/logging/ya.make index b9cf5c6e715..d839a6f07b7 100644 --- a/library/cpp/yt/logging/ya.make +++ b/library/cpp/yt/logging/ya.make @@ -4,6 +4,8 @@ INCLUDE(${ARCADIA_ROOT}/library/cpp/yt/ya_cpp.make.inc) SRCS( logger.cpp + tagged_payload.cpp + structured_payload.cpp ) PEERDIR( diff --git a/library/cpp/yt/memory/ref_counted-inl.h b/library/cpp/yt/memory/ref_counted-inl.h index 94acfc6669b..ce3c686ee28 100644 --- a/library/cpp/yt/memory/ref_counted-inl.h +++ b/library/cpp/yt/memory/ref_counted-inl.h @@ -124,6 +124,10 @@ Y_FORCE_INLINE void DestroyRefCountedImpl(T* obj) // No virtual call when T is final. obj->~T(); + // The ref-counter is a base subobject of *obj, so obj->~T() above poisons it + // under -fsanitize-memory-use-after-dtor. Unpoison before accessing it. + NSan::Unpoison(refCounter, sizeof(TRefCounter)); + // Fast path. Weak refs cannot appear if there are neither strong nor weak refs. if (refCounter->GetWeakRefCount() == 1) { NYT::NDetail::TMemoryReleaser<T>::Do(obj, offset); diff --git a/library/cpp/yt/memory/weak_ptr-inl.h b/library/cpp/yt/memory/weak_ptr-inl.h index 11026dc5fc6..0e5ad795774 100644 --- a/library/cpp/yt/memory/weak_ptr-inl.h +++ b/library/cpp/yt/memory/weak_ptr-inl.h @@ -4,6 +4,8 @@ #include "weak_ptr.h" #endif +#include <util/system/sanitizers.h> + namespace NYT { //////////////////////////////////////////////////////////////////////////////// @@ -16,8 +18,13 @@ template <class T> TWeakPtr<T>::TWeakPtr(T* p) noexcept : T_(p) { -#if defined(_tsan_enabled_) +#if defined(_tsan_enabled_) || defined(_msan_enabled_) if (T_) { + // p may legitimately point to an object whose destructor has already run + // while its ref-counter is still alive (weak count > 0). When T derives + // from TRefCounted virtually, the upcast below reads p's vptr, poisoned + // by -fsanitize-memory-use-after-dtor. Unpoison it (no-op for a live object). + NSan::Unpoison(T_, sizeof(void*)); RefCounter_ = GetRefCounter(T_); } #endif @@ -42,7 +49,7 @@ TWeakPtr<T>::TWeakPtr(const TIntrusivePtr<U>& ptr) noexcept template <class T> TWeakPtr<T>::TWeakPtr(const TWeakPtr& other) noexcept : T_(other.T_) -#if defined(_tsan_enabled_) +#if defined(_tsan_enabled_) || defined(_msan_enabled_) , RefCounter_(other.RefCounter_) #endif { @@ -77,7 +84,7 @@ TWeakPtr<T>::TWeakPtr(TWeakPtr<U>&& other) noexcept T_ = other.T_; other.T_ = nullptr; -#if defined(_tsan_enabled_) +#if defined(_tsan_enabled_) || defined(_msan_enabled_) RefCounter_ = other.RefCounter_; other.RefCounter_ = nullptr; #endif @@ -163,7 +170,7 @@ template <class T> void TWeakPtr<T>::Swap(TWeakPtr& other) noexcept { DoSwap(T_, other.T_); -#if defined(_tsan_enabled_) +#if defined(_tsan_enabled_) || defined(_msan_enabled_) DoSwap(RefCounter_, other.RefCounter_); #endif } @@ -208,12 +215,17 @@ void TWeakPtr<T>::ReleaseRef() if (T_) { // Support incomplete type. if (GetRefCounterImpl()->WeakUnref()) { + // The object's destructor has fully completed at this point + // and has poisoned T_'s vptr word under -fsanitize-memory-use-after-dtor. + // The upcast inside DeallocateRefCounted still needs that word + // to locate the virtual base. + NSan::Unpoison(T_, sizeof(void*)); DeallocateRefCounted(T_); } } } -#if defined(_tsan_enabled_) +#if defined(_tsan_enabled_) || defined(_msan_enabled_) template <class T> const TRefCounter* TWeakPtr<T>::GetRefCounterImpl() const { diff --git a/library/cpp/yt/memory/weak_ptr.h b/library/cpp/yt/memory/weak_ptr.h index d3598f62c74..c98abe15194 100644 --- a/library/cpp/yt/memory/weak_ptr.h +++ b/library/cpp/yt/memory/weak_ptr.h @@ -101,7 +101,7 @@ private: friend class TWeakPtr; T* T_ = nullptr; -#if defined(_tsan_enabled_) +#if defined(_tsan_enabled_) || defined(_msan_enabled_) const TRefCounter* RefCounter_ = nullptr; #endif |
