diff --git a/core/src/logging/component.cpp b/core/src/logging/component.cpp index 73ff46c4fb9c..6a55966881da 100644 --- a/core/src/logging/component.cpp +++ b/core/src/logging/component.cpp @@ -252,6 +252,7 @@ additionalProperties: false - tskv - ltsv - raw + - json flush_level: type: string description: messages of this and higher levels get flushed to the file immediately diff --git a/core/src/logging/json_string_test.cpp b/core/src/logging/json_string_test.cpp new file mode 100644 index 000000000000..4914890ff083 --- /dev/null +++ b/core/src/logging/json_string_test.cpp @@ -0,0 +1,44 @@ +#include + +#include + +#include + +USERVER_NAMESPACE_BEGIN + +TEST(JsonString, ConstructFromJson) { + using formats::literals::operator""_json; + + auto json = R"({ + "a": "foo", + "b": { + "c": "d", + "e": [ + 1, + 2 + ] + } + })"_json; + + logging::JsonString json_string(json); + + EXPECT_EQ(json_string.Value(), R"({"a":"foo","b":{"c":"d","e":[1,2]}})"); +} + +TEST(JsonString, ConstructFromString) { + std::string json = R"({"a":"foo", +"b":{"c":"d","e": +[1,2]}})"; + + logging::JsonString json_string(json); + + EXPECT_EQ(json_string.Value(), R"({"a":"foo","b":{"c":"d","e":[1,2]}})"); +} + +TEST(JsonString, ConstructNull) { + logging::JsonString json_string; + + EXPECT_EQ(json_string.Value(), R"(null)"); +} + +USERVER_NAMESPACE_END diff --git a/core/src/logging/log_message_test.cpp b/core/src/logging/log_message_test.cpp index 16020c4439fa..afa89ae083b2 100644 --- a/core/src/logging/log_message_test.cpp +++ b/core/src/logging/log_message_test.cpp @@ -67,6 +67,11 @@ TEST_F(LoggingTest, TskvEncode) { << "escaped sequence is present in the message"; } +TEST_F(LoggingJsonTest, JsonEncode) { + LOG_CRITICAL() << "line\"1\nline 2"; + EXPECT_THAT(GetStreamString(), testing::HasSubstr("line\\\"1\\nline 2")); +} + TEST_F(LoggingTest, TskvEncodeKeyWithDot) { logging::LogExtra le; le.Extend("http.port.ipv4", "4040"); @@ -76,6 +81,8 @@ TEST_F(LoggingTest, TskvEncodeKeyWithDot) { } TEST_F(LoggingTest, LogFormat) { + using formats::literals::operator""_json; + // Note: this is a golden test. The order and content of tags is stable, which // is an implementation detail, but it makes this test possible. If the order // or content of tags change, this test should be fixed to reflect the @@ -88,8 +95,40 @@ TEST_F(LoggingTest, LogFormat) { R"(task_id=[0-9A-F]+\t)" R"(thread_id=0x[0-9A-F]+\t)" R"(text=test\t)" - R"(foo=bar\n)"; - LOG_CRITICAL() << "test" << logging::LogExtra{{"foo", "bar"}}; + R"(foo=bar\t)" + R"(json={\"a\":\"foo\"}\n)"; + LOG_CRITICAL() << "test" << logging::LogExtra{{"foo", "bar"}} + << logging::LogExtra{{"json", R"({"a": "foo"})"_json}}; + logging::LogFlush(); + EXPECT_TRUE( + utils::regex_match(GetStreamString(), utils::regex(kExpectedPattern))) + << GetStreamString(); + + EXPECT_THAT(GetStreamString(), testing::Not(testing::HasSubstr(" ( /"))) + << "Path shortening for logs stopped working."; +} + +TEST_F(LoggingJsonTest, LogFormat) { + // Note: this is a golden test. The order and content of tags is stable, which + // is an implementation detail, but it makes this test possible. If the order + // or content of tags change, this test should be fixed to reflect the + // changes. + constexpr std::string_view kExpectedPattern = + R"({)" + R"("timestamp":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}",)" + R"("level":"[A-Z]+",)" + R"("module":"[\w\d ():./]+",)" + R"("task_id":"[0-9A-F]+",)" + R"("thread_id":"0x[0-9A-F]+",)" + R"("text":"test",)" + R"("str":"bar",)" + R"("int":"42",)" + R"("float":"0.123")" + R"(})" + R"(\n)"; + LOG_CRITICAL() << "test" << logging::LogExtra{{"str", "bar"}} + << logging::LogExtra{{"int", 42}} + << logging::LogExtra{{"float", 0.123f}}; logging::LogFlush(); EXPECT_TRUE( utils::regex_match(GetStreamString(), utils::regex(kExpectedPattern))) @@ -99,6 +138,82 @@ TEST_F(LoggingTest, LogFormat) { << "Path shortening for logs stopped working."; } +TEST_F(LoggingJsonTest, LogFormatEmptyTextNonemptyExtra) { + // Note: this is a golden test. The order and content of tags is stable, which + // is an implementation detail, but it makes this test possible. If the order + // or content of tags change, this test should be fixed to reflect the + // changes. + constexpr std::string_view kExpectedPattern = + R"({)" + R"("timestamp":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}",)" + R"("level":"[A-Z]+",)" + R"("module":"[\w\d ():./]+",)" + R"("task_id":"[0-9A-F]+",)" + R"("thread_id":"0x[0-9A-F]+",)" + R"("text":"",)" + R"("str":"bar",)" + R"("int":"42",)" + R"("float":"0.123")" + R"(})" + R"(\n)"; + LOG_CRITICAL() << logging::LogExtra{{"str", "bar"}} + << logging::LogExtra{{"int", 42}} + << logging::LogExtra{{"float", 0.123f}}; + logging::LogFlush(); + EXPECT_TRUE( + utils::regex_match(GetStreamString(), utils::regex(kExpectedPattern))) + << GetStreamString(); +} + +TEST_F(LoggingJsonTest, LogFormatEmptyTextEmptyExtra) { + // Note: this is a golden test. The order and content of tags is stable, which + // is an implementation detail, but it makes this test possible. If the order + // or content of tags change, this test should be fixed to reflect the + // changes. + constexpr std::string_view kExpectedPattern = + R"({)" + R"("timestamp":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}",)" + R"("level":"[A-Z]+",)" + R"("module":"[\w\d ():./]+",)" + R"("task_id":"[0-9A-F]+",)" + R"("thread_id":"0x[0-9A-F]+",)" + R"("text":"")" + R"(})" + R"(\n)"; + LOG_CRITICAL() << ""; + logging::LogFlush(); + EXPECT_TRUE( + utils::regex_match(GetStreamString(), utils::regex(kExpectedPattern))) + << GetStreamString(); +} + +TEST_F(LoggingJsonTest, LogFormatJsonExtra) { + using formats::literals::operator""_json; + + // Note: this is a golden test. The order and content of tags is stable, which + // is an implementation detail, but it makes this test possible. If the order + // or content of tags change, this test should be fixed to reflect the + // changes. + constexpr std::string_view kExpectedPattern = + R"({)" + R"("timestamp":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}",)" + R"("level":"[A-Z]+",)" + R"("module":"[\w\d ():./]+",)" + R"("task_id":"[0-9A-F]+",)" + R"("thread_id":"0x[0-9A-F]+",)" + R"("text":"",)" + R"("json1":{"a":"foo"},)" + R"("json2":{"b":"bar"})" + R"(})" + R"(\n)"; + LOG_CRITICAL() << logging::LogExtra{{"json1", R"({"a": "foo"})"_json}} + << logging::LogExtra{{"json2", R"({"b": "bar"})"_json}}; + logging::LogFlush(); + EXPECT_TRUE( + utils::regex_match(GetStreamString(), utils::regex(kExpectedPattern))) + << GetStreamString(); +} + TEST_F(LoggingTest, FloatingPoint) { constexpr float f = 3.1415F; EXPECT_EQ(ToStringViaLogging(f), ToStringViaStreams(f)); diff --git a/core/src/logging/logging_test.hpp b/core/src/logging/logging_test.hpp index 0fe19a799984..25468d051749 100644 --- a/core/src/logging/logging_test.hpp +++ b/core/src/logging/logging_test.hpp @@ -136,4 +136,11 @@ class LoggingLtsvTest : public LoggingTestBase { } }; +class LoggingJsonTest : public LoggingTestBase { + protected: + LoggingJsonTest() : LoggingTestBase(logging::Format::kJson) { + SetDefaultLogger(GetStreamLogger()); + } +}; + USERVER_NAMESPACE_END diff --git a/core/src/tracing/span_opentracing.cpp b/core/src/tracing/span_opentracing.cpp index 19c51755339c..44f6186fa0c7 100644 --- a/core/src/tracing/span_opentracing.cpp +++ b/core/src/tracing/span_opentracing.cpp @@ -42,22 +42,32 @@ constexpr utils::TrivialBiMap kGetOpentracingTags = [](auto selector) { }; struct LogExtraValueVisitor { - std::string string_value; + formats::json::StringBuilder& builder; - void operator()(const std::string& val) { string_value = val; } + void operator()(const std::string& val) { + builder.Key("value"); + builder.WriteString(val); + } + + void operator()(int val) { + builder.Key("value"); + builder.WriteString(std::to_string(val)); + } - void operator()(int val) { string_value = std::to_string(val); } + void operator()(const logging::JsonString& val) { + builder.Key("value"); + builder.WriteRawString(val.Value()); + } }; void GetTagObject(formats::json::StringBuilder& builder, std::string_view key, const logging::LogExtra::Value& value, std::string_view type) { const formats::json::StringBuilder::ObjectGuard guard(builder); - LogExtraValueVisitor visitor; - std::visit(visitor, value); - builder.Key("value"); - builder.WriteString(visitor.string_value); + // writes `value` + LogExtraValueVisitor visitor{builder}; + std::visit(visitor, value); builder.Key("type"); builder.WriteString(type); diff --git a/universal/include/userver/logging/format.hpp b/universal/include/userver/logging/format.hpp index 57c6a520d8e7..b031cdca9ce6 100644 --- a/universal/include/userver/logging/format.hpp +++ b/universal/include/userver/logging/format.hpp @@ -10,7 +10,7 @@ USERVER_NAMESPACE_BEGIN namespace logging { /// Log formats -enum class Format { kTskv, kLtsv, kRaw }; +enum class Format { kTskv, kLtsv, kRaw, kJson }; /// Parse Format enum from string Format FormatFromString(std::string_view format_str); diff --git a/universal/include/userver/logging/impl/tag_writer.hpp b/universal/include/userver/logging/impl/tag_writer.hpp index 443db1b673d5..f05e780785b2 100644 --- a/universal/include/userver/logging/impl/tag_writer.hpp +++ b/universal/include/userver/logging/impl/tag_writer.hpp @@ -42,9 +42,13 @@ class TagWriter { template void PutTag(TagKey key, const T& value); + void PutTag(TagKey key, const JsonString& value); + template void PutTag(RuntimeTagKey key, const T& value); + void PutTag(RuntimeTagKey key, const JsonString& value); + // The tags must not be duplicated in other Put* calls. void PutLogExtra(const LogExtra& extra); @@ -62,6 +66,8 @@ class TagWriter { void MarkValueEnd() noexcept; + void PutOptionalOpenCloseSeparator(); + LogHelper& lh_; }; @@ -88,14 +94,18 @@ USERVER_IMPL_CONSTEVAL TagKey::TagKey(const StringType& escaped_key) template void TagWriter::PutTag(TagKey key, const T& value) { PutKey(key); + PutOptionalOpenCloseSeparator(); lh_ << value; + PutOptionalOpenCloseSeparator(); MarkValueEnd(); } template void TagWriter::PutTag(RuntimeTagKey key, const T& value) { PutKey(key); + PutOptionalOpenCloseSeparator(); lh_ << value; + PutOptionalOpenCloseSeparator(); MarkValueEnd(); } diff --git a/universal/include/userver/logging/json_string.hpp b/universal/include/userver/logging/json_string.hpp new file mode 100644 index 000000000000..a57e96c3b600 --- /dev/null +++ b/universal/include/userver/logging/json_string.hpp @@ -0,0 +1,45 @@ +#pragma once + +/// @file userver/logging/json_string.hpp +/// @brief @copybrief logging::JsonString + +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace logging { + +/// One line json string. +class JsonString { + public: + /// @brief Constructs from provided json object. + /// The generated json string is an one line string. + JsonString(const formats::json::Value& value); + + /// @brief Constructs from provided json string. It is the user's + /// responsibility to ensure that the input json string is valid. + /// New lines will be removed during construction. + explicit JsonString(std::string json) noexcept; + + /// @brief Constructs "null" + JsonString() noexcept = default; + + JsonString(JsonString&&) noexcept = default; + JsonString(const JsonString&) = default; + + JsonString& operator=(JsonString&&) noexcept = default; + JsonString& operator=(const JsonString&) = default; + + /// @brief Returns view to json + std::string_view Value() const; + + private: + std::string json_; +}; + +} // namespace logging + +USERVER_NAMESPACE_END diff --git a/universal/include/userver/logging/log_extra.hpp b/universal/include/userver/logging/log_extra.hpp index 493b1ac40afc..c6a955e5cc85 100644 --- a/universal/include/userver/logging/log_extra.hpp +++ b/universal/include/userver/logging/log_extra.hpp @@ -12,6 +12,7 @@ #include #include +#include #include USERVER_NAMESPACE_BEGIN @@ -32,7 +33,8 @@ class TagWriter; class LogExtra final { public: using Value = std::variant; + unsigned long, unsigned long long, float, double, + JsonString>; using Key = std::string; using Pair = std::pair; diff --git a/universal/include/userver/logging/log_helper.hpp b/universal/include/userver/logging/log_helper.hpp index 54fdc0ddd9f9..122f425fb0c2 100644 --- a/universal/include/userver/logging/log_helper.hpp +++ b/universal/include/userver/logging/log_helper.hpp @@ -146,8 +146,6 @@ class LogHelper final { /// Extends internal LogExtra LogHelper& operator<<(LogExtra&& extra) noexcept; - LogHelper& operator<<(const LogExtra::Value& value) noexcept; - LogHelper& operator<<(Hex hex) noexcept; LogHelper& operator<<(HexShort hex) noexcept; diff --git a/universal/src/logging/encoding_json.cpp b/universal/src/logging/encoding_json.cpp new file mode 100644 index 000000000000..f5d68317a3c9 --- /dev/null +++ b/universal/src/logging/encoding_json.cpp @@ -0,0 +1,50 @@ +#include "encoding_json.hpp" + +#include + +USERVER_NAMESPACE_BEGIN + +namespace logging { + +namespace { +class LogBufferStreamWrapper { + public: + using Ch = LogBuffer::value_type; + + explicit LogBufferStreamWrapper(LogBuffer& lb, bool skip_first_quote) + : lb_{lb}, skip_first_quote_{skip_first_quote} {} + + void Put(Ch ch) { + if (!std::exchange(skip_first_quote_, false)) { + lb_.push_back(ch); + } + } + + void Reserve(size_t count) { lb_.reserve(lb_.size() + count); } + + void Flush() {} + + private: + LogBuffer& lb_; + bool skip_first_quote_; +}; + +void PutReserve(LogBufferStreamWrapper& stream, size_t count) { + stream.Reserve(count); +} +} // namespace + +void EncodeJson(LogBuffer& lb, std::string_view string) { + // NOTE: rapidjson::Writer adds leading and trailing quotes + LogBufferStreamWrapper output_stream(lb, /*skip_first_quote=*/true); + rapidjson::Writer writer(output_stream); + + writer.String(string.data(), string.size()); + + // skip last '"" + lb.resize(lb.size() - 1); +} + +} // namespace logging + +USERVER_NAMESPACE_END diff --git a/universal/src/logging/encoding_json.hpp b/universal/src/logging/encoding_json.hpp new file mode 100644 index 000000000000..d172a0458e5e --- /dev/null +++ b/universal/src/logging/encoding_json.hpp @@ -0,0 +1,12 @@ +#include "log_helper_impl.hpp" + +USERVER_NAMESPACE_BEGIN + +namespace logging { + +// Encode string for json format +void EncodeJson(LogBuffer& lb, std::string_view string); + +} // namespace logging + +USERVER_NAMESPACE_END diff --git a/universal/src/logging/format.cpp b/universal/src/logging/format.cpp index e4a4842fb7f2..bc431f080800 100644 --- a/universal/src/logging/format.cpp +++ b/universal/src/logging/format.cpp @@ -23,6 +23,10 @@ Format FormatFromString(std::string_view format_str) { return Format::kRaw; } + if (format_str == "json") { + return Format::kJson; + } + UINVARIANT( false, fmt::format("Unknown logging format '{}' (must be one of 'tskv', 'ltsv')", diff --git a/universal/src/logging/impl/tag_writer.cpp b/universal/src/logging/impl/tag_writer.cpp index 392db545cdf7..17d86cd65047 100644 --- a/universal/src/logging/impl/tag_writer.cpp +++ b/universal/src/logging/impl/tag_writer.cpp @@ -26,8 +26,10 @@ std::string_view RuntimeTagKey::GetUnescapedKey() const noexcept { } void TagWriter::PutLogExtra(const LogExtra& extra) { - for (const auto& item : *extra.extra_) { - PutTag(RuntimeTagKey{item.first}, item.second.GetValue()); + for (const auto& [key, value] : *extra.extra_) { + std::visit( + [key = &key, this](auto& value) { PutTag(RuntimeTagKey{*key}, value); }, + value.GetValue()); } } @@ -47,6 +49,22 @@ void TagWriter::PutKey(RuntimeTagKey key) { void TagWriter::MarkValueEnd() noexcept { lh_.pimpl_->MarkValueEnd(); } +void TagWriter::PutOptionalOpenCloseSeparator() { + lh_.pimpl_->PutOptionalOpenCloseSeparator(); +} + +void TagWriter::PutTag(TagKey key, const JsonString& value) { + PutKey(key); + lh_.pimpl_->WriteRawJsonValue(value.Value()); + MarkValueEnd(); +} + +void TagWriter::PutTag(RuntimeTagKey key, const JsonString& value) { + PutKey(key); + lh_.pimpl_->WriteRawJsonValue(value.Value()); + MarkValueEnd(); +} + } // namespace logging::impl USERVER_NAMESPACE_END diff --git a/universal/src/logging/json_string.cpp b/universal/src/logging/json_string.cpp new file mode 100644 index 000000000000..b00995ce8a00 --- /dev/null +++ b/universal/src/logging/json_string.cpp @@ -0,0 +1,50 @@ +#include "userver/logging/json_string.hpp" + +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace logging { + +namespace { +constexpr std::string_view kNull = "null"; + +bool DebugValid(std::string_view json) { + try { + formats::json::FromString(json); + } catch (const formats::json::ParseException& error) { + LOG_ERROR() << "Invalid json: " << json << ", error: " << error; + return false; + } + return true; +} + +} // namespace + +JsonString::JsonString(const formats::json::Value& value) + : json_{ToString(value)} { + // ToString builds one line string by RapidJson +} + +JsonString::JsonString(std::string json) noexcept : json_{std::move(json)} { + UASSERT(DebugValid(json_)); + + // Remove extra new lines from user provided json string + json_.erase(std::remove_if(json_.begin(), json_.end(), + [](auto ch) { return ch == '\n' || ch == '\r'; }), + json_.end()); +} + +std::string_view JsonString::Value() const { + if (json_.empty()) { + return kNull; + } + return json_; +} + +} // namespace logging + +USERVER_NAMESPACE_END diff --git a/universal/src/logging/log_helper.cpp b/universal/src/logging/log_helper.cpp index 02f436358694..ab68a00e6b5f 100644 --- a/universal/src/logging/log_helper.cpp +++ b/universal/src/logging/log_helper.cpp @@ -190,6 +190,10 @@ void LogHelper::DoLog() noexcept { if (pimpl_->IsWithinValue()) { pimpl_->MarkValueEnd(); } + + // make sure that we have ended writing the "text" value + pimpl_->StopText(); + GetTagWriter().PutLogExtra(pimpl_->GetLogExtra()); pimpl_->PutMessageEnd(); @@ -321,11 +325,6 @@ LogHelper& LogHelper::operator<<(LogExtra&& extra) noexcept { return *this; } -LogHelper& LogHelper::operator<<(const LogExtra::Value& value) noexcept { - std::visit([this](const auto& unwrapped) { *this << unwrapped; }, value); - return *this; -} - void LogHelper::PutFloatingPoint(float value) { fmt::format_to(fmt::appender(pimpl_->GetBufferForRawValuePart()), FMT_COMPILE("{}"), value); diff --git a/universal/src/logging/log_helper_impl.cpp b/universal/src/logging/log_helper_impl.cpp index 2c3bb30e91fa..98750a11da75 100644 --- a/universal/src/logging/log_helper_impl.cpp +++ b/universal/src/logging/log_helper_impl.cpp @@ -11,6 +11,7 @@ #include #include #include +#include "encoding_json.hpp" USERVER_NAMESPACE_BEGIN @@ -18,18 +19,45 @@ namespace logging { namespace { -char GetSeparatorFromLogger(LoggerRef logger) { +char GetKeyValueSeparatorFromLogger(LoggerRef logger) { switch (logger.GetFormat()) { case Format::kTskv: case Format::kRaw: return '='; case Format::kLtsv: + case Format::kJson: return ':'; } UINVARIANT(false, "Invalid logging::Format enum value"); } +char GetItemSeparatorFromLogger(LoggerRef logger) { + switch (logger.GetFormat()) { + case Format::kTskv: + case Format::kRaw: + case Format::kLtsv: + return '\t'; + case Format::kJson: + return ','; + } + + UINVARIANT(false, "Invalid logging::Format enum value"); +} + +char GetOpenCloseSeparatorFromLogger(LoggerRef logger) { + switch (logger.GetFormat()) { + case Format::kTskv: + case Format::kRaw: + case Format::kLtsv: + return '\0'; + case Format::kJson: + return '"'; + } + + UINVARIANT(false, "Invalid logging::Format enum value"); +} + using TimePoint = std::chrono::system_clock::time_point; auto FractionalMicroseconds(TimePoint time) noexcept { @@ -88,7 +116,10 @@ std::streamsize LogHelper::Impl::BufferStd::xsputn(const char_type* s, LogHelper::Impl::Impl(LoggerRef logger, Level level) noexcept : logger_(&logger), level_(std::max(level, logger_->GetLevel())), - key_value_separator_(GetSeparatorFromLogger(*logger_)) { + key_value_separator_(GetKeyValueSeparatorFromLogger(*logger_)), + item_separator_(GetItemSeparatorFromLogger(*logger_)), + open_close_separator_optional_( + GetOpenCloseSeparatorFromLogger(*logger_)) { static_assert(sizeof(LogHelper::Impl) < 4096, "Structures with size more than 4096 would consume at least " "8KB memory in allocator."); @@ -128,48 +159,101 @@ void LogHelper::Impl::PutMessageBegin() { msg_.append(std::string_view{"tskv"}); return; } + case Format::kJson: { + constexpr std::string_view kTemplate = + R"({"timestamp":"0000-00-00T00:00:00.000000","level":"")"; + const auto now = TimePoint::clock::now(); + const auto level_string = logging::ToUpperCaseString(level_); + msg_.resize(kTemplate.size() + level_string.size()); + fmt::format_to(msg_.data(), + // double `{` at the beginning for escaping: + // https://stackoverflow.com/a/68207254 + FMT_COMPILE(R"({{"timestamp":"{}.{:06}","level":"{}")"), + GetCurrentTimeString(now).ToStringView(), + FractionalMicroseconds(now), level_string); + return; + } } UASSERT_MSG(false, "Invalid value of Format enum"); } -void LogHelper::Impl::PutMessageEnd() { msg_.push_back('\n'); } +void LogHelper::Impl::PutMessageEnd() { + if (logger_->GetFormat() == Format::kJson) { + msg_.push_back('}'); + } + msg_.push_back('\n'); +} void LogHelper::Impl::PutKey(std::string_view key) { - if (!utils::encoding::ShouldKeyBeEscaped(key)) { + // make sure that we have ended writing the "text" value + StopText(); + + // `utils::encoding::ShouldKeyBeEscaped` works only with non json format + if (!utils::encoding::ShouldKeyBeEscaped(key) && + logger_->GetFormat() != Format::kJson) { PutRawKey(key); } else { UASSERT(!std::exchange(is_within_value_, true)); CheckRepeatedKeys(key); - msg_.push_back(utils::encoding::kTskvPairsSeparator); - utils::encoding::EncodeTskv( - msg_, key, utils::encoding::EncodeTskvMode::kKeyReplacePeriod); - msg_.push_back(key_value_separator_); + + PutItemSeparator(); + PutOptionalOpenCloseSeparator(); + if (logger_->GetFormat() == Format::kJson) { + EncodeJson(msg_, key); + } else { + utils::encoding::EncodeTskv( + msg_, key, utils::encoding::EncodeTskvMode::kKeyReplacePeriod); + } + PutOptionalOpenCloseSeparator(); + PutKeyValueSeparator(); } } void LogHelper::Impl::PutRawKey(std::string_view key) { + // make sure that we have ended writing the "text" value + StopText(); + UASSERT(!std::exchange(is_within_value_, true)); CheckRepeatedKeys(key); - const auto old_size = msg_.size(); - msg_.resize(old_size + 1 + key.size() + 1); + msg_.reserve(msg_.size() + key.size() + 4); - auto* position = msg_.data() + old_size; - *(position++) = utils::encoding::kTskvPairsSeparator; - key.copy(position, key.size()); - position += key.size(); - *(position++) = key_value_separator_; + PutItemSeparator(); + PutOptionalOpenCloseSeparator(); + msg_.append(key); + PutOptionalOpenCloseSeparator(); + PutKeyValueSeparator(); } void LogHelper::Impl::PutValuePart(std::string_view value) { UASSERT(is_within_value_); - utils::encoding::EncodeTskv(msg_, value, - utils::encoding::EncodeTskvMode::kValue); + if (logger_->GetFormat() == Format::kJson) { + EncodeJson(msg_, value); + } else { + utils::encoding::EncodeTskv(msg_, value, + utils::encoding::EncodeTskvMode::kValue); + } } void LogHelper::Impl::PutValuePart(char text_part) { UASSERT(is_within_value_); - utils::encoding::EncodeTskv(fmt::appender(msg_), text_part, - utils::encoding::EncodeTskvMode::kValue); + if (logger_->GetFormat() == Format::kJson) { + EncodeJson(msg_, std::string_view{&text_part, 1}); + } else { + utils::encoding::EncodeTskv(fmt::appender(msg_), text_part, + utils::encoding::EncodeTskvMode::kValue); + } +} + +void LogHelper::Impl::PutKeyValueSeparator() { + msg_.push_back(key_value_separator_); +} + +void LogHelper::Impl::PutItemSeparator() { msg_.push_back(item_separator_); } + +void LogHelper::Impl::PutOptionalOpenCloseSeparator() { + if (open_close_separator_optional_) { + msg_.push_back(open_close_separator_optional_); + } } LogBuffer& LogHelper::Impl::GetBufferForRawValuePart() noexcept { @@ -182,8 +266,21 @@ void LogHelper::Impl::MarkValueEnd() noexcept { } void LogHelper::Impl::StartText() { + UASSERT(is_text_finished_ == false); PutRawKey("text"); + PutOptionalOpenCloseSeparator(); initial_length_ = msg_.size(); + UASSERT(is_text_finished_ == false); +} + +void LogHelper::Impl::StopText() { + if (initial_length_ == 0) { + // text hasn't started yet + return; + } + if (!std::exchange(is_text_finished_, true)) { + PutOptionalOpenCloseSeparator(); + } } LogHelper::Impl::LazyInitedStream& LogHelper::Impl::GetLazyInitedStream() { @@ -214,6 +311,17 @@ void LogHelper::Impl::CheckRepeatedKeys( fmt::format("Repeated tag in logs: '{}'", raw_key)); } +void LogHelper::Impl::WriteRawJsonValue(std::string_view json) { + UASSERT(is_within_value_); + + if (logger_->GetFormat() == Format::kJson) { + msg_.append(json); + } else { + utils::encoding::EncodeTskv(msg_, json, + utils::encoding::EncodeTskvMode::kValue); + } +} + } // namespace logging USERVER_NAMESPACE_END diff --git a/universal/src/logging/log_helper_impl.hpp b/universal/src/logging/log_helper_impl.hpp index feeb25bb4311..025a614dcae9 100644 --- a/universal/src/logging/log_helper_impl.hpp +++ b/universal/src/logging/log_helper_impl.hpp @@ -44,6 +44,10 @@ class LogHelper::Impl final { void StartText(); + // If the text has been started an optional open-close separator is placed and + // is_text_finished_ is set to true. If the text is already stopped do nothing + void StopText(); + std::size_t GetTextSize() const { return msg_.size() - initial_length_; } void LogTheMessage() const; @@ -52,6 +56,12 @@ class LogHelper::Impl final { bool IsBroken() const noexcept; + void PutKeyValueSeparator(); + void PutItemSeparator(); + void PutOptionalOpenCloseSeparator(); + + void WriteRawJsonValue(std::string_view json); + private: class BufferStd final : public std::streambuf { public: @@ -80,11 +90,14 @@ class LogHelper::Impl final { impl::LoggerBase* logger_; const Level level_; - const char key_value_separator_; + const char key_value_separator_; // ':' or '=' + const char item_separator_; // ',' or '\t' + const char open_close_separator_optional_; // '"' or 0 LogBuffer msg_; std::optional lazy_stream_; LogExtra extra_; std::size_t initial_length_{0}; + bool is_text_finished_{false}; bool is_within_value_{false}; std::optional> debug_tag_keys_; };