diff --git a/.clang-tidy b/.clang-tidy index 4f23893d4e1c..f6c98ff44515 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -97,6 +97,8 @@ Checks: ,-readability-redundant-declaration\ ,-readability-uppercase-literal-suffix\ ,-readability-use-anyofallof\ + ,-hicpp-use-emplace\ + ,-modernize-use-emplace\ " WarningsAsErrors: '*' diff --git a/.mapping.json b/.mapping.json index 3b3d1051c470..f963ef7422af 100644 --- a/.mapping.json +++ b/.mapping.json @@ -750,7 +750,6 @@ "core/include/userver/utils/impl/wait_token_storage.hpp":"taxi/uservices/userver/core/include/userver/utils/impl/wait_token_storage.hpp", "core/include/userver/utils/impl/wrapped_call.hpp":"taxi/uservices/userver/core/include/userver/utils/impl/wrapped_call.hpp", "core/include/userver/utils/impl/wrapped_call_base.hpp":"taxi/uservices/userver/core/include/userver/utils/impl/wrapped_call_base.hpp", - "core/include/userver/utils/internal_tag_fwd.hpp":"taxi/uservices/userver/core/include/userver/utils/internal_tag_fwd.hpp", "core/include/userver/utils/lazy_shared_ptr.hpp":"taxi/uservices/userver/core/include/userver/utils/lazy_shared_ptr.hpp", "core/include/userver/utils/log.hpp":"taxi/uservices/userver/core/include/userver/utils/log.hpp", "core/include/userver/utils/periodic_task.hpp":"taxi/uservices/userver/core/include/userver/utils/periodic_task.hpp", @@ -1531,7 +1530,6 @@ "core/src/utils/impl/wait_token_storage.cpp":"taxi/uservices/userver/core/src/utils/impl/wait_token_storage.cpp", "core/src/utils/impl/wait_token_storage_test.cpp":"taxi/uservices/userver/core/src/utils/impl/wait_token_storage_test.cpp", "core/src/utils/impl/wrapped_call_base.cpp":"taxi/uservices/userver/core/src/utils/impl/wrapped_call_base.cpp", - "core/src/utils/internal_tag.hpp":"taxi/uservices/userver/core/src/utils/internal_tag.hpp", "core/src/utils/jemalloc.cpp":"taxi/uservices/userver/core/src/utils/jemalloc.cpp", "core/src/utils/jemalloc.hpp":"taxi/uservices/userver/core/src/utils/jemalloc.hpp", "core/src/utils/lazy_shared_ptr_test.cpp":"taxi/uservices/userver/core/src/utils/lazy_shared_ptr_test.cpp", @@ -3670,6 +3668,8 @@ "universal/include/userver/utils/distances.hpp":"taxi/uservices/userver/universal/include/userver/utils/distances.hpp", "universal/include/userver/utils/encoding/hex.hpp":"taxi/uservices/userver/universal/include/userver/utils/encoding/hex.hpp", "universal/include/userver/utils/encoding/tskv.hpp":"taxi/uservices/userver/universal/include/userver/utils/encoding/tskv.hpp", + "universal/include/userver/utils/encoding/tskv_parser.hpp":"taxi/uservices/userver/universal/include/userver/utils/encoding/tskv_parser.hpp", + "universal/include/userver/utils/encoding/tskv_parser_read.hpp":"taxi/uservices/userver/universal/include/userver/utils/encoding/tskv_parser_read.hpp", "universal/include/userver/utils/enumerate.hpp":"taxi/uservices/userver/universal/include/userver/utils/enumerate.hpp", "universal/include/userver/utils/exception.hpp":"taxi/uservices/userver/universal/include/userver/utils/exception.hpp", "universal/include/userver/utils/expected.hpp":"taxi/uservices/userver/universal/include/userver/utils/expected.hpp", @@ -3684,13 +3684,13 @@ "universal/include/userver/utils/get_if.hpp":"taxi/uservices/userver/universal/include/userver/utils/get_if.hpp", "universal/include/userver/utils/impl/boost_variadic_to_seq.hpp":"taxi/uservices/userver/universal/include/userver/utils/impl/boost_variadic_to_seq.hpp", "universal/include/userver/utils/impl/disable_core_dumps.hpp":"taxi/uservices/userver/universal/include/userver/utils/impl/disable_core_dumps.hpp", + "universal/include/userver/utils/impl/internal_tag.hpp":"taxi/uservices/userver/universal/include/userver/utils/impl/internal_tag.hpp", "universal/include/userver/utils/impl/internal_tag_fwd.hpp":"taxi/uservices/userver/universal/include/userver/utils/impl/internal_tag_fwd.hpp", "universal/include/userver/utils/impl/intrusive_link_mode.hpp":"taxi/uservices/userver/universal/include/userver/utils/impl/intrusive_link_mode.hpp", "universal/include/userver/utils/impl/projecting_view.hpp":"taxi/uservices/userver/universal/include/userver/utils/impl/projecting_view.hpp", "universal/include/userver/utils/impl/source_location.hpp":"taxi/uservices/userver/universal/include/userver/utils/impl/source_location.hpp", "universal/include/userver/utils/impl/static_registration.hpp":"taxi/uservices/userver/universal/include/userver/utils/impl/static_registration.hpp", "universal/include/userver/utils/impl/transparent_hash.hpp":"taxi/uservices/userver/universal/include/userver/utils/impl/transparent_hash.hpp", - "universal/include/userver/utils/impl/weak_internal_tag.hpp":"taxi/uservices/userver/universal/include/userver/utils/impl/weak_internal_tag.hpp", "universal/include/userver/utils/invariant_error.hpp":"taxi/uservices/userver/universal/include/userver/utils/invariant_error.hpp", "universal/include/userver/utils/ip.hpp":"taxi/uservices/userver/universal/include/userver/utils/ip.hpp", "universal/include/userver/utils/lazy_prvalue.hpp":"taxi/uservices/userver/universal/include/userver/utils/lazy_prvalue.hpp", @@ -3952,6 +3952,8 @@ "universal/src/utils/encoding/hex.cpp":"taxi/uservices/userver/universal/src/utils/encoding/hex.cpp", "universal/src/utils/encoding/hex_benchmark.cpp":"taxi/uservices/userver/universal/src/utils/encoding/hex_benchmark.cpp", "universal/src/utils/encoding/hex_test.cpp":"taxi/uservices/userver/universal/src/utils/encoding/hex_test.cpp", + "universal/src/utils/encoding/tskv_parser.cpp":"taxi/uservices/userver/universal/src/utils/encoding/tskv_parser.cpp", + "universal/src/utils/encoding/tskv_parser_test.cpp":"taxi/uservices/userver/universal/src/utils/encoding/tskv_parser_test.cpp", "universal/src/utils/encoding/tskv_test.cpp":"taxi/uservices/userver/universal/src/utils/encoding/tskv_test.cpp", "universal/src/utils/encoding/tskv_testdata_bin.hpp":"taxi/uservices/userver/universal/src/utils/encoding/tskv_testdata_bin.hpp", "universal/src/utils/enumerate_test.cpp":"taxi/uservices/userver/universal/src/utils/enumerate_test.cpp", @@ -4047,6 +4049,7 @@ "universal/utest/src/utest/assert_macros_test.cpp":"taxi/uservices/userver/universal/utest/src/utest/assert_macros_test.cpp", "universal/utest/src/utest/current_process_open_files.cpp":"taxi/uservices/userver/universal/utest/src/utest/current_process_open_files.cpp", "universal/utest/src/utest/current_process_open_files_test.cpp":"taxi/uservices/userver/universal/utest/src/utest/current_process_open_files_test.cpp", + "universal/utest/src/utest/log_capture_fixture.cpp":"taxi/uservices/userver/universal/utest/src/utest/log_capture_fixture.cpp", "universal/utest/src/utest/parameter_names_test.cpp":"taxi/uservices/userver/universal/utest/src/utest/parameter_names_test.cpp", "ydb/CMakeLists.txt":"taxi/uservices/userver/ydb/CMakeLists.txt", "ydb/functional_tests/CMakeLists.txt":"taxi/uservices/userver/ydb/functional_tests/CMakeLists.txt", diff --git a/chaotic/integration_tests/tests/render/logging.cpp b/chaotic/integration_tests/tests/render/logging.cpp index ee471bf3a225..209ee86e25bf 100644 --- a/chaotic/integration_tests/tests/render/logging.cpp +++ b/chaotic/integration_tests/tests/render/logging.cpp @@ -13,20 +13,18 @@ using Logging = utest::LogCaptureFixture<>; TEST_F(Logging, Object) { auto json = formats::json::MakeObject( - "integer", 1, "boolean", true, "number", 1.1, "string", "foo", + "integer", 1, "boolean", true, "number", 1.5, "string", "foo", "string-enum", "1", "object", formats::json::MakeObject(), "array", formats::json::MakeArray(1)); auto obj = json.As(); - LOG_INFO() << obj; - ASSERT_THAT( - ExtractRawLog(), - testing::HasSubstr( - "text={\"boolean\":true,\"integer\":1,\"number\":1.1,\"string\":" - "\"foo\",\"object\":{},\"array\":[1],\"string-enum\":\"1\"}\n")); + const auto obj_string = GetLogCapture().ToStringViaLogging(obj); + EXPECT_EQ(obj_string, + R"({"boolean":true,"integer":1,"number":1.5,"string":"foo",)" + R"("object":{},"array":[1],"string-enum":"1"})"); - LOG_INFO() << obj.string_enum; - ASSERT_THAT(ExtractRawLog(), testing::HasSubstr("\ttext=1\n")); + const auto enum_string = GetLogCapture().ToStringViaLogging(obj.string_enum); + ASSERT_EQ(enum_string, "1"); } USERVER_NAMESPACE_END diff --git a/core/include/userver/cache/cache_statistics.hpp b/core/include/userver/cache/cache_statistics.hpp index 893282fe5665..e1d3cca9274e 100644 --- a/core/include/userver/cache/cache_statistics.hpp +++ b/core/include/userver/cache/cache_statistics.hpp @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include @@ -62,7 +62,8 @@ class UpdateStatisticsScope final { ~UpdateStatisticsScope(); - impl::UpdateState GetState(utils::InternalTag) const; + // For internal use only + impl::UpdateState GetState(utils::impl::InternalTag) const; /// @endcond /// @brief Mark that the `Update` has finished with changes diff --git a/core/include/userver/dynamic_config/updater/component.hpp b/core/include/userver/dynamic_config/updater/component.hpp index 44894d1fd4a7..d5d69088ce90 100644 --- a/core/include/userver/dynamic_config/updater/component.hpp +++ b/core/include/userver/dynamic_config/updater/component.hpp @@ -21,7 +21,6 @@ #include #include #include -#include USERVER_NAMESPACE_BEGIN diff --git a/core/include/userver/dynamic_config/value.hpp b/core/include/userver/dynamic_config/value.hpp index 7af88edab635..808b191e5256 100644 --- a/core/include/userver/dynamic_config/value.hpp +++ b/core/include/userver/dynamic_config/value.hpp @@ -8,8 +8,8 @@ #include #include #include +#include #include -#include USERVER_NAMESPACE_BEGIN @@ -35,14 +35,16 @@ class DocsMap final { bool AreContentsEqual(const DocsMap& other) const; /// @cond - // For internal use only + // For internal use only. // Set of configs expected to be used is automatically updated when // configs are retrieved with 'Get' method. void SetConfigsExpectedToBeUsed( - utils::impl::TransparentSet configs, utils::InternalTag); + utils::impl::TransparentSet configs, + utils::impl::InternalTag); + // For internal use only. const utils::impl::TransparentSet& GetConfigsExpectedToBeUsed( - utils::InternalTag) const; + utils::impl::InternalTag) const; /// @endcond private: diff --git a/core/include/userver/os_signals/processor.hpp b/core/include/userver/os_signals/processor.hpp index 98a00d6de3be..50a82bd917f8 100644 --- a/core/include/userver/os_signals/processor.hpp +++ b/core/include/userver/os_signals/processor.hpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include USERVER_NAMESPACE_BEGIN @@ -64,7 +64,7 @@ class Processor final { /// @cond // For internal use - void Notify(int signum, utils::InternalTag); + void Notify(int signum, utils::impl::InternalTag); /// @endcond private: concurrent::AsyncEventChannel channel_; diff --git a/core/include/userver/tracing/span.hpp b/core/include/userver/tracing/span.hpp index 973fbb9e93c9..b06b9969d1d4 100644 --- a/core/include/userver/tracing/span.hpp +++ b/core/include/userver/tracing/span.hpp @@ -10,8 +10,8 @@ #include #include #include +#include #include -#include USERVER_NAMESPACE_BEGIN @@ -206,9 +206,11 @@ class Span final { std::chrono::system_clock::time_point GetStartSystemTime() const; /// @cond - void AddTags(const logging::LogExtra&, utils::InternalTag); + // For internal use only. + void AddTags(const logging::LogExtra&, utils::impl::InternalTag); - impl::TimeStorage& GetTimeStorage(); + // For internal use only. + impl::TimeStorage& GetTimeStorage(utils::impl::InternalTag); // For internal use only. void LogTo(logging::impl::TagWriter writer) const&; diff --git a/core/include/userver/utils/internal_tag_fwd.hpp b/core/include/userver/utils/internal_tag_fwd.hpp deleted file mode 100644 index 7ca08f11ffc6..000000000000 --- a/core/include/userver/utils/internal_tag_fwd.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -/// @file userver/utils/internal_tag_fwd.hpp -/// @brief @copybrief utils::InternalTag - -USERVER_NAMESPACE_BEGIN - -namespace utils { - -/// @brief Guard tag for functions intended only for internal use -class InternalTag; - -} // namespace utils - -USERVER_NAMESPACE_END diff --git a/core/src/cache/cache_statistics.cpp b/core/src/cache/cache_statistics.cpp index 3f481b44f8d8..a9826891a604 100644 --- a/core/src/cache/cache_statistics.cpp +++ b/core/src/cache/cache_statistics.cpp @@ -3,7 +3,6 @@ #include #include #include -#include USERVER_NAMESPACE_BEGIN @@ -123,7 +122,8 @@ UpdateStatisticsScope::~UpdateStatisticsScope() { } } -impl::UpdateState UpdateStatisticsScope::GetState(utils::InternalTag) const { +impl::UpdateState UpdateStatisticsScope::GetState( + utils::impl::InternalTag) const { return state_; } diff --git a/core/src/cache/cache_update_trait_impl.cpp b/core/src/cache/cache_update_trait_impl.cpp index 525e3989ec62..b9d31078de0b 100644 --- a/core/src/cache/cache_update_trait_impl.cpp +++ b/core/src/cache/cache_update_trait_impl.cpp @@ -20,7 +20,6 @@ #include #include #include -#include USERVER_NAMESPACE_BEGIN @@ -403,7 +402,8 @@ void CacheUpdateTrait::Impl::DoUpdate(UpdateType update_type, try { customized_trait_.Update(update_type, last_update_, now, stats); - CheckUpdateState(stats.GetState(utils::InternalTag{}), update_type_str); + CheckUpdateState(stats.GetState(utils::impl::InternalTag{}), + update_type_str); } catch (const std::exception& e) { OnUpdateFailure(config); throw; diff --git a/core/src/components/manager.cpp b/core/src/components/manager.cpp index 4b3c782bc848..1c03cd121492 100644 --- a/core/src/components/manager.cpp +++ b/core/src/components/manager.cpp @@ -23,7 +23,6 @@ #include #include #include -#include USERVER_NAMESPACE_BEGIN @@ -233,7 +232,7 @@ void Manager::OnSignal(int signum) { std::shared_lock lock(context_mutex_); if (components_cleared_) return; if (signal_processor_) { - signal_processor_->Get().Notify(signum, utils::InternalTag{}); + signal_processor_->Get().Notify(signum, utils::impl::InternalTag{}); } } diff --git a/core/src/dynamic_config/storage/component.cpp b/core/src/dynamic_config/storage/component.cpp index c6dab15fa6a1..6b1cecb76311 100644 --- a/core/src/dynamic_config/storage/component.cpp +++ b/core/src/dynamic_config/storage/component.cpp @@ -26,7 +26,6 @@ #include #include -#include USERVER_NAMESPACE_BEGIN @@ -199,9 +198,9 @@ dynamic_config::impl::SnapshotData DynamicConfig::Impl::ParseConfig( void DynamicConfig::Impl::DoSetConfig(const dynamic_config::DocsMap& value) { auto config = ParseConfig(value); - if (!value.GetConfigsExpectedToBeUsed(utils::InternalTag{}).empty()) { + if (!value.GetConfigsExpectedToBeUsed(utils::impl::InternalTag{}).empty()) { LOG_INFO() << "Some configs expected to be used are actually not needed: " - << value.GetConfigsExpectedToBeUsed(utils::InternalTag{}); + << value.GetConfigsExpectedToBeUsed(utils::impl::InternalTag{}); } auto after_assign_hook = [&] { diff --git a/core/src/dynamic_config/updater/component.cpp b/core/src/dynamic_config/updater/component.cpp index 6cae7802bebd..183a3a8ccb9d 100644 --- a/core/src/dynamic_config/updater/component.cpp +++ b/core/src/dynamic_config/updater/component.cpp @@ -12,7 +12,6 @@ #include #include #include -#include USERVER_NAMESPACE_BEGIN @@ -94,7 +93,8 @@ dynamic_config::DocsMap DynamicConfigClientUpdater::MergeDocsMap( const std::vector& removed) { dynamic_config::DocsMap combined(std::move(update)); combined.MergeMissing(current); - combined.SetConfigsExpectedToBeUsed(docs_map_keys_, utils::InternalTag{}); + combined.SetConfigsExpectedToBeUsed(docs_map_keys_, + utils::impl::InternalTag{}); for (const auto& key : removed) { combined.Remove(key); } diff --git a/core/src/dynamic_config/value.cpp b/core/src/dynamic_config/value.cpp index 2e29e40e0d88..0e1bfb229d71 100644 --- a/core/src/dynamic_config/value.cpp +++ b/core/src/dynamic_config/value.cpp @@ -6,7 +6,6 @@ #include #include -#include USERVER_NAMESPACE_BEGIN @@ -83,12 +82,13 @@ bool DocsMap::AreContentsEqual(const DocsMap& other) const { } void DocsMap::SetConfigsExpectedToBeUsed( - utils::impl::TransparentSet configs, utils::InternalTag) { + utils::impl::TransparentSet configs, + utils::impl::InternalTag) { configs_to_be_used_ = std::move(configs); } const utils::impl::TransparentSet& -DocsMap::GetConfigsExpectedToBeUsed(utils::InternalTag) const { +DocsMap::GetConfigsExpectedToBeUsed(utils::impl::InternalTag) const { return configs_to_be_used_; } diff --git a/core/src/dynamic_config/value_test.cpp b/core/src/dynamic_config/value_test.cpp index 63473b6fd8cb..cba2f6172f84 100644 --- a/core/src/dynamic_config/value_test.cpp +++ b/core/src/dynamic_config/value_test.cpp @@ -6,7 +6,6 @@ #include #include #include -#include USERVER_NAMESPACE_BEGIN @@ -21,15 +20,15 @@ TEST(DocsMap, HasConfig) { TEST(DocsMap, AreContentsEqualTrue) { dynamic_config::DocsMap docs_map1; - docs_map1.SetConfigsExpectedToBeUsed({"a"}, utils::InternalTag{}); + docs_map1.SetConfigsExpectedToBeUsed({"a"}, utils::impl::InternalTag{}); docs_map1.Parse(R"({"a": "a", "b": "b"})", false); dynamic_config::DocsMap docs_map2; - docs_map2.SetConfigsExpectedToBeUsed({"b"}, utils::InternalTag{}); + docs_map2.SetConfigsExpectedToBeUsed({"b"}, utils::impl::InternalTag{}); docs_map2.Parse(R"({"b": "b", "a": "a"})", false); - EXPECT_NE(docs_map1.GetConfigsExpectedToBeUsed(utils::InternalTag{}), - docs_map2.GetConfigsExpectedToBeUsed(utils::InternalTag{})); + EXPECT_NE(docs_map1.GetConfigsExpectedToBeUsed(utils::impl::InternalTag{}), + docs_map2.GetConfigsExpectedToBeUsed(utils::impl::InternalTag{})); EXPECT_TRUE(docs_map1.AreContentsEqual(docs_map2)); } @@ -40,8 +39,8 @@ TEST(DocsMap, AreContentsEqualFalse) { dynamic_config::DocsMap docs_map2; docs_map2.Parse(R"({"a": "b", "b": "a"})", false); - EXPECT_EQ(docs_map1.GetConfigsExpectedToBeUsed(utils::InternalTag{}), - docs_map2.GetConfigsExpectedToBeUsed(utils::InternalTag{})); + EXPECT_EQ(docs_map1.GetConfigsExpectedToBeUsed(utils::impl::InternalTag{}), + docs_map2.GetConfigsExpectedToBeUsed(utils::impl::InternalTag{})); EXPECT_FALSE(docs_map1.AreContentsEqual(docs_map2)); } @@ -50,22 +49,23 @@ TEST(DocsMap, ConfigExpectedToBeUsedRemovedAfterGet) { utils::impl::TransparentSet to_be_used = {"a", "b"}; docs_map.SetConfigsExpectedToBeUsed( utils::impl::TransparentSet(to_be_used), - utils::InternalTag{}); + utils::impl::InternalTag{}); docs_map.Parse(R"({"a": "a", "b": "b"})", false); - EXPECT_EQ(docs_map.GetConfigsExpectedToBeUsed(utils::InternalTag{}), + EXPECT_EQ(docs_map.GetConfigsExpectedToBeUsed(utils::impl::InternalTag{}), to_be_used); (void)docs_map.Get("a"); - EXPECT_EQ(docs_map.GetConfigsExpectedToBeUsed(utils::InternalTag{}), + EXPECT_EQ(docs_map.GetConfigsExpectedToBeUsed(utils::impl::InternalTag{}), utils::impl::TransparentSet({"b"})); dynamic_config::DocsMap docs_map_copy(docs_map); (void)docs_map_copy.Get("b"); EXPECT_TRUE( - docs_map_copy.GetConfigsExpectedToBeUsed(utils::InternalTag{}).empty()); + docs_map_copy.GetConfigsExpectedToBeUsed(utils::impl::InternalTag{}) + .empty()); } TEST(DocsMap, Merge) { diff --git a/core/src/os_signals/component.cpp b/core/src/os_signals/component.cpp index 7acdd3fbeb0e..27d3b123fdcf 100644 --- a/core/src/os_signals/component.cpp +++ b/core/src/os_signals/component.cpp @@ -2,8 +2,6 @@ #include -#include - USERVER_NAMESPACE_BEGIN namespace os_signals { diff --git a/core/src/os_signals/processor.cpp b/core/src/os_signals/processor.cpp index b5b8ea70186d..802720c4e24c 100644 --- a/core/src/os_signals/processor.cpp +++ b/core/src/os_signals/processor.cpp @@ -6,8 +6,6 @@ #include #include -#include - USERVER_NAMESPACE_BEGIN namespace os_signals { @@ -24,7 +22,7 @@ Processor::Processor(engine::TaskProcessor& task_processor) : channel_(kOsSignalProcessorChannelName.data()), task_processor_(task_processor) {} -void Processor::Notify(int signum, utils::InternalTag) { +void Processor::Notify(int signum, utils::impl::InternalTag) { UINVARIANT(signum == SIGUSR1 || signum == SIGUSR2, "Processor::Notify() should be used only with SIGUSR1 and SIGUSR2 " "signum values"); diff --git a/core/src/os_signals/processor_mock.cpp b/core/src/os_signals/processor_mock.cpp index 507dacdc6312..82fe935b2512 100644 --- a/core/src/os_signals/processor_mock.cpp +++ b/core/src/os_signals/processor_mock.cpp @@ -1,7 +1,5 @@ #include -#include - USERVER_NAMESPACE_BEGIN namespace os_signals { @@ -12,7 +10,7 @@ ProcessorMock::ProcessorMock(engine::TaskProcessor& task_processor) Processor& ProcessorMock::Get() { return manager_; } void ProcessorMock::Notify(int signum) { - manager_.Notify(signum, utils::InternalTag{}); + manager_.Notify(signum, utils::impl::InternalTag{}); } } // namespace os_signals diff --git a/core/src/tracing/scope_time.cpp b/core/src/tracing/scope_time.cpp index 65e526a6783f..5344a4ac19b1 100644 --- a/core/src/tracing/scope_time.cpp +++ b/core/src/tracing/scope_time.cpp @@ -9,10 +9,12 @@ USERVER_NAMESPACE_BEGIN namespace tracing { ScopeTime::ScopeTime() - : ScopeTime(tracing::Span::CurrentSpan().GetTimeStorage()) {} + : ScopeTime(tracing::Span::CurrentSpan().GetTimeStorage( + utils::impl::InternalTag{})) {} ScopeTime::ScopeTime(std::string scope_name) - : ScopeTime(tracing::Span::CurrentSpan().GetTimeStorage(), + : ScopeTime(tracing::Span::CurrentSpan().GetTimeStorage( + utils::impl::InternalTag{}), std::move(scope_name)) {} std::optional ScopeTime::CreateOptionalScopeTime() { diff --git a/core/src/tracing/span.cpp b/core/src/tracing/span.cpp index 325363d42c74..c449f3886548 100644 --- a/core/src/tracing/span.cpp +++ b/core/src/tracing/span.cpp @@ -16,7 +16,6 @@ #include #include #include -#include USERVER_NAMESPACE_BEGIN @@ -343,11 +342,14 @@ void Span::AddTag(std::string key, logging::LogExtra::Value value) { pimpl_->log_extra_inheritable_.Extend(std::move(key), std::move(value)); } -void Span::AddTags(const logging::LogExtra& log_extra, utils::InternalTag) { +void Span::AddTags(const logging::LogExtra& log_extra, + utils::impl::InternalTag) { pimpl_->log_extra_inheritable_.Extend(log_extra); } -impl::TimeStorage& Span::GetTimeStorage() { return pimpl_->GetTimeStorage(); } +impl::TimeStorage& Span::GetTimeStorage(utils::impl::InternalTag) { + return pimpl_->GetTimeStorage(); +} std::string Span::GetTag(std::string_view tag) const { const auto& value = pimpl_->log_extra_inheritable_.GetValue(tag); diff --git a/core/src/utils/internal_tag.hpp b/core/src/utils/internal_tag.hpp deleted file mode 100644 index 10c85de2a069..000000000000 --- a/core/src/utils/internal_tag.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include - -USERVER_NAMESPACE_BEGIN - -namespace utils { - -class InternalTag {}; - -} // namespace utils - -USERVER_NAMESPACE_END diff --git a/grpc/tests/src/cancel_test.cpp b/grpc/tests/src/cancel_test.cpp index 41369d8d0803..8513d80a0c53 100644 --- a/grpc/tests/src/cancel_test.cpp +++ b/grpc/tests/src/cancel_test.cpp @@ -353,12 +353,13 @@ UTEST_F(GrpcCancelSleep, CancelByTimeoutLogging) { engine::SleepFor(std::chrono::seconds(1)); - ASSERT_THAT( - ExtractRawLog(), - testing::HasSubstr("Handler task cancelled, error in " - "'sample.ugrpc.UnitTestService/SayHello': " - "'sample.ugrpc.UnitTestService/SayHello' failed: " - "connection error at Finish")); + EXPECT_THAT( + GetLogCapture().Filter("Handler task cancelled, error in " + "'sample.ugrpc.UnitTestService/SayHello': " + "'sample.ugrpc.UnitTestService/SayHello' failed: " + "connection error at Finish"), + testing::SizeIs(1)) + << GetLogCapture().GetAll(); } namespace { @@ -385,10 +386,11 @@ UTEST_F(GrpcCancelError, CancelByError) { engine::SleepFor(std::chrono::seconds(1)); - ASSERT_THAT(ExtractRawLog(), - testing::HasSubstr("Handler task cancelled, error in " - "'sample.ugrpc.UnitTestService/Chat': " - "Some error (std::runtime_error)")); + ASSERT_THAT(GetLogCapture().Filter("Handler task cancelled, error in " + "'sample.ugrpc.UnitTestService/Chat': " + "Some error (std::runtime_error)"), + testing::SizeIs(1)) + << GetLogCapture().GetAll(); } USERVER_NAMESPACE_END diff --git a/grpc/tests/src/logging_test.cpp b/grpc/tests/src/logging_test.cpp index c55686faf723..2e998ae28bef 100644 --- a/grpc/tests/src/logging_test.cpp +++ b/grpc/tests/src/logging_test.cpp @@ -1,8 +1,10 @@ #include +#include #include +#include -#include +#include #include #include @@ -13,25 +15,8 @@ USERVER_NAMESPACE_BEGIN namespace { -class Logger final : public logging::impl::LoggerBase { - public: - Logger() noexcept : LoggerBase(logging::Format::kRaw) { - LoggerBase::SetLevel(logging::Level::kInfo); - } - - void SetLevel(logging::Level) override {} // do nothing - void Log(logging::Level, std::string_view str) override { log += str; } - void Flush() override {} - - std::string log; -}; - -struct LoggerHolder { - std::shared_ptr logger_; -}; - ugrpc::server::ServerConfig MakeServerConfig( - std::shared_ptr access_tskv_logger) { + logging::LoggerPtr access_tskv_logger) { ugrpc::server::ServerConfig config; config.port = 0; config.access_tskv_logger = access_tskv_logger; @@ -50,15 +35,16 @@ class UnitTestService final : public sample::ugrpc::UnitTestServiceBase { template // NOLINTNEXTLINE(fuchsia-multiple-inheritance) -class ServiceWithAccessLogFixture : public LoggerHolder, - public ugrpc::tests::Service, - public ::testing::Test { +class ServiceWithAccessLogFixture + : protected boost::base_from_member, + public ugrpc::tests::Service, + public ::testing::Test { public: ServiceWithAccessLogFixture() - : LoggerHolder{std::make_shared()}, + : boost::base_from_member(logging::Format::kRaw), ugrpc::tests::Service( - dynamic_config::MakeDefaultStorage({}), MakeServerConfig(logger_)) { - } + dynamic_config::MakeDefaultStorage({}), + MakeServerConfig(member.GetLogger())) {} }; } // namespace @@ -85,8 +71,10 @@ UTEST_F(GrpcAccessLog, Test) { R"(grpc_status=\d+\t)" R"(grpc_status_code=[A-Z_]+\n)"; - EXPECT_TRUE(utils::regex_match(logger_->log, utils::regex(kExpectedPattern))) - << logger_->log; + const auto logs = member.ExtractSingle(); + EXPECT_TRUE( + utils::regex_match(logs.GetLogRaw(), utils::regex(kExpectedPattern))) + << logs; } USERVER_NAMESPACE_END diff --git a/universal/include/userver/utils/encoding/tskv_parser.hpp b/universal/include/userver/utils/encoding/tskv_parser.hpp new file mode 100644 index 000000000000..17ada4435398 --- /dev/null +++ b/universal/include/userver/utils/encoding/tskv_parser.hpp @@ -0,0 +1,82 @@ +#pragma once + +/// @file userver/utils/encoding/tskv_parser.hpp +/// @brief @copybrief utils::encoding::TskvParser + +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace utils::encoding { + +/// @brief A streaming parser for the TSKV variant that userver emits. +/// +/// **Supported syntax** +/// 1. Records are separated by a single non-escaped `\n` character +/// 2. A record consists of entries separated by a single `\t` character +/// 3. The first entry SHOULD be verbatim `tskv` +/// 4. Entries MAY have values, separated by the first non-escaped `=` +/// 5. An entry MAY have no key-value split, in which case it all counts as key +/// +/// **Escaping** +/// 1. `\n`, `\t`, `\\` in keys or values SHOULD be escaped as +/// `"\\\n"`, `"\\\t"`, `"\\\\"` +/// 2. `=` SHOULD be escaped as `"\\="` in keys, MAY be escaped in values +/// 3. `\r` and `\0` MAY be escaped as `"\\\r"`, `"\\\0"` +/// +/// **Parsing process** +/// Initialize `TskvParser` with a caller-owned string that may contain a single +/// or multiple TSKV records. For each record, first call `SkipToRecordBegin`. +/// Then call `ReadKey` and `ReadValue` a bunch of times. +/// +/// For a simpler way to read TSKV records, see utils::encoding::TskvReadRecord. +class TskvParser final { + public: + /// The status is returned once the record is complete. We ignore invalid + /// escaping, not reporting it as an error, trying to push through. + /// `nullopt` status means we should keep reading the current TSKV record. + enum class [[nodiscard]] RecordStatus { + kReachedEnd, ///< successfully read the whole record + kIncomplete, ///< the record ends abruptly, the service is probably + ///< still writing the record + }; + + explicit TskvParser(std::string_view in) noexcept; + + /// @brief Skips the current record or optional invalid junk until it finds + /// the start of the a valid record. + /// @returns pointer to the start of the next record if there is one, + /// `nullptr` otherwise + /// @note `tskv\n` records are currently not supported + const char* SkipToRecordBegin() noexcept; + + /// @brief Skips to the end of the current record, which might not be the same + /// as the start of the next valid record. + /// @note RecordStatus::kReachedEnd SHOULD + /// not have been returned for the current record, otherwise the behavior is + /// unspecified. + /// @note `tskv\n` records are currently not supported + RecordStatus SkipToRecordEnd() noexcept; + + /// @brief Parses the key, replacing `result` with it. + /// @throws std::bad_alloc only if `std::string` allocation throws + /// @note RecordStatus::kReachedEnd is returned specifically + /// on a trailing `\t\n` sequence + std::optional ReadKey(std::string& result); + + /// @brief Parses the value, replacing `result` with it. + /// @throws std::bad_alloc only if `std::string` allocation throws + std::optional ReadValue(std::string& result); + + /// @returns pointer to the data that will be read by the next operation + const char* GetStreamPosition() const noexcept; + + private: + std::string_view in_; +}; + +} // namespace utils::encoding + +USERVER_NAMESPACE_END diff --git a/universal/include/userver/utils/encoding/tskv_parser_read.hpp b/universal/include/userver/utils/encoding/tskv_parser_read.hpp new file mode 100644 index 000000000000..194cf0826c60 --- /dev/null +++ b/universal/include/userver/utils/encoding/tskv_parser_read.hpp @@ -0,0 +1,69 @@ +#pragma once + +/// @file userver/utils/encoding/tskv_parser_read.hpp +/// @brief @copybrief utils::encoding::TskvReadKeysValues + +#include + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace utils::encoding { + +namespace impl { + +struct TskvParserKvStorage { + std::string key{}; + std::string value{}; +}; + +inline compiler::ThreadLocal tskv_parser_kv_storage = [] { + return TskvParserKvStorage{}; +}; + +} // namespace impl + +/// @brief Read all keys-values for 1 TSKV record. +/// +/// @param parser parser that should have already found the start of the TSKV +/// record using TskvParser::SkipToRecordBegin +/// @param consumer a lambda with the signature +/// `(const std::string&, const std::string&) -> bool`; +/// the strings are temporaries, references to them should not be stored; +/// can return `false` to skip the record and return immediately +/// +/// Usage example: +/// @snippet utils/encoding/tskv_parser_test.cpp sample +template +TskvParser::RecordStatus TskvReadRecord(TskvParser& parser, + TagConsumer consumer) { + using RecordStatus = TskvParser::RecordStatus; + + auto kv_storage = impl::tskv_parser_kv_storage.Use(); + + while (true) { + if (const auto key_status = parser.ReadKey(kv_storage->key)) { + return *key_status; + } + + const auto value_status = parser.ReadValue(kv_storage->value); + if (value_status == RecordStatus::kIncomplete) { + return RecordStatus::kIncomplete; + } + + const bool record_ok = consumer(std::as_const(kv_storage->key), + std::as_const(kv_storage->value)); + if (value_status == RecordStatus::kReachedEnd) { + return RecordStatus::kReachedEnd; + } + if (!record_ok) { + return parser.SkipToRecordEnd(); + } + } +} + +} // namespace utils::encoding + +USERVER_NAMESPACE_END diff --git a/universal/include/userver/utils/impl/internal_tag.hpp b/universal/include/userver/utils/impl/internal_tag.hpp new file mode 100644 index 000000000000..98b339b4264a --- /dev/null +++ b/universal/include/userver/utils/impl/internal_tag.hpp @@ -0,0 +1,19 @@ +#pragma once + +USERVER_NAMESPACE_BEGIN + +namespace utils::impl { + +// Guard tag for functions intended only for use only within userver itself. +// +// Consequences of using this tag outside of userver: +// 1. support for your service will be dropped by userver team; +// 2. calling the function outside of userver may break your code up to UB; +// 3. compilation of your service may break after a patch-level userver update. +struct InternalTag final { + constexpr explicit InternalTag() noexcept = default; +}; + +} // namespace utils::impl + +USERVER_NAMESPACE_END diff --git a/universal/include/userver/utils/impl/weak_internal_tag.hpp b/universal/include/userver/utils/impl/weak_internal_tag.hpp deleted file mode 100644 index 2fb6281dcd0b..000000000000 --- a/universal/include/userver/utils/impl/weak_internal_tag.hpp +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -USERVER_NAMESPACE_BEGIN - -namespace utils::impl { - -// Guard tag for functions intended only for internal use. -// -// This version of the tag is weak compared to utils::InternalTag, because it's -// does not formally prevent the user from using the protected functionality. -// This is mainly needed in templates. -// -// Hopefully, utils::impl::WeakInternalTag{} will scare off users that care -// about their code not being broken after a userver update. -struct WeakInternalTag { - explicit WeakInternalTag() = default; -}; - -} // namespace utils::impl - -USERVER_NAMESPACE_END diff --git a/universal/include/userver/utils/struct_subsets.hpp b/universal/include/userver/utils/struct_subsets.hpp index e4931d5c812e..81278ec85c4b 100644 --- a/universal/include/userver/utils/struct_subsets.hpp +++ b/universal/include/userver/utils/struct_subsets.hpp @@ -11,7 +11,7 @@ #include #include -#include +#include USERVER_NAMESPACE_BEGIN @@ -20,7 +20,7 @@ namespace utils::impl { struct RequireSemicolon; struct NonMovable final { - explicit NonMovable(WeakInternalTag) {} + constexpr explicit NonMovable(InternalTag) noexcept {} }; template @@ -48,7 +48,7 @@ USERVER_NAMESPACE_END #define USERVER_IMPL_MAKE_FROM_SUPERSET(Self, ...) \ template \ static Self MakeFromSupersetImpl( \ - OtherDeps&& other, USERVER_NAMESPACE::utils::impl::WeakInternalTag) { \ + OtherDeps&& other, USERVER_NAMESPACE::utils::impl::InternalTag) { \ return {BOOST_PP_SEQ_FOR_EACH(USERVER_IMPL_STRUCT_MAP, BOOST_PP_EMPTY(), \ USERVER_IMPL_VARIADIC_TO_SEQ(__VA_ARGS__))}; \ } @@ -79,7 +79,7 @@ USERVER_NAMESPACE_END Enable = 0> \ /*implicit*/ operator Other() const& { \ return Other::MakeFromSupersetImpl( \ - *this, USERVER_NAMESPACE::utils::impl::WeakInternalTag{}); \ + *this, USERVER_NAMESPACE::utils::impl::InternalTag{}); \ } \ \ template < \ @@ -89,7 +89,7 @@ USERVER_NAMESPACE_END Enable = 0> \ /*implicit*/ operator Other()&& { \ return Other::MakeFromSupersetImpl( \ - std::move(*this), USERVER_NAMESPACE::utils::impl::WeakInternalTag{}); \ + std::move(*this), USERVER_NAMESPACE::utils::impl::InternalTag{}); \ } \ \ friend struct USERVER_NAMESPACE::utils::impl::RequireSemicolon @@ -157,7 +157,7 @@ USERVER_NAMESPACE_END \ /* Protects against copying into async functions */ \ USERVER_NAMESPACE::utils::impl::NonMovable _impl_non_movable{ \ - USERVER_NAMESPACE::utils::impl::WeakInternalTag{}}; \ + USERVER_NAMESPACE::utils::impl::InternalTag{}}; \ \ USERVER_IMPL_MAKE_FROM_SUPERSET(SubsetStructRef, __VA_ARGS__) \ \ diff --git a/universal/src/utils/encoding/tskv_parser.cpp b/universal/src/utils/encoding/tskv_parser.cpp new file mode 100644 index 000000000000..4ede5097cab5 --- /dev/null +++ b/universal/src/utils/encoding/tskv_parser.cpp @@ -0,0 +1,154 @@ +#include + +#include + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace utils::encoding { + +namespace { + +constexpr std::string_view kTskv = "tskv\t"; + +std::optional GetUnescapedChar(char second_char) { + switch (second_char) { + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + case '0': + return '\0'; + case '\\': + return '\\'; + case '=': + return '='; + default: + return std::nullopt; + } +} + +// Returns the number of chars consumed. +std::size_t Unescape(char second_char, std::string& result) { + if (const auto unescaped = GetUnescapedChar(second_char)) { + result.push_back(*unescaped); + return 2; + } + // Unsupported escaping. Consider this as a garbage '\\' to avoid the + // situation where invalid escaping contaminates the entire log. + result.push_back('\\'); + return 1; +} + +} // namespace + +TskvParser::TskvParser(std::string_view in) noexcept : in_(in) {} + +const char* TskvParser::SkipToRecordBegin() noexcept { + // TODO support empty records of the form "tskv\n"? + + while (true) { + if (text::StartsWith(in_, kTskv)) { + // Happy case. + const char* const result = in_.data(); + in_.remove_prefix(kTskv.size()); + return result; + } + + const auto newline_pos = in_.find('\n'); + if (newline_pos == std::string_view::npos) { + return nullptr; + } + + in_.remove_prefix(newline_pos + 1); + } +} + +TskvParser::RecordStatus TskvParser::SkipToRecordEnd() noexcept { + const auto newline_pos = in_.find('\n'); + if (newline_pos == std::string_view::npos) { + return RecordStatus::kIncomplete; + } + + in_.remove_prefix(newline_pos + 1); + return RecordStatus::kReachedEnd; +} + +std::optional TskvParser::ReadKey( + std::string& result) { + result.clear(); + + while (true) { + const auto key_end = in_.find_first_of("\\=\t\n"); + if (key_end == std::string_view::npos) { + return RecordStatus::kIncomplete; + } + const auto separator = in_[key_end]; + + if (separator == '=') { + result.append(in_.substr(0, key_end)); + in_.remove_prefix(key_end + 1); + return std::nullopt; + } + + if (separator == '\n' && key_end == 0) { + // Drop an otherwise empty record in a "\t\n" sequence. + in_.remove_prefix(1); + return RecordStatus::kReachedEnd; + } + + if (separator == '\t' || separator == '\n') { + // A key-only entry. + result.append(in_.substr(0, key_end)); + in_.remove_prefix(key_end); + return std::nullopt; + } + + UASSERT(separator == '\\'); + if (key_end + 1 == in_.size()) { + return RecordStatus::kIncomplete; + } + result.append(in_.substr(0, key_end)); + in_.remove_prefix(key_end + Unescape(in_[key_end + 1], result)); + } +} + +std::optional TskvParser::ReadValue( + std::string& result) { + result.clear(); + + while (true) { + const auto value_end = in_.find_first_of("\\\t\n"); + if (value_end == std::string_view::npos) { + return RecordStatus::kIncomplete; + } + const auto separator = in_[value_end]; + + if (separator != '\\') { + UASSERT(separator == '\t' || separator == '\n'); + result.append(in_.substr(0, value_end)); + in_.remove_prefix(value_end + 1); + return separator == '\n' ? std::make_optional(RecordStatus::kReachedEnd) + : std::nullopt; + } + + UASSERT(separator == '\\'); + if (value_end + 1 == in_.size()) { + return RecordStatus::kIncomplete; + } + result.append(in_.substr(0, value_end)); + in_.remove_prefix(value_end + Unescape(in_[value_end + 1], result)); + } +} + +const char* TskvParser::GetStreamPosition() const noexcept { + return in_.data(); +} + +} // namespace utils::encoding + +USERVER_NAMESPACE_END diff --git a/universal/src/utils/encoding/tskv_parser_test.cpp b/universal/src/utils/encoding/tskv_parser_test.cpp new file mode 100644 index 000000000000..75a47bd089fa --- /dev/null +++ b/universal/src/utils/encoding/tskv_parser_test.cpp @@ -0,0 +1,291 @@ +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace { + +/// [sample] +struct Record { + std::vector> tags; +}; +struct Junk { + std::string data; +}; +struct Incomplete { + std::string data; +}; +using SingleParsedRecord = std::variant; +using ParsedRecords = std::vector; + +ParsedRecords ParseTskv(std::string_view in) { + ParsedRecords result; + utils::encoding::TskvParser parser{in}; + + const auto account_incomplete = [&](const char* incomplete_begin) { + const std::string_view remaining(incomplete_begin, + in.data() + in.size() - incomplete_begin); + if (!remaining.empty()) { + result.push_back(Incomplete{std::string(remaining)}); + } + }; + + while (true) { + const char* const junk_begin = parser.GetStreamPosition(); + const char* const record_begin = parser.SkipToRecordBegin(); + + if (!record_begin) { + // Note: production code should stop and wait for more data here. + account_incomplete(junk_begin); + break; + } + + // Note: accounting for junk in-between records is supported, but is + // typically not important in production. + if (record_begin != junk_begin) { + result.push_back(Junk{std::string(junk_begin, record_begin)}); + } + + std::vector> tags; + + const auto status = utils::encoding::TskvReadRecord( + parser, [&](const std::string& key, const std::string& value) { + tags.emplace_back(key, value); + return true; + }); + + if (status == utils::encoding::TskvParser::RecordStatus::kIncomplete) { + // Note: production code should stop and wait for more data here. + account_incomplete(record_begin); + break; + } + + result.push_back(Record{std::move(tags)}); + } + + return result; +} +/// [sample] + +struct TestCase { + std::string test_name; + std::string tskv; + ParsedRecords result; +}; + +using TestData = std::initializer_list; + +bool operator==(const Record& lhs, const Record& rhs) { + return lhs.tags == rhs.tags; +} +[[maybe_unused]] void PrintTo(const Record& value, std::ostream* out) { + *out << testing::PrintToString(value.tags); +} + +bool operator==(const Junk& lhs, const Junk& rhs) { + return lhs.data == rhs.data; +} +[[maybe_unused]] void PrintTo(const Junk& value, std::ostream* out) { + *out << value.data; +} + +bool operator==(const Incomplete& lhs, const Incomplete& rhs) { + return lhs.data == rhs.data; +} +[[maybe_unused]] void PrintTo(const Incomplete& value, std::ostream* out) { + *out << value.data; +} + +[[maybe_unused]] void PrintTo(const SingleParsedRecord& value, + std::ostream* out) { + *out << utils::Visit( + value, [](const Record&) { return "Record"; }, + [](const Junk&) { return "Junk"; }, + [](const Incomplete&) { return "Incomplete"; }) + << "("; + std::visit([out](const auto& value) { PrintTo(value, out); }, value); + *out << ")"; +} + +[[maybe_unused]] void PrintTo(const TestCase& test, std::ostream* out) { + *out << test.test_name; +} + +} // namespace + +class TskvParseTest : public ::testing::TestWithParam {}; + +TEST_P(TskvParseTest, Parsing) { + EXPECT_EQ(ParseTskv(GetParam().tskv), GetParam().result); +} + +INSTANTIATE_TEST_SUITE_P( // + SingleLine, TskvParseTest, + ::testing::ValuesIn(TestData{ + {"1_field", "tskv\thello=word\n", {Record{{{"hello", "word"}}}}}, + + {"2_fields", + "tskv\thello=word\tfoo=bar\n", + {Record{{{"hello", "word"}, {"foo", "bar"}}}}}, + + {"2_field_and_eq_sign", + "tskv\thello\\=there=word\tfoo\\=there=bar\n", + {Record{{{"hello=there", "word"}, {"foo=there", "bar"}}}}}, + + {"2_fields_and_many_eq_signs", + "tskv\t\\=hello=word\t\\=foo\\=\\=there\\==bar\n", + {Record{{{"=hello", "word"}, {"=foo==there=", "bar"}}}}}, + + {"2_fields_and_many_tabs", + "tskv\thello=\\t\\tw\\t\\to\\tr\\td\\t\\t\tfoo\\=there=\\tbar\n", + {Record{{{"hello", "\t\tw\t\to\tr\td\t\t"}, {"foo=there", "\tbar"}}}}}, + + {"2_fields_and_many_newlines", + "tskv\thello=\\n\\nw\\n\\no\\nr\\nd\\n\\n\tfoo\\=there=\\nbar\n", + {Record{{{"hello", "\n\nw\n\no\nr\nd\n\n"}, {"foo=there", "\nbar"}}}}}, + + {"3_fields_with_empty_key_values_fixed", + "tskv\t=word\tfoo=\t=\n", + {Record{{{"", "word"}, {"foo", ""}, {"", ""}}}}}, + + {"3_fields_with_empty_key_values_and_eq_signs", + "tskv\t\\===\tfoo==\t==\n", + {Record{{{"=", "="}, {"foo", "="}, {"", "="}}}}}, + + {"key_no_equals", + "tskv\tkey1\tkey2\n", + {Record{{{"key1", ""}, {"key2", ""}}}}}, + + {"empty_record", "tskv\t\n", {Record{{}}}}, + + {"just_empty", "", {}}, + }), + utest::PrintTestName{}); + +INSTANTIATE_TEST_SUITE_P( // + IncompleteSingleLine, TskvParseTest, + ::testing::ValuesIn(TestData{ + {"no_newline", "tskv\th=w", {Incomplete{"tskv\th=w"}}}, + {"just_t", "t", {Incomplete{"t"}}}, + {"t_newline", "t\n", {Incomplete{"t\n"}}}, + {"tskv_newline", "tskv\n", {Incomplete{"tskv\n"}}}, + {"just_newline", "\n", {Incomplete{"\n"}}}, + {"newline_tskv", "\ntskv", {Incomplete{"\ntskv"}}}, + {"just_tskv_header", "\ntskv\t", {Junk{"\n"}, Incomplete{"tskv\t"}}}, + {"no_newline_2", "\ntskv\tk", {Junk{"\n"}, Incomplete{"tskv\tk"}}}, + {"no_newline_3", "\ntskv\tk=", {Junk{"\n"}, Incomplete{"tskv\tk="}}}, + {"no_newline_4", "\ntskv\tk=v", {Junk{"\n"}, Incomplete{"tskv\tk=v"}}}, + }), + utest::PrintTestName{}); + +INSTANTIATE_TEST_SUITE_P( // + BrokenStartLine, TskvParseTest, + ::testing::ValuesIn(TestData{ + {"start_newline", "\ntskv\tk=v\n", {Junk{"\n"}, Record{{{"k", "v"}}}}}, + {"start_q_newline", + "q\ntskv\tk=v\n", + {Junk{"q\n"}, Record{{{"k", "v"}}}}}, + {"start_q_newline_newline", + "q\n\ntskv\tk=v\n", + {Junk{"q\n\n"}, Record{{{"k", "v"}}}}}, + {"start_q123_newline", + "q123\ntskv\tk=v\n", + {Junk{"q123\n"}, Record{{{"k", "v"}}}}}, + {"start_multiple_junk_newlines", + "1\n2\n3\ntskv\tk=v\n", + {Junk{"1\n2\n3\n"}, Record{{{"k", "v"}}}}}, + }), + utest::PrintTestName{}); + +INSTANTIATE_TEST_SUITE_P( // + EscapeSingleLine, TskvParseTest, + ::testing::ValuesIn(TestData{ + {"quotes", + "tskv\t\"k\"=\"v\"\t\"=\"\n", + {Record{{{"\"k\"", "\"v\""}, {"\"", "\""}}}}}, + + {"quotes_backslashes", + "tskv\t\\\"k\\\"=\\\"v\\\"\t\\\"=\\\"\n", + {Record{{{"\\\"k\\\"", "\\\"v\\\""}, {"\\\"", "\\\""}}}}}, + + // This test contains multiple invalid (hanging) backslashes. We can + // potentially give out arbitrary result for those. Still the parser + // must not skip \t and \n control characters because of them. + {"backslashes", + "tskv\t\\k=\\v\\\t\\\\=\\\n", + {Record{{{"\\k", "\\v\\"}, {"\\", "\\"}}}}}, + + {"backslashes_fixed", + "tskv\t\\k=\\v\\\\\t\\\\=\\\\\n", + {Record{{{"\\k", "\\v\\"}, {"\\", "\\"}}}}}, + + {"backslashes_and_empty_fixed", + "tskv\t\\\\k=\\\\v\\\\\t=\\\\\n", + {Record{{{"\\k", "\\v\\"}, {"", "\\"}}}}}, + + {"carriage_returns", + "tskv\t\rk\r=\rv\r\t\r=\r\n", + {Record{{{"\rk\r", "\rv\r"}, {"\r", "\r"}}}}}, + + {"code_1_char", + "tskv\t\1k\1=\1v\1\t\1=\1\n", + {Record{{{"\u0001k\u0001", "\u0001v\u0001"}, {"\u0001", "\u0001"}}}}}, + + {"newline_escape_fixed", + "tskv\t\\rk\\r=\\nv\\n\t\\n=\\b\n", + {Record{{{"\rk\r", "\nv\n"}, {"\n", "\\b"}}}}}, + + {"control_char_1f", + "tskv\t\x1fk\x1f=\x1fv\x1f\t\x1f=\x1f\n", + {Record{{{"\u001fk\u001f", "\u001fv\u001f"}, {"\u001f", "\u001f"}}}}}, + }), + utest::PrintTestName{}); + +INSTANTIATE_TEST_SUITE_P( + SingleLineEmpties, TskvParseTest, + ::testing::ValuesIn(TestData{ + {"key_missing", "tskv\t=world\n", {Record{{{"", "world"}}}}}, + {"value_missing", "tskv\thello=\n", {Record{{{"hello", ""}}}}}, + {"key_value_missing_equals", "tskv\t=\n", {Record{{{"", ""}}}}}, + {"middle_double_tab", + "tskv\t\tfoo=bar\n", + {Record{{{"", ""}, {"foo", "bar"}}}}}, + {"trailing_tab_newline", + "tskv\tfoo=bar\t\n", + {Record{{{"foo", "bar"}}}}}, + {"trailing_tab_newline_single", "tskv\t\n", {Record{{}}}}, + {"multiple_values_missing", + "tskv\tt=\tmodule=\tlevel=\t\n", + {Record{{{"t", ""}, {"module", ""}, {"level", ""}}}}}, + }), + utest::PrintTestName{}); + +INSTANTIATE_TEST_SUITE_P( + MultiLine, TskvParseTest, + ::testing::ValuesIn(TestData{ + {"multiple_logs", + "tskv\tlevel=ERROR\t_type=testing_error_log\n" + "tskv\tlevel=INFO\t_type=testing_non_error_log\n" + "tskv\tlevel=INFO\ttype=testing_another_non_error_log\n" + "tskv\tlevel=ERROR\t\n", + { + Record{{{"level", "ERROR"}, {"_type", "testing_error_log"}}}, + Record{{{"level", "INFO"}, {"_type", "testing_non_error_log"}}}, + Record{{{"level", "INFO"}, + {"type", "testing_another_non_error_log"}}}, + Record{{{"level", "ERROR"}}}, + }}, + }), + utest::PrintTestName{}); + +USERVER_NAMESPACE_END diff --git a/universal/utest/include/userver/utest/log_capture_fixture.hpp b/universal/utest/include/userver/utest/log_capture_fixture.hpp index 1577bf1d9544..742395fa64b5 100644 --- a/universal/utest/include/userver/utest/log_capture_fixture.hpp +++ b/universal/utest/include/userver/utest/log_capture_fixture.hpp @@ -3,57 +3,125 @@ /// @file userver/utest/log_capture_fixture.hpp /// @brief @copybrief utest::LogCaptureFixture -#include +#include +#include +#include +#include +#include -#include #include #include +#include +#include #include +#include +#include +#include +#include USERVER_NAMESPACE_BEGIN namespace utest { namespace impl { +class ToStringLogger; +} // namespace impl -/// @brief Thread-safe logger that allows to capture and extract log. -class ToStringLogger : public logging::impl::LoggerBase { +/// @brief Represents single log record, typically written via `LOG_*` macros. +/// @see @ref utest::LogCaptureLogger +/// @see @ref utest::LogCaptureFixture +class LogRecord final { public: - ToStringLogger() noexcept : logging::impl::LoggerBase(logging::Format::kRaw) { - logging::impl::LoggerBase::SetLevel(logging::Level::kInfo); - } + /// @returns decoded text of the log record + /// @throws if no 'text' tag in the log + const std::string& GetText() const; - void Log(logging::Level level, std::string_view str) override { - const std::lock_guard lock{log_mutex_}; - log_ += fmt::format("level={}\t{}", logging::ToString(level), str); - } + /// @returns decoded value of the tag in the log record + /// @throws if no such tag in the log + const std::string& GetTag(std::string_view key) const; - std::string ExtractLog() { - const std::lock_guard lock{log_mutex_}; - return std::exchange(log_, std::string{}); - } + /// @returns decoded value of the tag in the log record, or `std::nullopt` + const std::string* GetTagOptional(std::string_view key) const; + + /// @returns serialized log record + const std::string& GetLogRaw() const; + + /// @returns the log level of the record + logging::Level GetLevel() const; + + /// @cond + // For internal use only. + LogRecord(utils::impl::InternalTag, logging::Level level, + std::string&& log_raw); + /// @endcond private: - std::string log_; - std::mutex log_mutex_; + logging::Level level_; + std::string log_raw_; + std::vector> tags_; }; -} // namespace impl +std::ostream& operator<<(std::ostream&, const LogRecord& data); + +std::ostream& operator<<(std::ostream&, const std::vector& data); + +/// @brief A mocked logger that stores the log records in memory. +/// @see @ref utest::LogCaptureFixture +class LogCaptureLogger final { + public: + explicit LogCaptureLogger(logging::Format format = logging::Format::kRaw); + + /// @returns the mocked logger. + logging::LoggerPtr GetLogger() const; + + /// @returns all collected logs. + std::vector GetAll() const; + + /// @returns the single collected log record, then clears logs. + /// @throws std::runtime_error if there are zero or multiple log records. + LogRecord ExtractSingle(); + + /// @returns logs filtered by (optional) text substring and (optional) tags + /// substrings. + std::vector Filter( + std::string_view text_substring, + utils::span> + tag_substrings = {}) const; + + /// @returns logs filtered by an arbitrary predicate. + std::vector Filter( + utils::function_ref predicate) const; + + /// @brief Discards the collected logs. + void Clear() noexcept; + + /// @brief Logs @a value as-if using `LOG_*`, then extracts the log text. + template + std::string ToStringViaLogging(const T& value) { + Clear(); + LOG_CRITICAL() << value; + return ExtractSingle().GetText(); + } + + private: + utils::SharedRef logger_; +}; -/// @brief Fixture that allows to capture and extract log. +/// @brief Fixture that allows to capture and extract log written into the +/// default logger. +/// @see @ref utest::LogCaptureLogger template -class LogCaptureFixture : public utest::DefaultLoggerFixture { +class LogCaptureFixture : public DefaultLoggerFixture { protected: - LogCaptureFixture() : logger_(std::make_shared()) { - utest::DefaultLoggerFixture::SetDefaultLogger(logger_); + LogCaptureFixture() { + DefaultLoggerFixture::SetDefaultLogger(logger_.GetLogger()); } - /// Extract and clear current log - std::string ExtractRawLog() { return logger_->ExtractLog(); } + LogCaptureLogger& GetLogCapture() { return logger_; } private: - std::shared_ptr logger_; + LogCaptureLogger logger_; }; } // namespace utest diff --git a/universal/utest/src/utest/log_capture_fixture.cpp b/universal/utest/src/utest/log_capture_fixture.cpp new file mode 100644 index 000000000000..2d1d5036fad0 --- /dev/null +++ b/universal/utest/src/utest/log_capture_fixture.cpp @@ -0,0 +1,183 @@ +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace utest { + +namespace impl { + +class ToStringLogger : public logging::impl::LoggerBase { + public: + ToStringLogger(logging::Format format) : logging::impl::LoggerBase(format) { + UINVARIANT( + format == logging::Format::kTskv || format == logging::Format::kRaw, + "Parsing for this logging::Format is not supported"); + logging::impl::LoggerBase::SetLevel(logging::Level::kInfo); + } + + void Log(logging::Level level, std::string_view str) override { + const std::lock_guard lock{mutex_}; + records_.push_back( + LogRecord{utils::impl::InternalTag{}, level, std::string{str}}); + } + + std::vector GetAll() const { + const std::lock_guard lock{mutex_}; + return records_; + } + + std::vector Filter( + utils::function_ref predicate) const { + const std::lock_guard lock{mutex_}; + std::vector result; + for (const auto& record : records_) { + if (predicate(record)) { + result.push_back(record); + } + } + return result; + } + + void Clear() noexcept { + const std::lock_guard lock{mutex_}; + records_.clear(); + } + + private: + std::vector records_; + mutable std::mutex mutex_; +}; + +} // namespace impl + +const std::string& LogRecord::GetText() const { return GetTag("text"); } + +const std::string& LogRecord::GetTag(std::string_view key) const { + auto tag_value = GetTagOptional(key); + if (!tag_value) { + throw std::runtime_error( + fmt::format("No '{}' tag in log record:\n{}", key, log_raw_)); + } + return *std::move(tag_value); +} + +const std::string* LogRecord::GetTagOptional(std::string_view key) const { + const auto iter = + std::find_if(tags_.begin(), tags_.end(), + [&](const std::pair& tag) { + return tag.first == key; + }); + + if (iter != tags_.end()) { + return &iter->second; + } + + return nullptr; +} + +const std::string& LogRecord::GetLogRaw() const { return log_raw_; } + +logging::Level LogRecord::GetLevel() const { return level_; } + +LogRecord::LogRecord(utils::impl::InternalTag, logging::Level level, + std::string&& log_raw) + : level_(level), log_raw_(std::move(log_raw)) { + utils::encoding::TskvParser parser{log_raw_}; + + const auto on_invalid_record = [&] { + throw std::runtime_error( + fmt::format("Invalid log record: \"{}\"", log_raw_)); + }; + + const char* const record_begin = parser.SkipToRecordBegin(); + if (!record_begin || record_begin != log_raw_.data()) { + on_invalid_record(); + } + + const auto status = utils::encoding::TskvReadRecord( + parser, [&](const std::string& key, const std::string& value) { + tags_.emplace_back(key, value); + return true; + }); + + if (status == utils::encoding::TskvParser::RecordStatus::kIncomplete) { + on_invalid_record(); + } + if (parser.GetStreamPosition() != log_raw_.data() + log_raw_.size()) { + on_invalid_record(); + } +} + +std::ostream& operator<<(std::ostream& os, const LogRecord& data) { + os << data.GetLogRaw(); + return os; +} + +std::ostream& operator<<(std::ostream& os, const std::vector& data) { + for (const auto& record : data) { + os << record; + } + return os; +} + +LogCaptureLogger::LogCaptureLogger(logging::Format format) + : logger_(utils::MakeSharedRef(format)) {} + +logging::LoggerPtr LogCaptureLogger::GetLogger() const { + return logger_.GetBase(); +} + +std::vector LogCaptureLogger::GetAll() const { + return logger_->GetAll(); +} + +LogRecord LogCaptureLogger::ExtractSingle() { + auto log = GetAll(); + if (log.size() != 1) { + std::string msg = + fmt::format("There are {} log records instead of 1:\n", log.size()); + for (const auto& record : log) { + msg += record.GetLogRaw(); + } + throw std::runtime_error(msg); + } + auto single_record = std::move(log[0]); + Clear(); + return single_record; +} + +std::vector LogCaptureLogger::Filter( + std::string_view text_substring, + utils::span> tag_substrings) + const { + return Filter([&](const LogRecord& record) { + return record.GetText().find(text_substring) != std::string_view::npos && + boost::algorithm::all_of(tag_substrings, [&](const auto& kv) { + const auto* tag_value = record.GetTagOptional(kv.first); + return tag_value && + tag_value->find(kv.second) != std::string_view::npos; + }); + }); +} + +std::vector LogCaptureLogger::Filter( + utils::function_ref predicate) const { + return logger_->Filter(predicate); +} + +void LogCaptureLogger::Clear() noexcept { logger_->Clear(); } + +} // namespace utest + +USERVER_NAMESPACE_END