diff options
| author | robot-piglet <[email protected]> | 2026-07-22 16:22:33 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-07-22 17:56:37 +0300 |
| commit | ef41a76254f071ceffbc41003f485fc7504dd164 (patch) | |
| tree | da0db93c379a6e8bfde387b52493339d4daa6b4c | |
| parent | 91efd6f7555841500fdf2170ffeabf07592b5203 (diff) | |
Intermediate changes
commit_hash:8521bae955ae15bf7265f8bb58ebe126a62841ea
| -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 |
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 ) |
