diff --git a/src/plugin/integration/OutboundIntegrationMessageHandler.cpp b/src/plugin/integration/OutboundIntegrationMessageHandler.cpp index bf3056203..d2438343e 100644 --- a/src/plugin/integration/OutboundIntegrationMessageHandler.cpp +++ b/src/plugin/integration/OutboundIntegrationMessageHandler.cpp @@ -3,6 +3,7 @@ #include "IntegrationConnection.h" #include "MessageInterface.h" #include "OutboundIntegrationMessageHandler.h" +#include "log/ApiLoggerInterface.h" namespace UKControllerPlugin::Integration { OutboundIntegrationMessageHandler::OutboundIntegrationMessageHandler( @@ -13,7 +14,30 @@ namespace UKControllerPlugin::Integration { void OutboundIntegrationMessageHandler::SendEvent(std::shared_ptr message) const { - LogDebug("Sending integration message: " + message->ToJson().dump()); + try { + LogDebug("Sending integration message: " + message->ToJson().dump()); + } catch (const std::exception& exception) { + if (apiLoggedTypes.find(message->GetMessageType().type) == apiLoggedTypes.end()) { + LogError( + "Failed to log integration message, something's wrong with the JSON: " + + message->GetMessageType().type); + std::string messageType = message->GetMessageType().type; + + // Add the message type to the set so we don't log it again + apiLoggedTypes.insert(messageType); + + const auto metadata = nlohmann::json{ + {"json_without_strict", + message->ToJson().dump(-1, ' ', false, nlohmann::json::error_handler_t::replace)}, + {"exception", exception.what()}}; + + ApiLogger().Log("INTEGRATION_INVALID_JSON", "Failed to log integration message", metadata); + }; + + // We'll just have to accept that we can't send this message to integrations + return; + } + std::for_each( this->clientManager->cbegin(), this->clientManager->cend(), diff --git a/src/plugin/integration/OutboundIntegrationMessageHandler.h b/src/plugin/integration/OutboundIntegrationMessageHandler.h index 9472bf079..6e148cd26 100644 --- a/src/plugin/integration/OutboundIntegrationMessageHandler.h +++ b/src/plugin/integration/OutboundIntegrationMessageHandler.h @@ -17,5 +17,8 @@ namespace UKControllerPlugin::Integration { private: const std::shared_ptr clientManager; + + // Array to ensure we only log the same message type once + mutable std::set apiLoggedTypes; }; } // namespace UKControllerPlugin::Integration diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index fe28fb501..0853f780f 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -132,6 +132,9 @@ set(log "log/LoggerBootstrap.h" "log/LoggerFunctions.cpp" "log/LoggerFunctions.h" + log/ApiLogger.cpp + log/ApiLogger.h + log/ApiLoggerInterface.h ) source_group("log" FILES ${log}) diff --git a/src/utils/api/ApiBootstrap.cpp b/src/utils/api/ApiBootstrap.cpp index 4338b9471..d0b78a7fb 100644 --- a/src/utils/api/ApiBootstrap.cpp +++ b/src/utils/api/ApiBootstrap.cpp @@ -10,6 +10,7 @@ #include "curl/CurlApi.h" #include "eventhandler/EventBus.h" #include "eventhandler/EventHandlerFlags.h" +#include "log/ApiLogger.h" #include "setting/SettingRepository.h" #include "setting/JsonFileSettingProvider.h" @@ -42,6 +43,10 @@ namespace UKControllerPluginUtils::Api { EventHandler::EventHandlerFlags::Async); SetApiRequestFactory(factory); + + // Create an API logger here and set globally + SetApiLoggerInstance(std::make_shared()); + return factory; } diff --git a/src/utils/log/ApiLogger.cpp b/src/utils/log/ApiLogger.cpp new file mode 100644 index 000000000..c2ec4b687 --- /dev/null +++ b/src/utils/log/ApiLogger.cpp @@ -0,0 +1,86 @@ +#include "ApiLogger.h" +#include "api/ApiRequestFactory.h" +#include "api/ApiRequestException.h" +#include "update/PluginVersion.h" + +namespace UKControllerPluginUtils::Log { + + struct ApiLogger::Impl + { + [[nodiscard]] auto CreatePayloadNoMetadata(const std::string& type, const std::string& message) const + -> nlohmann::json + { + return {{"type", type}, {"message", message}, {"metadata", PluginVersionMetadata().dump()}}; + } + + [[nodiscard]] auto + CreatePayload(const std::string& type, const std::string& message, const nlohmann::json& metadata) const + -> nlohmann::json + { + auto metadataWithVersion = PluginVersionMetadata(); + metadataWithVersion.update(metadata); + return {{"type", type}, {"message", message}, {"metadata", metadataWithVersion.dump()}}; + } + + [[nodiscard]] auto PluginVersionMetadata() const -> nlohmann::json + { + return {{"plugin_version", UKControllerPlugin::Plugin::PluginVersion::version}}; + } + + void WriteLog(const nlohmann::json& data) + { + ApiRequest() + .Post("plugin/logs", data) + .Then([](const UKControllerPluginUtils::Api::Response& response) { + const auto data = response.Data(); + if (!data.is_object() || !data.contains("id") || !data["id"].is_string()) { + LogError("Failed to send log to API, response was not as expected"); + return; + } + + LogInfo("Log sent to API with ID " + data["id"].get()); + }) + .Catch([](const Api::ApiRequestException& exception) { + LogError( + "Failed to send log to API, status code was " + + std::to_string(static_cast(exception.StatusCode()))); + }) + .Await(); + } + + void WriteLogAsync(const nlohmann::json& data) + { + ApiRequest().Post("plugin/logs", data).Catch([](const Api::ApiRequestException& exception) { + LogError( + "Failed to send log to API, status code was " + + std::to_string(static_cast(exception.StatusCode()))); + }); + } + }; + + ApiLogger::ApiLogger() : impl(std::make_unique()) + { + } + + ApiLogger::~ApiLogger() = default; + + void ApiLogger::Log(const std::string& type, const std::string& message) const + { + impl->WriteLog(impl->CreatePayloadNoMetadata(type, message)); + } + + void ApiLogger::Log(const std::string& type, const std::string& message, const nlohmann::json& metadata) const + { + impl->WriteLog(impl->CreatePayload(type, message, metadata)); + } + + void ApiLogger::LogAsync(const std::string& type, const std::string& message) const + { + impl->WriteLogAsync(impl->CreatePayloadNoMetadata(type, message)); + } + + void ApiLogger::LogAsync(const std::string& type, const std::string& message, const nlohmann::json& metadata) const + { + impl->WriteLogAsync(impl->CreatePayload(type, message, metadata)); + } +} // namespace UKControllerPluginUtils::Log diff --git a/src/utils/log/ApiLogger.h b/src/utils/log/ApiLogger.h new file mode 100644 index 000000000..6b29a5d6a --- /dev/null +++ b/src/utils/log/ApiLogger.h @@ -0,0 +1,20 @@ +#pragma once +#include "ApiLoggerInterface.h" + +namespace UKControllerPluginUtils::Log { + class ApiLogger : public ApiLoggerInterface + { + public: + ApiLogger(); + ~ApiLogger() override; + void Log(const std::string& type, const std::string& message) const override; + void Log(const std::string& type, const std::string& message, const nlohmann::json& metadata) const override; + void LogAsync(const std::string& type, const std::string& message) const override; + void + LogAsync(const std::string& type, const std::string& message, const nlohmann::json& metadata) const override; + + private: + struct Impl; + std::unique_ptr impl; + }; +} // namespace UKControllerPluginUtils::Log diff --git a/src/utils/log/ApiLoggerInterface.h b/src/utils/log/ApiLoggerInterface.h new file mode 100644 index 000000000..65ef74613 --- /dev/null +++ b/src/utils/log/ApiLoggerInterface.h @@ -0,0 +1,18 @@ +#pragma once + +namespace UKControllerPluginUtils::Log { + /** + * An interface for logging things to the API where we need more information + * or just want to know what's going on. + */ + class ApiLoggerInterface + { + public: + virtual ~ApiLoggerInterface() = default; + virtual void Log(const std::string& type, const std::string& message) const = 0; + virtual void Log(const std::string& type, const std::string& message, const nlohmann::json& metadata) const = 0; + virtual void LogAsync(const std::string& type, const std::string& message) const = 0; + virtual void + LogAsync(const std::string& type, const std::string& message, const nlohmann::json& metadata) const = 0; + }; +} // namespace UKControllerPluginUtils::Log diff --git a/src/utils/log/LoggerFunctions.cpp b/src/utils/log/LoggerFunctions.cpp index 0230f8218..f5b2e7734 100644 --- a/src/utils/log/LoggerFunctions.cpp +++ b/src/utils/log/LoggerFunctions.cpp @@ -1,6 +1,8 @@ +#include "log/ApiLoggerInterface.h" #include "log/LoggerFunctions.h" std::shared_ptr logger; +std::shared_ptr apiLogger; void LogCritical(std::string message) { @@ -45,13 +47,21 @@ void ShutdownLogger(void) LogInfo("Logger shutdown"); spdlog::drop_all(); logger.reset(); + apiLogger.reset(); } void LogFatalExceptionAndRethrow(const std::string& source, const std::exception& exception) { - logger->critical( - "Critical exception of type " + std::string(typeid(exception).name()) + " at " + source + ": " + - exception.what()); + const auto exceptionMessage = "Critical exception of type " + std::string(typeid(exception).name()) + " at " + + source + ": " + exception.what(); + logger->critical(exceptionMessage); + + try { + ApiLogger().Log("FATAL_EXCEPTION", exceptionMessage); + } catch (const std::exception& e) { + LogCritical("Exception caught in LogFatalExceptionAndRethrow: " + std::string(e.what())); + } + throw; } @@ -60,3 +70,22 @@ void LogFatalExceptionAndRethrow( { LogFatalExceptionAndRethrow(source + "::" + subsource, exception); } + +void SetApiLoggerInstance(std::shared_ptr instance) +{ + if (apiLogger) { + return; + } + + apiLogger = instance; +} + +auto ApiLogger() -> const UKControllerPluginUtils::Log::ApiLoggerInterface& +{ + if (!apiLogger) { + LogError("ApiLogger not set"); + throw std::runtime_error("ApiLogger not set"); + } + + return *apiLogger; +} diff --git a/src/utils/log/LoggerFunctions.h b/src/utils/log/LoggerFunctions.h index bfc233180..1309c78f9 100644 --- a/src/utils/log/LoggerFunctions.h +++ b/src/utils/log/LoggerFunctions.h @@ -4,6 +4,11 @@ namespace spdlog { class logger; } // namespace spdlog +namespace UKControllerPluginUtils::Log { + class ApiLoggerInterface; +} + +[nodiscard] auto ApiLogger() -> const UKControllerPluginUtils::Log::ApiLoggerInterface&; void LogFatalExceptionAndRethrow(const std::string& source, const std::exception& exception); void LogFatalExceptionAndRethrow( const std::string& source, const std::string& subsource, const std::exception& exception); @@ -13,4 +18,5 @@ void LogError(std::string message); void LogInfo(std::string message); void LogWarning(std::string message); void SetLoggerInstance(std::shared_ptr instance); +void SetApiLoggerInstance(std::shared_ptr instance); void ShutdownLogger(void); diff --git a/test/testingutils/test/ApiTestCase.h b/test/testingutils/test/ApiTestCase.h index 2c222d76e..94d6f32d6 100644 --- a/test/testingutils/test/ApiTestCase.h +++ b/test/testingutils/test/ApiTestCase.h @@ -1,5 +1,8 @@ #pragma once #include "ApiMethodExpectation.h" +#include "ApiUriExpectation.h" +#include "ApiRequestExpectation.h" +#include "ApiResponseExpectation.h" namespace UKControllerPluginUtils::Api { class ApiFactory; diff --git a/test/utils/CMakeLists.txt b/test/utils/CMakeLists.txt index 894b66ef7..c3db43c5c 100644 --- a/test/utils/CMakeLists.txt +++ b/test/utils/CMakeLists.txt @@ -69,6 +69,7 @@ source_group("test\\http" FILES ${test__http}) set(test__log "log/LoggerBootstrapTest.cpp" + log/ApiLoggerTest.cpp ) source_group("test\\log" FILES ${test__log}) diff --git a/test/utils/log/ApiLoggerTest.cpp b/test/utils/log/ApiLoggerTest.cpp new file mode 100644 index 000000000..4ba64e9fe --- /dev/null +++ b/test/utils/log/ApiLoggerTest.cpp @@ -0,0 +1,58 @@ +#include "log/ApiLogger.h" +#include "test/ApiTestCase.h" +#include "update/PluginVersion.h" + +namespace UKControllerPluginUtilsTest::Api { + class ApiLoggerTest : public UKControllerPluginTest::ApiTestCase + { + public: + ApiLoggerTest() : ApiTestCase() + { + } + + const nlohmann::json expectedPluginVersionMetadata = { + {"plugin_version", UKControllerPlugin::Plugin::PluginVersion::version}}; + + UKControllerPluginUtils::Log::ApiLogger logger; + }; + + TEST_F(ApiLoggerTest, ItLogsSync) + { + const nlohmann::json expectedPayload = { + {"type", "type"}, {"message", "message"}, {"metadata", expectedPluginVersionMetadata.dump()}}; + + this->ExpectApiRequest()->Post().To("plugin/logs").WithBody(expectedPayload).WillReturnCreated(); + logger.Log("type", "message"); + } + + TEST_F(ApiLoggerTest, ItLogsSyncWithMetadata) + { + nlohmann::json metadata = {{"key", "value"}}; + metadata.update(expectedPluginVersionMetadata); + const nlohmann::json expectedPayload = { + {"type", "type"}, {"message", "message"}, {"metadata", metadata.dump()}}; + + this->ExpectApiRequest()->Post().To("plugin/logs").WithBody(expectedPayload).WillReturnCreated(); + logger.Log("type", "message", metadata); + } + + TEST_F(ApiLoggerTest, ItLogsAsync) + { + const nlohmann::json expectedPayload = { + {"type", "type"}, {"message", "message"}, {"metadata", expectedPluginVersionMetadata.dump()}}; + + this->ExpectApiRequest()->Post().To("plugin/logs").WithBody(expectedPayload).WillReturnCreated(); + logger.LogAsync("type", "message"); + } + + TEST_F(ApiLoggerTest, ItLogsAsyncWithMetadata) + { + nlohmann::json metadata = {{"key", "value"}}; + metadata.update(expectedPluginVersionMetadata); + const nlohmann::json expectedPayload = { + {"type", "type"}, {"message", "message"}, {"metadata", metadata.dump()}}; + + this->ExpectApiRequest()->Post().To("plugin/logs").WithBody(expectedPayload).WillReturnCreated(); + logger.LogAsync("type", "message", metadata); + } +} // namespace UKControllerPluginUtilsTest::Api