diff options
Diffstat (limited to 'library/cpp')
| -rw-r--r-- | library/cpp/logger/deploy/README.md | 31 | ||||
| -rw-r--r-- | library/cpp/logger/deploy/backend.cpp | 84 | ||||
| -rw-r--r-- | library/cpp/logger/deploy/backend.h | 35 | ||||
| -rw-r--r-- | library/cpp/logger/deploy/ut/backend_ut.cpp | 134 | ||||
| -rw-r--r-- | library/cpp/logger/deploy/ut/ya.make | 13 | ||||
| -rw-r--r-- | library/cpp/logger/deploy/ya.make | 16 | ||||
| -rw-r--r-- | library/cpp/logger/ya.make | 1 | ||||
| -rw-r--r-- | library/cpp/tld/tlds-alpha-by-domain.txt | 3 | ||||
| -rw-r--r-- | library/cpp/yt/error/error_code.cpp | 30 | ||||
| -rw-r--r-- | library/cpp/yt/logging/backends/stream/unittests/stream_log_manager_ut.cpp | 2 | ||||
| -rw-r--r-- | library/cpp/yt/logging/logger-inl.h | 62 | ||||
| -rw-r--r-- | library/cpp/yt/logging/logger.h | 38 | ||||
| -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 |
17 files changed, 725 insertions, 63 deletions
diff --git a/library/cpp/logger/deploy/README.md b/library/cpp/logger/deploy/README.md new file mode 100644 index 00000000000..dc19f4bf90a --- /dev/null +++ b/library/cpp/logger/deploy/README.md @@ -0,0 +1,31 @@ +# Deploy JSON backend для `library/cpp/logger` + +Пишет логи в [формате Yandex Deploy](https://deploy.yandex-team.ru/docs/logs/format) +(JSON-строки для Unified Agent / Monitoring). + +Аналоги: Python `library/python/deploy_formatter`, Go `library/go/core/log/zap` +(`NewDeployLogger`), TypeScript `yandex-logger` (deploy stream). + +## Использование + +```cpp +#include <library/cpp/logger/deploy/backend.h> +#include <library/cpp/logger/log.h> +#include <library/cpp/logger/stream.h> + +TLog log(MakeHolder<TDeployJsonLogBackend>( + MakeHolder<TStreamLogBackend>(&Cout), + "my-service")); + +TLogElement(&log, TLOG_INFO) + .With("request_id", "abc") + .With("code", "200") + << "hello"; +``` + +```json +{"@timestamp":"...","levelStr":"INFO","message":"hello","loggerName":"my-service","request_id":"abc","@fields":{"code":"200"}} +``` + +`loggerName` задаётся в конструкторе. Через `With(...)` в корень JSON попадают +`request_id`, `user_id`, `stackTrace`, `threadName`; остальное — в `@fields`. diff --git a/library/cpp/logger/deploy/backend.cpp b/library/cpp/logger/deploy/backend.cpp new file mode 100644 index 00000000000..4cd700c8080 --- /dev/null +++ b/library/cpp/logger/deploy/backend.cpp @@ -0,0 +1,84 @@ +#include "backend.h" + +#include <library/cpp/json/writer/json.h> + +#include <util/datetime/base.h> +#include <util/generic/algorithm.h> +#include <util/generic/hash.h> +#include <util/string/cast.h> + +namespace { + // Keys with dedicated root getters in Unified Agent PayloadAttributesProvider + // (logbroker/unified_agent/plugins/yd_module/lib/attributes.*). + bool IsTopLevelMetaKey(TStringBuf key) noexcept { + return EqualToOneOf( + key, + "request_id", + "user_id", + "stackTrace", + "threadName"); + } +} // namespace + +TString FormatDeployJsonLogRecord(const TLogRecord& rec, TStringBuf loggerName) { + NJsonWriter::TBuf buf; + buf.BeginObject(); + buf.WriteKey("@timestamp").WriteString(TInstant::Now().ToString()); + buf.WriteKey("levelStr").WriteString(ToString(rec.Priority)); + + TStringBuf message(rec.Data, rec.Len); + message.ChopSuffix(TStringBuf("\n")); + buf.WriteKey("message").WriteString(message); + + if (loggerName) { + buf.WriteKey("loggerName").WriteString(loggerName); + } + + // TLogElement::With appends MetaFlags, so keys may repeat; last value wins. + THashMap<TStringBuf, TStringBuf> meta; + for (const auto& [key, value] : rec.MetaFlags) { + meta[key] = value; + } + + for (const auto& [key, value] : meta) { + if (IsTopLevelMetaKey(key)) { + buf.WriteKey(key).WriteString(value); + } + } + + bool openedFields = false; + for (const auto& [key, value] : meta) { + if (key == "loggerName" || IsTopLevelMetaKey(key)) { + continue; + } + if (!openedFields) { + buf.WriteKey("@fields"); + buf.BeginObject(); + openedFields = true; + } + buf.WriteKey(key).WriteString(value); + } + if (openedFields) { + buf.EndObject(); + } + + buf.EndObject(); + return buf.Str() + '\n'; +} + +TDeployJsonLogBackend::TDeployJsonLogBackend( + THolder<TLogBackend> slave, + TString loggerName) + : Slave_(std::move(slave)) + , LoggerName_(std::move(loggerName)) +{ +} + +void TDeployJsonLogBackend::WriteData(const TLogRecord& rec) { + const TString formatted = FormatDeployJsonLogRecord(rec, LoggerName_); + Slave_->WriteData(TLogRecord(rec.Priority, formatted.data(), formatted.size())); +} + +void TDeployJsonLogBackend::ReopenLog() { + Slave_->ReopenLog(); +} diff --git a/library/cpp/logger/deploy/backend.h b/library/cpp/logger/deploy/backend.h new file mode 100644 index 00000000000..f9d6e71e274 --- /dev/null +++ b/library/cpp/logger/deploy/backend.h @@ -0,0 +1,35 @@ +#pragma once + +#include <library/cpp/logger/backend.h> +#include <library/cpp/logger/priority.h> +#include <library/cpp/logger/record.h> + +#include <util/generic/ptr.h> +#include <util/generic/string.h> + +// Formats a single log record as one Deploy JSON line (with trailing '\n'). +// See https://deploy.yandex-team.ru/docs/logs/format +TString FormatDeployJsonLogRecord( + const TLogRecord& rec, + TStringBuf loggerName = {}); + +// Wraps a slave backend and rewrites each record as Deploy JSON. +// +// Must be a backend (not TLog::SetFormatter): MetaFlags are only available in +// TLogBackend::WriteData. +// +// Recommended stack (outer → inner): +// Filter → Thread → TDeployJsonLogBackend → sink +class TDeployJsonLogBackend: public TLogBackend { +public: + explicit TDeployJsonLogBackend( + THolder<TLogBackend> slave, + TString loggerName = {}); + + void WriteData(const TLogRecord& rec) override; + void ReopenLog() override; + +private: + THolder<TLogBackend> Slave_; + TString LoggerName_; +}; diff --git a/library/cpp/logger/deploy/ut/backend_ut.cpp b/library/cpp/logger/deploy/ut/backend_ut.cpp new file mode 100644 index 00000000000..011e726c877 --- /dev/null +++ b/library/cpp/logger/deploy/ut/backend_ut.cpp @@ -0,0 +1,134 @@ +#include <library/cpp/logger/deploy/backend.h> + +#include <library/cpp/json/json_reader.h> +#include <library/cpp/logger/element.h> +#include <library/cpp/logger/log.h> +#include <library/cpp/logger/stream.h> +#include <library/cpp/testing/unittest/registar.h> + +#include <util/stream/str.h> + +Y_UNIT_TEST_SUITE(DeployJsonLogBackendTest) { + Y_UNIT_TEST(FormatBasicRecord) { + const char* message = "hello"; + TLogRecord rec(TLOG_INFO, message, strlen(message)); + + NJson::TJsonValue json; + UNIT_ASSERT(NJson::ReadJsonTree(FormatDeployJsonLogRecord(rec, "test-logger"), &json)); + + UNIT_ASSERT(json.Has("@timestamp")); + UNIT_ASSERT_VALUES_EQUAL(json["levelStr"].GetStringSafe(), "INFO"); + UNIT_ASSERT(!json.Has("level")); + UNIT_ASSERT_VALUES_EQUAL(json["message"].GetStringSafe(), "hello"); + UNIT_ASSERT_VALUES_EQUAL(json["loggerName"].GetStringSafe(), "test-logger"); + UNIT_ASSERT(!json.Has("@fields")); + UNIT_ASSERT(!json.Has("threadName")); + } + + Y_UNIT_TEST(LevelStrUsesEnumSerialization) { + auto levelStrOf = [](ELogPriority priority) { + const char* message = "x"; + TLogRecord rec(priority, message, 1); + NJson::TJsonValue json; + UNIT_ASSERT(NJson::ReadJsonTree(FormatDeployJsonLogRecord(rec), &json)); + return json["levelStr"].GetStringSafe(); + }; + + UNIT_ASSERT_VALUES_EQUAL(levelStrOf(TLOG_EMERG), "EMERG"); + UNIT_ASSERT_VALUES_EQUAL(levelStrOf(TLOG_ALERT), "ALERT"); + UNIT_ASSERT_VALUES_EQUAL(levelStrOf(TLOG_CRIT), "CRITICAL_INFO"); + UNIT_ASSERT_VALUES_EQUAL(levelStrOf(TLOG_ERR), "ERROR"); + UNIT_ASSERT_VALUES_EQUAL(levelStrOf(TLOG_WARNING), "WARNING"); + UNIT_ASSERT_VALUES_EQUAL(levelStrOf(TLOG_NOTICE), "NOTICE"); + UNIT_ASSERT_VALUES_EQUAL(levelStrOf(TLOG_INFO), "INFO"); + UNIT_ASSERT_VALUES_EQUAL(levelStrOf(TLOG_DEBUG), "DEBUG"); + UNIT_ASSERT_VALUES_EQUAL(levelStrOf(TLOG_RESOURCES), "RESOURCES"); + } + + Y_UNIT_TEST(PromotesTopLevelMetaAndKeepsFields) { + const char* message = "RequestStats"; + TLogRecord rec( + TLOG_INFO, + message, + strlen(message), + TLogRecord::TMetaFlags{ + {"request_id", "req-1"}, + {"trace.id", "trace-1"}, + {"code", "200"}, + }); + + NJson::TJsonValue json; + UNIT_ASSERT(NJson::ReadJsonTree(FormatDeployJsonLogRecord(rec), &json)); + + UNIT_ASSERT_VALUES_EQUAL(json["request_id"].GetStringSafe(), "req-1"); + UNIT_ASSERT(!json.Has("trace.id")); + UNIT_ASSERT_VALUES_EQUAL(json["@fields"]["trace.id"].GetStringSafe(), "trace-1"); + UNIT_ASSERT_VALUES_EQUAL(json["@fields"]["code"].GetStringSafe(), "200"); + UNIT_ASSERT(!json["@fields"].Has("request_id")); + } + + Y_UNIT_TEST(LoggerNameIgnoresMetaFlags) { + const char* message = "x"; + TLogRecord rec( + TLOG_INFO, + message, + 1, + TLogRecord::TMetaFlags{{"loggerName", "from-meta"}}); + + NJson::TJsonValue json; + UNIT_ASSERT(NJson::ReadJsonTree(FormatDeployJsonLogRecord(rec, "from-backend"), &json)); + + UNIT_ASSERT_VALUES_EQUAL(json["loggerName"].GetStringSafe(), "from-backend"); + UNIT_ASSERT(!json.Has("@fields")); + } + + Y_UNIT_TEST(MetaFlagsLastWins) { + const char* message = "dup"; + TLogRecord rec( + TLOG_INFO, + message, + strlen(message), + TLogRecord::TMetaFlags{ + {"file", "first.cpp"}, + {"request_id", "old"}, + {"file", "second.cpp"}, + {"request_id", "new"}, + }); + + const TString formatted = FormatDeployJsonLogRecord(rec); + NJson::TJsonValue json; + UNIT_ASSERT(NJson::ReadJsonTree(formatted, &json)); + + UNIT_ASSERT_VALUES_EQUAL(json["request_id"].GetStringSafe(), "new"); + UNIT_ASSERT_VALUES_EQUAL(json["@fields"]["file"].GetStringSafe(), "second.cpp"); + // NJson is last-wins too; assert the key was emitted once. + UNIT_ASSERT_VALUES_EQUAL(formatted.find("\"file\""), formatted.rfind("\"file\"")); + } + + Y_UNIT_TEST(StripsTrailingNewlineFromMessage) { + const char* message = "line\n"; + TLogRecord rec(TLOG_INFO, message, strlen(message)); + + NJson::TJsonValue json; + UNIT_ASSERT(NJson::ReadJsonTree(FormatDeployJsonLogRecord(rec), &json)); + UNIT_ASSERT_VALUES_EQUAL(json["message"].GetStringSafe(), "line"); + } + + Y_UNIT_TEST(WritesThroughSlaveBackend) { + TStringStream output; + TLog log(MakeHolder<TDeployJsonLogBackend>( + MakeHolder<TStreamLogBackend>(&output), + "suite")); + + { + TLogElement element(&log, TLOG_WARNING); + element.With("request_id", "req-1") << "warn"; + } + + NJson::TJsonValue json; + UNIT_ASSERT(NJson::ReadJsonTree(output.Str(), &json)); + UNIT_ASSERT_VALUES_EQUAL(json["message"].GetStringSafe(), "warn"); + UNIT_ASSERT_VALUES_EQUAL(json["request_id"].GetStringSafe(), "req-1"); + UNIT_ASSERT_VALUES_EQUAL(json["loggerName"].GetStringSafe(), "suite"); + } +} // Y_UNIT_TEST_SUITE(DeployJsonLogBackendTest) diff --git a/library/cpp/logger/deploy/ut/ya.make b/library/cpp/logger/deploy/ut/ya.make new file mode 100644 index 00000000000..8c6c95be5da --- /dev/null +++ b/library/cpp/logger/deploy/ut/ya.make @@ -0,0 +1,13 @@ +UNITTEST() + +PEERDIR( + library/cpp/json + library/cpp/logger + library/cpp/logger/deploy +) + +SRCS( + backend_ut.cpp +) + +END() diff --git a/library/cpp/logger/deploy/ya.make b/library/cpp/logger/deploy/ya.make new file mode 100644 index 00000000000..3d8d860c2ce --- /dev/null +++ b/library/cpp/logger/deploy/ya.make @@ -0,0 +1,16 @@ +LIBRARY() + +PEERDIR( + library/cpp/json/writer + library/cpp/logger +) + +SRCS( + backend.cpp +) + +END() + +RECURSE_FOR_TESTS( + ut +) diff --git a/library/cpp/logger/ya.make b/library/cpp/logger/ya.make index 07c269faff1..4e4105caf8b 100644 --- a/library/cpp/logger/ya.make +++ b/library/cpp/logger/ya.make @@ -39,6 +39,7 @@ SRCS( END() RECURSE( + deploy global ut ) diff --git a/library/cpp/tld/tlds-alpha-by-domain.txt b/library/cpp/tld/tlds-alpha-by-domain.txt index d14bb7480c9..250d46a816d 100644 --- a/library/cpp/tld/tlds-alpha-by-domain.txt +++ b/library/cpp/tld/tlds-alpha-by-domain.txt @@ -1,4 +1,4 @@ -# Version 2026062302, Last Updated Wed Jun 24 07:07:01 2026 UTC +# Version 2026072401, Last Updated Fri Jul 24 18:55:10 2026 UTC AAA AARP ABB @@ -1233,6 +1233,7 @@ WATCH WATCHES WEATHER WEATHERCHANNEL +WEB WEBCAM WEBER WEBSITE diff --git a/library/cpp/yt/error/error_code.cpp b/library/cpp/yt/error/error_code.cpp index d82d03214d4..ef9ce0f33c1 100644 --- a/library/cpp/yt/error/error_code.cpp +++ b/library/cpp/yt/error/error_code.cpp @@ -62,11 +62,10 @@ void TErrorCodeRegistry::RegisterErrorCode(int code, const TErrorCodeInfo& error if (code == 119) { return; } - YT_LOG_FATAL( - "Duplicate error code (Code: %v, StoredCodeInfo: %v, NewCodeInfo: %v)", - code, - CodeToInfo_[code], - errorCodeInfo); + YT_TLOG_FATAL("Duplicate error code") + .With("Code", code) + .With("StoredCodeInfo", CodeToInfo_[code]) + .With("NewCodeInfo", errorCodeInfo); } } @@ -91,11 +90,11 @@ void TErrorCodeRegistry::RegisterErrorCodeRange(int from, int to, std::string na TErrorCodeRangeInfo newRange{from, to, std::move(namespaceName), std::move(formatter)}; for (const auto& range : ErrorCodeRanges_) { - YT_LOG_FATAL_IF( + YT_TLOG_FATAL_IF( range.Intersects(newRange), - "Intersecting error code ranges registered (FirstRange: %v, SecondRange: %v)", - range, - newRange); + "Intersecting error code ranges registered") + .With("FirstRange", range) + .With("SecondRange", newRange); } ErrorCodeRanges_.push_back(std::move(newRange)); CheckCodesAgainstRanges(); @@ -105,14 +104,13 @@ void TErrorCodeRegistry::CheckCodesAgainstRanges() const { for (const auto& [code, info] : CodeToInfo_) { for (const auto& range : ErrorCodeRanges_) { - YT_LOG_FATAL_IF( + YT_TLOG_FATAL_IF( range.Contains(code), - "Error code range contains another registered code " - "(Range: %v, Code: %v, RangeCodeInfo: %v, StandaloneCodeInfo: %v)", - range, - code, - range.Get(code), - info); + "Error code range contains another registered code") + .With("Range", range) + .With("Code", code) + .With("RangeCodeInfo", range.Get(code)) + .With("StandaloneCodeInfo", info); } } } diff --git a/library/cpp/yt/logging/backends/stream/unittests/stream_log_manager_ut.cpp b/library/cpp/yt/logging/backends/stream/unittests/stream_log_manager_ut.cpp index cb3e244e3b9..95b2ade9b65 100644 --- a/library/cpp/yt/logging/backends/stream/unittests/stream_log_manager_ut.cpp +++ b/library/cpp/yt/logging/backends/stream/unittests/stream_log_manager_ut.cpp @@ -20,7 +20,7 @@ TEST(TStreamLogManagerTest, Simple) TStringOutput output(str); auto logManager = CreateStreamLogManager(&output); TLogger Logger(logManager.get(), "Test"); - YT_LOG_INFO("Hello world"); + YT_TLOG_INFO("Hello world"); } TVector<TStringBuf> tokens; 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 0f29b6f7376..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,12 +504,12 @@ 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) -#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) +#define YT_TLOG_TRACE_IF(condition, message) if (!(condition)) { } else YT_TLOG_TRACE(message) +#define YT_TLOG_TRACE_UNLESS(condition, message) if (condition) { } else 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) @@ -512,24 +518,24 @@ void LogStructuredEvent( #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_DEBUG_IF(condition, message) if (!(condition)) { } else YT_TLOG_DEBUG(message) +#define YT_TLOG_DEBUG_UNLESS(condition, message) if (condition) { } else 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_INFO_IF(condition, message) if (!(condition)) { } else YT_TLOG_INFO(message) +#define YT_TLOG_INFO_UNLESS(condition, message) if (condition) { } else 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_WARNING_IF(condition, message) if (!(condition)) { } else YT_TLOG_WARNING(message) +#define YT_TLOG_WARNING_UNLESS(condition, message) if (condition) { } else 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_ERROR_IF(condition, message) if (!(condition)) { } else YT_TLOG_ERROR(message) +#define YT_TLOG_ERROR_UNLESS(condition, message) if (condition) { } else 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) +#define YT_TLOG_ALERT_IF(condition, message) if (!(condition)) { } else YT_TLOG_ALERT(message) +#define YT_TLOG_ALERT_UNLESS(condition, message) if (condition) { } else 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 @@ -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; |
