From a99a28549560a75c40cfc48f147e6301c3914fde Mon Sep 17 00:00:00 2001 From: Ulrich Hertlein Date: Thu, 16 May 2024 12:49:37 +0200 Subject: [PATCH] Add logging of per-context variables to logging system and network stack (#1507) * Minor code cleanups related to OLPEDGE-2893 Relates-to: OLPEDGE-2893 Signed-off-by: Hertlein, Ulrich * Add per-thread context values to logging system This change adds a per-thread logging context to the logging system, which can be set by the user and will then be logged from all threads and tasks created with that context. This is useful to correlate requests from the client to any network requests made based on that request. The logging formatter must be set up by including a `MessageFormatter::ElementType::ContextValue` with the 'format' set to the context key to use. Resolves: OLPEDGE-2893 Signed-off-by: Hertlein, Ulrich * Store+track log context for Curl network stack When a network requests is created, the current log context is captured and subsequently made active whenever this request is processed. Resolves: OLPEDGE-2893 Signed-off-by: Hertlein, Ulrich --------- Signed-off-by: Hertlein, Ulrich Co-authored-by: Hertlein, Ulrich --- olp-cpp-sdk-core/CMakeLists.txt | 2 + .../include/olp/core/logging/Log.h | 4 +- .../include/olp/core/logging/LogContext.h | 81 ++++++++++ .../olp/core/logging/MessageFormatter.h | 5 +- .../src/http/curl/NetworkCurl.cpp | 28 ++-- olp-cpp-sdk-core/src/http/curl/NetworkCurl.h | 4 +- olp-cpp-sdk-core/src/logging/Log.cpp | 1 + olp-cpp-sdk-core/src/logging/LogContext.cpp | 79 +++++++++ .../src/logging/MessageFormatter.cpp | 10 +- .../src/thread/ThreadPoolTaskScheduler.cpp | 26 ++- .../tests/logging/MessageFormatterTest.cpp | 152 +++++++++++++++++- tests/common/ResponseGenerator.h | 1 + 12 files changed, 368 insertions(+), 25 deletions(-) create mode 100644 olp-cpp-sdk-core/include/olp/core/logging/LogContext.h create mode 100644 olp-cpp-sdk-core/src/logging/LogContext.cpp diff --git a/olp-cpp-sdk-core/CMakeLists.txt b/olp-cpp-sdk-core/CMakeLists.txt index 9679120f5..b8ecc686a 100644 --- a/olp-cpp-sdk-core/CMakeLists.txt +++ b/olp-cpp-sdk-core/CMakeLists.txt @@ -156,6 +156,7 @@ set(OLP_SDK_LOGGING_HEADERS ./include/olp/core/logging/Format.h ./include/olp/core/logging/Level.h ./include/olp/core/logging/Log.h + ./include/olp/core/logging/LogContext.h ./include/olp/core/logging/LogMessage.h ./include/olp/core/logging/MessageFormatter.h ) @@ -361,6 +362,7 @@ set(OLP_SDK_LOGGING_SOURCES ./src/logging/FilterGroup.cpp ./src/logging/Format.cpp ./src/logging/Log.cpp + ./src/logging/LogContext.cpp ./src/logging/MessageFormatter.cpp ./src/logging/ThreadId.cpp ./src/logging/ThreadId.h diff --git a/olp-cpp-sdk-core/include/olp/core/logging/Log.h b/olp-cpp-sdk-core/include/olp/core/logging/Log.h index 5830a2d67..8483f7603 100644 --- a/olp-cpp-sdk-core/include/olp/core/logging/Log.h +++ b/olp-cpp-sdk-core/include/olp/core/logging/Log.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 HERE Europe B.V. + * Copyright (C) 2019-2024 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,7 @@ */ #define OLP_SDK_DO_LOG(level, tag, message) \ do { \ - std::stringstream __strm; \ + std::ostringstream __strm; \ __strm << message; \ ::olp::logging::Log::logMessage(level, tag, __strm.str(), __FILE__, \ __LINE__, __FUNCTION__, \ diff --git a/olp-cpp-sdk-core/include/olp/core/logging/LogContext.h b/olp-cpp-sdk-core/include/olp/core/logging/LogContext.h new file mode 100644 index 000000000..e6e5f7f42 --- /dev/null +++ b/olp-cpp-sdk-core/include/olp/core/logging/LogContext.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#pragma once + +#include + +#include +#include +#include +#include + +/** + * @brief The LogContext object stores per-thread key/value pairs that can be + * picked up in log messages using the + * 'olp::logging::MessageFormatter::ElementType::ContextValue' logging element. + */ +namespace olp { +namespace logging { +/// A context key/value map. The keys are required to be const and immutable. +using LogContext = std::unordered_map; + +/// A function that sets the current log context. +using LogContextSetter = std::function)>; + +/// A function that gets the current log context. +using LogContextGetter = std::function()>; + +/** + * Sets the getter and setter for the log context. + * + * @param[in] getter The log context getter, or the default getter if + * nullptr. + * @param[in] setter The log context setter, or the default setter if + * nullptr. + */ +CORE_API void SetLogContextGetterSetter(LogContextGetter getter, + LogContextSetter setter); + +/// Gets the current thread context. +CORE_API std::shared_ptr GetContext(); + +/// Gets a value from the current thread context. +CORE_API const std::string& GetContextValue(const std::string& key); + +/** + * @brief The ScopedLogContext class takes ownership of a log context + * and makes it active on construction and restores the previous context on + * destruction. + * + * Passing 'nullptr' is the same as passing an empty LogContext. + * + * @see MessageFormatter::ElementType::Context + */ +class CORE_API ScopedLogContext final { + public: + ScopedLogContext(const std::shared_ptr& context); + ~ScopedLogContext(); + + private: + std::shared_ptr context_; + std::shared_ptr prev_context_; +}; +} // namespace logging +} // namespace olp diff --git a/olp-cpp-sdk-core/include/olp/core/logging/MessageFormatter.h b/olp-cpp-sdk-core/include/olp/core/logging/MessageFormatter.h index 30d560272..c4e028a41 100644 --- a/olp-cpp-sdk-core/include/olp/core/logging/MessageFormatter.h +++ b/olp-cpp-sdk-core/include/olp/core/logging/MessageFormatter.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 HERE Europe B.V. + * Copyright (C) 2019-2024 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -91,8 +91,9 @@ class CORE_API MessageFormatter { /// `strftime()`. TimeMs, ///< The millisecond portion of the timestamp. It is formatted as ///< an unsigned integer. - ThreadId ///< The thread ID of the thread that generated the message. + ThreadId, ///< The thread ID of the thread that generated the message. ///< It is formatted as an unsigned long. + ContextValue, ///< A key/value literal from LogContext; 'format' is the key to look up. }; /** diff --git a/olp-cpp-sdk-core/src/http/curl/NetworkCurl.cpp b/olp-cpp-sdk-core/src/http/curl/NetworkCurl.cpp index 78165fb70..e6669de4e 100644 --- a/olp-cpp-sdk-core/src/http/curl/NetworkCurl.cpp +++ b/olp-cpp-sdk-core/src/http/curl/NetworkCurl.cpp @@ -844,6 +844,7 @@ NetworkCurl::RequestHandle* NetworkCurl::GetHandle( handle.send_time = std::chrono::steady_clock::now(); handle.error_text[0] = 0; handle.skip_content = false; + handle.log_context = logging::GetContext(); return &handle; } @@ -1004,8 +1005,9 @@ void NetworkCurl::CompleteMessage(CURL* handle, CURLcode result) { int index = GetHandleIndex(handle); if (index >= 0 && index < static_cast(handles_.size())) { RequestHandle& rhandle = handles_[index]; - + logging::ScopedLogContext scopedLogContext(rhandle.log_context); auto callback = rhandle.callback; + if (!callback) { OLP_SDK_LOG_WARNING( kLogTag, @@ -1111,35 +1113,36 @@ void NetworkCurl::Run() { events_.pop_front(); // Only handle handles that are actually used - if (!event.handle->in_use) { + auto* rhandle = event.handle; + if (!rhandle->in_use) { continue; } if (event.type == EventInfo::Type::SEND_EVENT) { - auto res = curl_multi_add_handle(curl_, event.handle->handle); + auto res = curl_multi_add_handle(curl_, rhandle->handle); if ((res != CURLM_OK) && (res != CURLM_CALL_MULTI_PERFORM)) { - OLP_SDK_LOG_ERROR( - kLogTag, "Send failed, id=" << event.handle->id << ", error=" - << curl_multi_strerror(res)); + OLP_SDK_LOG_ERROR(kLogTag, + "Send failed, id=" << rhandle->id << ", error=" + << curl_multi_strerror(res)); // Do not add the handle to msgs vector in case it is a duplicate // handle error as it will be reset in CompleteMessage handler, // and curl will crash in the next call of curl_multi_perform // function. In any other case, lets complete the message. if (res != CURLM_ADDED_ALREADY) { - msgs.push_back(event.handle->handle); + msgs.push_back(rhandle->handle); } } } else { // Request was cancelled, so lets remove it from curl - auto code = curl_multi_remove_handle(curl_, event.handle->handle); + auto code = curl_multi_remove_handle(curl_, rhandle->handle); if (code != CURLM_OK) { OLP_SDK_LOG_ERROR(kLogTag, "curl_multi_remove_handle failed, error=" << curl_multi_strerror(code)); } lock.unlock(); - CompleteMessage(event.handle->handle, CURLE_OPERATION_TIMEDOUT); + CompleteMessage(rhandle->handle, CURLE_OPERATION_TIMEDOUT); lock.lock(); } } @@ -1195,7 +1198,10 @@ void NetworkCurl::Run() { int handle_index = GetHandleIndex(handle); if (handle_index >= 0) { RequestHandle& rhandle = handles_[handle_index]; - if (!rhandle.callback) { + logging::ScopedLogContext scopedLogContext(rhandle.log_context); + + auto callback = rhandle.callback; + if (!callback) { OLP_SDK_LOG_WARNING( kLogTag, "Request completed without callback, id=" << rhandle.id); @@ -1208,7 +1214,7 @@ void NetworkCurl::Run() { .WithError("CURL error") .WithBytesDownloaded(download_bytes) .WithBytesUploaded(upload_bytes); - rhandle.callback(response); + callback(response); lock.lock(); } diff --git a/olp-cpp-sdk-core/src/http/curl/NetworkCurl.h b/olp-cpp-sdk-core/src/http/curl/NetworkCurl.h index d40b0b6aa..3f9a182f0 100644 --- a/olp-cpp-sdk-core/src/http/curl/NetworkCurl.h +++ b/olp-cpp-sdk-core/src/http/curl/NetworkCurl.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 HERE Europe B.V. + * Copyright (C) 2019-2024 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,6 +59,7 @@ #include "olp/core/http/Network.h" #include "olp/core/http/NetworkInitializationSettings.h" #include "olp/core/http/NetworkRequest.h" +#include "olp/core/logging/LogContext.h" namespace olp { namespace http { @@ -134,6 +135,7 @@ class NetworkCurl : public olp::http::Network, bool cancelled{}; bool skip_content{}; char error_text[CURL_ERROR_SIZE]{}; + std::shared_ptr log_context; }; /** diff --git a/olp-cpp-sdk-core/src/logging/Log.cpp b/olp-cpp-sdk-core/src/logging/Log.cpp index 5e67f7196..93f0fb040 100644 --- a/olp-cpp-sdk-core/src/logging/Log.cpp +++ b/olp-cpp-sdk-core/src/logging/Log.cpp @@ -180,6 +180,7 @@ void LogImpl::appendLogItem(const LogItem& log_item) { appender_with_log_level.appender->append(log_item); } } + // implementation of public static Log API //-------------------------------------------------------- diff --git a/olp-cpp-sdk-core/src/logging/LogContext.cpp b/olp-cpp-sdk-core/src/logging/LogContext.cpp new file mode 100644 index 000000000..7e620db90 --- /dev/null +++ b/olp-cpp-sdk-core/src/logging/LogContext.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2019-2024 HERE Europe B.V. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +#include + +#include + +namespace olp { +namespace logging { +namespace { +thread_local std::shared_ptr tls_logContext; + +LogContextSetter s_defaultLogContextSetter = + [](std::shared_ptr context) { + tls_logContext = std::move(context); + }; + +LogContextSetter s_logContextSetter = s_defaultLogContextSetter; + +LogContextGetter s_defaultLogContextGetter = []() { return tls_logContext; }; +LogContextGetter s_logContextGetter = s_defaultLogContextGetter; + +} // namespace + +void SetLogContextGetterSetter(LogContextGetter getter, + LogContextSetter setter) { + s_logContextGetter = getter ? std::move(getter) : s_defaultLogContextGetter; + s_logContextSetter = setter ? std::move(setter) : s_defaultLogContextSetter; +} + +std::shared_ptr GetContext() { + assert(s_logContextGetter && "Invalid LogContext getter"); + return s_logContextGetter(); +} + +const std::string& GetContextValue(const std::string& key) { + static std::string s_empty = ""; + auto ctx = GetContext(); + if (!ctx || key.empty()) + return s_empty; + + auto it = ctx->find(key); + if (it == ctx->end()) + return s_empty; + + return it->second; +} + +ScopedLogContext::ScopedLogContext( + const std::shared_ptr& context) + : context_(context) { + prev_context_ = GetContext(); + assert(s_logContextSetter && "Invalid LogContext setter"); + s_logContextSetter(context_); +} + +ScopedLogContext::~ScopedLogContext() { + assert(s_logContextSetter && "Invalid LogContext setter"); + s_logContextSetter(std::move(prev_context_)); +} + +} // namespace logging +} // namespace olp diff --git a/olp-cpp-sdk-core/src/logging/MessageFormatter.cpp b/olp-cpp-sdk-core/src/logging/MessageFormatter.cpp index 0bbc9ab21..f1907e7ae 100644 --- a/olp-cpp-sdk-core/src/logging/MessageFormatter.cpp +++ b/olp-cpp-sdk-core/src/logging/MessageFormatter.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 HERE Europe B.V. + * Copyright (C) 2019-2024 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,12 @@ */ #include +#include #include + #include #include #include -#include #include namespace olp { @@ -180,6 +181,11 @@ std::string MessageFormatter::format(const LogMessage& message) const { curElement = curElementBuffer.format(element.format.c_str(), message.threadId); break; + case ElementType::ContextValue: + // 'format' is key to lookup + curElement = curElementBuffer.format( + "%s", GetContextValue(element.format).c_str()); + break; default: continue; } diff --git a/olp-cpp-sdk-core/src/thread/ThreadPoolTaskScheduler.cpp b/olp-cpp-sdk-core/src/thread/ThreadPoolTaskScheduler.cpp index d8a915990..7b4b569cf 100644 --- a/olp-cpp-sdk-core/src/thread/ThreadPoolTaskScheduler.cpp +++ b/olp-cpp-sdk-core/src/thread/ThreadPoolTaskScheduler.cpp @@ -32,7 +32,7 @@ #include #include "olp/core/logging/Log.h" -#include "olp/core/porting/make_unique.h" +#include "olp/core/logging/LogContext.h" #include "olp/core/porting/platform.h" #include "olp/core/thread/SyncQueue.h" #include "olp/core/utils/WarningWorkarounds.h" @@ -127,12 +127,32 @@ ThreadPoolTaskScheduler::~ThreadPoolTaskScheduler() { } void ThreadPoolTaskScheduler::EnqueueTask(TaskScheduler::CallFuncType&& func) { - queue_->Push({std::move(func), thread::NORMAL}); + EnqueueTask(std::move(func), thread::NORMAL); } void ThreadPoolTaskScheduler::EnqueueTask(TaskScheduler::CallFuncType&& func, uint32_t priority) { - queue_->Push({std::move(func), priority}); + auto logContext = logging::GetContext(); + +#if __cplusplus >= 201402L + // At least C++14, use generalized lambda capture + auto funcWithCapturedLogContext = [logContext = std::move(logContext), + func = std::move(func)]() { + olp::logging::ScopedLogContext scopedContext(logContext); + func(); + }; +#else + // C++11 does not support generalized lambda capture :( + auto funcWithCapturedLogContext = std::bind( + [](std::shared_ptr& logContext, + TaskScheduler::CallFuncType& func) { + olp::logging::ScopedLogContext scopedContext(logContext); + func(); + }, + std::move(logContext), std::move(func)); +#endif + + queue_->Push({std::move(funcWithCapturedLogContext), priority}); } } // namespace thread diff --git a/olp-cpp-sdk-core/tests/logging/MessageFormatterTest.cpp b/olp-cpp-sdk-core/tests/logging/MessageFormatterTest.cpp index 91dd15143..786c267a6 100644 --- a/olp-cpp-sdk-core/tests/logging/MessageFormatterTest.cpp +++ b/olp-cpp-sdk-core/tests/logging/MessageFormatterTest.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 HERE Europe B.V. + * Copyright (C) 2019-2024 HERE Europe B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,16 @@ #include +#include #include +#include + namespace { -using namespace olp::logging; +using olp::logging::Level; +using olp::logging::LogMessage; +using olp::logging::MessageFormatter; TEST(MessageFormatterTest, ElementConstructors) { MessageFormatter::Element string_element( @@ -268,6 +273,145 @@ TEST(MessageFormatterTest, Format) { formatter.format(message)); } +TEST(MessageFormatterTest, FormatContextValue) { + // No active context + EXPECT_TRUE(olp::logging::GetContextValue("foo").empty()); + + // Empty key + EXPECT_TRUE(olp::logging::GetContextValue("").empty()); + + // Set up message formatter to log + // "foo=" + value_of_key['foo'] + ",bar=" + value_of_key['bar'] + std::vector elements = { + MessageFormatter::Element(MessageFormatter::ElementType::String, "foo="), + MessageFormatter::Element(MessageFormatter::ElementType::ContextValue, + "foo"), + MessageFormatter::Element(MessageFormatter::ElementType::String, ",bar="), + MessageFormatter::Element(MessageFormatter::ElementType::ContextValue, + "bar")}; + MessageFormatter formatter(elements); + + { + auto context = std::make_shared(); + (*context)["foo"] = "baz"; + olp::logging::ScopedLogContext scopedContext(context); + EXPECT_EQ("foo=baz,bar=", formatter.format({})); + } + + { + auto context = std::make_shared(); + (*context)["bar"] = "baz"; + olp::logging::ScopedLogContext scopedContext(context); + EXPECT_EQ("foo=,bar=baz", formatter.format({})); + } + + { + auto context = std::make_shared(); + (*context)["foo"] = "baz"; + (*context)["bar"] = "baz"; + olp::logging::ScopedLogContext scopedContext(context); + EXPECT_EQ("foo=baz,bar=baz", formatter.format({})); + } +} + +TEST(MessageFormatterTest, FormatContextValue_Threading) { + // Log "foo=" + value_of_key['foo'] + std::vector elements = { + MessageFormatter::Element(MessageFormatter::ElementType::String, "foo="), + MessageFormatter::Element(MessageFormatter::ElementType::ContextValue, + "foo")}; + MessageFormatter formatter(elements); + + auto testingTesting = [&formatter]() { + // Create context, but don't make it active yet + auto context = std::make_shared(); + (*context)["foo"] = "bar"; + + // No active context + EXPECT_EQ("foo=", formatter.format({})); + + { + // Push log context + olp::logging::ScopedLogContext scopedContext(context); + EXPECT_EQ("foo=bar", formatter.format({})); + + { + // Push same log context again + olp::logging::ScopedLogContext scopedContext(context); + EXPECT_EQ("foo=bar", formatter.format({})); + } + { + // Push different context + auto context = std::make_shared(); + (*context)["foo"] = "baz"; + + olp::logging::ScopedLogContext scopedContext(context); + EXPECT_EQ("foo=baz", formatter.format({})); + } + EXPECT_EQ("foo=bar", formatter.format({})); + + { + // Push an empty log context + olp::logging::ScopedLogContext scopedContext(nullptr); + EXPECT_EQ("foo=", formatter.format({})); + } + EXPECT_EQ("foo=bar", formatter.format({})); + } + EXPECT_EQ("foo=", formatter.format({})); + }; + + testingTesting(); + + auto bazFut = + std::async(std::launch::async, [&testingTesting]() { testingTesting(); }); + bazFut.wait(); +} + +TEST(MessageFormatterTest, FormatContextValue_SetterGetter) { + // Log "foo=" + value_of_key['foo'] + std::vector elements = { + MessageFormatter::Element(MessageFormatter::ElementType::String, "foo="), + MessageFormatter::Element(MessageFormatter::ElementType::ContextValue, + "foo")}; + MessageFormatter formatter(elements); + + auto ourLogContext = std::make_shared(); + + uint32_t numGetterCalled = 0; + auto getter = [&ourLogContext, &numGetterCalled]() { + ++numGetterCalled; + return ourLogContext; + }; + + uint32_t numSetterCalled = 0; + auto setter = + [&ourLogContext, &numSetterCalled]( + std::shared_ptr logContext) { + ++numSetterCalled; + ourLogContext = logContext; + }; + olp::logging::SetLogContextGetterSetter(getter, setter); + + // No active context + EXPECT_EQ("foo=", formatter.format({})); + EXPECT_EQ(numGetterCalled, 1); // 1x format + EXPECT_EQ(numSetterCalled, 0); + + { + auto context = std::make_shared(); + (*context)["foo"] = "baz"; + olp::logging::ScopedLogContext scopedContext(context); + EXPECT_EQ("foo=baz", formatter.format({})); + + EXPECT_EQ(numGetterCalled, 3); // Previous + 1x SLC ctor + 1x format + EXPECT_EQ(numSetterCalled, 1); // 1x SLC dtor + } + + // Restore to defaults + olp::logging::SetLogContextGetterSetter(nullptr, nullptr); + EXPECT_EQ("foo=", formatter.format({})); // Not set in default log context +} + TEST(MessageFormatterTest, ThreadId) { static const unsigned long kThreadId1 = 1; static const unsigned long kThreadId2 = 2; @@ -288,8 +432,8 @@ TEST(MessageFormatterTest, ThreadId) { message.threadId = kThreadId2; std::string thread2_message = formatter.format(message); - EXPECT_EQ(format("%lu", kThreadId1), thread1_message); - EXPECT_EQ(format("%lu", kThreadId2), thread2_message); + EXPECT_EQ(olp::logging::format("%lu", kThreadId1), thread1_message); + EXPECT_EQ(olp::logging::format("%lu", kThreadId2), thread2_message); } TEST(MessageFormatterTest, TagLimits) { diff --git a/tests/common/ResponseGenerator.h b/tests/common/ResponseGenerator.h index c6af89018..337148a62 100644 --- a/tests/common/ResponseGenerator.h +++ b/tests/common/ResponseGenerator.h @@ -19,6 +19,7 @@ #pragma once +#include #include #include