summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-07-22 16:22:33 +0300
committerrobot-piglet <[email protected]>2026-07-22 17:56:37 +0300
commitef41a76254f071ceffbc41003f485fc7504dd164 (patch)
treeda0db93c379a6e8bfde387b52493339d4daa6b4c
parent91efd6f7555841500fdf2170ffeabf07592b5203 (diff)
Intermediate changes
commit_hash:8521bae955ae15bf7265f8bb58ebe126a62841ea
-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
7 files changed, 314 insertions, 0 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
)