summaryrefslogtreecommitdiffstats
path: root/library/cpp
diff options
context:
space:
mode:
Diffstat (limited to 'library/cpp')
-rw-r--r--library/cpp/logger/deploy/README.md31
-rw-r--r--library/cpp/logger/deploy/backend.cpp84
-rw-r--r--library/cpp/logger/deploy/backend.h35
-rw-r--r--library/cpp/logger/deploy/ut/backend_ut.cpp134
-rw-r--r--library/cpp/logger/deploy/ut/ya.make13
-rw-r--r--library/cpp/logger/deploy/ya.make16
-rw-r--r--library/cpp/logger/ya.make1
-rw-r--r--library/cpp/tld/tlds-alpha-by-domain.txt3
-rw-r--r--library/cpp/yt/error/error_code.cpp30
-rw-r--r--library/cpp/yt/logging/backends/stream/unittests/stream_log_manager_ut.cpp2
-rw-r--r--library/cpp/yt/logging/logger-inl.h62
-rw-r--r--library/cpp/yt/logging/logger.h38
-rw-r--r--library/cpp/yt/logging/tag-inl.h114
-rw-r--r--library/cpp/yt/logging/tag.h60
-rw-r--r--library/cpp/yt/logging/tagged_payload-inl.h30
-rw-r--r--library/cpp/yt/logging/tagged_payload.h10
-rw-r--r--library/cpp/yt/logging/unittests/logger_ut.cpp125
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;