diff --git a/source/common/common/logger.cc b/source/common/common/logger.cc index 476f2e06a9f9a..74cee4d084e4c 100644 --- a/source/common/common/logger.cc +++ b/source/common/common/logger.cc @@ -3,6 +3,7 @@ #include // use direct system-assert to avoid cyclic dependency. #include #include +#include #include #include @@ -306,6 +307,24 @@ void setLogFormatForLogger(spdlog::logger& logger, const std::string& log_format logger.set_formatter(std::move(formatter)); } +std::string serializeLogTags(const std::map& tags) { + if (tags.empty()) { + return ""; + } + + std::stringstream tags_stream; + tags_stream << "[Tags: "; + for (const auto& tag : tags) { + tags_stream << "\"" << tag.first << "\":\"" << tag.second << "\","; + } + + std::string serialized = tags_stream.str(); + serialized.pop_back(); + serialized += "] "; + + return serialized; +} + } // namespace Utility namespace CustomFlagFormatter { diff --git a/source/common/common/logger.h b/source/common/common/logger.h index 28aecaf86640a..720e9b1bb68d7 100644 --- a/source/common/common/logger.h +++ b/source/common/common/logger.h @@ -401,6 +401,11 @@ namespace Utility { */ void setLogFormatForLogger(spdlog::logger& logger, const std::string& log_format); +/** + * Serializes custom log tags to a string that will be prepended to the log message. + */ +std::string serializeLogTags(const std::map& tags); + } // namespace Utility // Contains custom flags to introduce user defined flags in log pattern. Reference: @@ -517,6 +522,25 @@ class EscapeMessageJsonString : public spdlog::custom_flag_formatter { */ #define ENVOY_LOG(LEVEL, ...) ENVOY_LOG_TO_LOGGER(ENVOY_LOGGER(), LEVEL, ##__VA_ARGS__) +#define ENVOY_TAGGED_LOG_TO_LOGGER(LOGGER, LEVEL, TAGS, FORMAT, ...) \ + do { \ + if (ENVOY_LOG_COMP_LEVEL(LOGGER, LEVEL)) { \ + ENVOY_LOG_TO_LOGGER(LOGGER, LEVEL, \ + ::Envoy::Logger::Utility::serializeLogTags(TAGS) + FORMAT, \ + ##__VA_ARGS__); \ + } \ + } while (0) + +/** + * Log with tags which are a map of key and value strings. When ENVOY_TAGGED_LOG is used, the tags + * are serialized and prepended to the log message. + * For example, the map {{"key1","val1","key2","val2"}} would be serialized to: + * [Tags: "key1":"val1","key2":"val2"]. The serialization pattern is defined by + * Envoy::Logger::Utility::serializeLogTags function. + */ +#define ENVOY_TAGGED_LOG(LEVEL, TAGS, FORMAT, ...) \ + ENVOY_TAGGED_LOG_TO_LOGGER(ENVOY_LOGGER(), LEVEL, TAGS, FORMAT, ##__VA_ARGS__) + /** * Log with a stable event name. This allows emitting a log line with a stable name in addition to * the standard log line. The stable log line is passed to custom sinks that may want to intercept diff --git a/test/common/common/log_macros_test.cc b/test/common/common/log_macros_test.cc index 5058e032f35f3..aaa9dce4154af 100644 --- a/test/common/common/log_macros_test.cc +++ b/test/common/common/log_macros_test.cc @@ -32,11 +32,15 @@ class TestFilterLog : public Logger::Loggable { ENVOY_STREAM_LOG(info, "fake message", stream_); ENVOY_CONN_LOG(error, "fake error", connection_); ENVOY_STREAM_LOG(error, "fake error", stream_); + ENVOY_TAGGED_LOG(info, tags_, "fake message {}", "val"); + ENVOY_TAGGED_LOG(info, (std::map{{"key", "val"}}), "fake message {}", + "val"); } void logMessageEscapeSequences() { ENVOY_LOG_MISC(info, "line 1 \n line 2 \t tab \\r test"); } private: + std::map tags_{{"key", "val"}}; NiceMock connection_; NiceMock stream_; }; diff --git a/test/common/common/logger_test.cc b/test/common/common/logger_test.cc index e9998db311e3b..d9e039a4d517b 100644 --- a/test/common/common/logger_test.cc +++ b/test/common/common/logger_test.cc @@ -384,6 +384,54 @@ TEST(LoggerTest, TestJsonFormatWithNestedJsonMessage) { ENVOY_LOG_MISC(info, "{\"nested_message\":\"hello\"}"); } +TEST(LoggerUtilityTest, TestSerializeLogTags) { + // No entries + EXPECT_EQ("", Envoy::Logger::Utility::serializeLogTags({})); + + // Empty key or value + EXPECT_EQ("[Tags: \"\":\"\"] ", Envoy::Logger::Utility::serializeLogTags({{"", ""}})); + EXPECT_EQ("[Tags: \"\":\"value\"] ", Envoy::Logger::Utility::serializeLogTags({{"", "value"}})); + EXPECT_EQ("[Tags: \"key\":\"\"] ", Envoy::Logger::Utility::serializeLogTags({{"key", ""}})); + + // Single entry + EXPECT_EQ("[Tags: \"key\":\"value\"] ", + Envoy::Logger::Utility::serializeLogTags({{"key", "value"}})); + + // Multiple entries + EXPECT_EQ("[Tags: \"key1\":\"value1\",\"key2\":\"value2\",\"key3\":\"value3\"] ", + Envoy::Logger::Utility::serializeLogTags( + {{"key1", "value1"}, {"key2", "value2"}, {"key3", "value3"}})); +} + +class ClassForTaggedLog : public Envoy::Logger::Loggable { +public: + void logMessageWithPreCreatedTags() { ENVOY_TAGGED_LOG(info, tags_, "fake message {}", "val"); } + + void logMessageWithInlineTags() { + ENVOY_TAGGED_LOG(info, (std::map{{"key_inline", "val"}}), + "fake message {}", "val"); + } + +private: + std::map tags_{{"key", "val"}}; +}; + +TEST(TaggedLogTest, TestTaggedLog) { + Envoy::Logger::Registry::setLogFormat("%v"); + MockLogSink sink(Envoy::Logger::Registry::getSink()); + EXPECT_CALL(sink, log(_, _)) + .WillOnce(Invoke([](auto msg, auto&) { + EXPECT_THAT(msg, HasSubstr("[Tags: \"key\":\"val\"] fake message val")); + })) + .WillOnce(Invoke([](auto msg, auto&) { + EXPECT_THAT(msg, HasSubstr("[Tags: \"key_inline\":\"val\"] fake message val")); + })); + + ClassForTaggedLog object; + object.logMessageWithPreCreatedTags(); + object.logMessageWithInlineTags(); +} + } // namespace } // namespace Logger } // namespace Envoy