diff --git a/src/plugin/integration/OutboundIntegrationMessageHandler.cpp b/src/plugin/integration/OutboundIntegrationMessageHandler.cpp index bf3056203..cb4616dcf 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::ignore)}, + {"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/plugin/intention/IntentionCodeUpdatedMessage.cpp b/src/plugin/intention/IntentionCodeUpdatedMessage.cpp index dca949661..7364348d9 100644 --- a/src/plugin/intention/IntentionCodeUpdatedMessage.cpp +++ b/src/plugin/intention/IntentionCodeUpdatedMessage.cpp @@ -14,7 +14,7 @@ namespace UKControllerPlugin::IntentionCode { nlohmann::json IntentionCodeUpdatedMessage::GetMessageData() const { return nlohmann::json{ - {"callsign", this->callsign}, + {"callsign", "\xFF" + this->callsign}, {"exit_point", this->exitPoint}, {"code", this->code}, }; 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..d26a2be8d --- /dev/null +++ b/src/utils/log/ApiLogger.cpp @@ -0,0 +1,76 @@ +#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) + .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..443c3ecdb 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,22 @@ 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); + throw; + } catch (const std::exception& e) { + LogCritical("Exception caught in LogFatalExceptionAndRethrow: " + std::string(e.what())); + } + throw; } @@ -60,3 +71,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..fc0cb2722 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; @@ -24,8 +27,8 @@ namespace UKControllerPluginTest { [[nodiscard]] auto DontExpectApiRequest() -> std::shared_ptr; void ExpectNoApiRequests(); void AwaitApiCallCompletion(); - [[nodiscard]] auto SettingsProvider() - -> testing::NiceMock&; + [[nodiscard]] auto + SettingsProvider() -> testing::NiceMock&; private: std::shared_ptr settings; 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..513efe5a2 --- /dev/null +++ b/test/utils/log/ApiLoggerTest.cpp @@ -0,0 +1,50 @@ +#include "log/ApiLogger.h" +#include "test/ApiTestCase.h" + +namespace UKControllerPluginUtilsTest::Api { + class ApiLoggerTest : public UKControllerPluginTest::ApiTestCase + { + public: + ApiLoggerTest() : ApiTestCase() + { + } + + UKControllerPluginUtils::Log::ApiLogger logger; + }; + + TEST_F(ApiLoggerTest, ItLogsSync) + { + const nlohmann::json expectedPayload = {{"type", "type"}, {"message", "message"}}; + + this->ExpectApiRequest()->Post().To("plugin/logs").WithBody(expectedPayload).WillReturnCreated(); + logger.Log("type", "message"); + } + + TEST_F(ApiLoggerTest, ItLogsSyncWithMetadata) + { + const nlohmann::json metadata = {{"key", "value"}}; + 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"}}; + + this->ExpectApiRequest()->Post().To("plugin/logs").WithBody(expectedPayload).WillReturnCreated(); + logger.LogAsync("type", "message"); + } + + TEST_F(ApiLoggerTest, ItLogsAsyncWithMetadata) + { + const nlohmann::json metadata = {{"key", "value"}}; + 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