Skip to content

Commit

Permalink
Merge pull request #556 from AndyTWF/send-exception-info-to-api
Browse files Browse the repository at this point in the history
feat: log fatal exceptions to the api
  • Loading branch information
AndyTWF authored Apr 15, 2024
2 parents daba080 + 4ce2292 commit 66202f0
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 4 deletions.
26 changes: 25 additions & 1 deletion src/plugin/integration/OutboundIntegrationMessageHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "IntegrationConnection.h"
#include "MessageInterface.h"
#include "OutboundIntegrationMessageHandler.h"
#include "log/ApiLoggerInterface.h"

namespace UKControllerPlugin::Integration {
OutboundIntegrationMessageHandler::OutboundIntegrationMessageHandler(
Expand All @@ -13,7 +14,30 @@ namespace UKControllerPlugin::Integration {

void OutboundIntegrationMessageHandler::SendEvent(std::shared_ptr<MessageInterface> 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(),
Expand Down
3 changes: 3 additions & 0 deletions src/plugin/integration/OutboundIntegrationMessageHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@ namespace UKControllerPlugin::Integration {

private:
const std::shared_ptr<IntegrationClientManager> clientManager;

// Array to ensure we only log the same message type once
mutable std::set<std::string> apiLoggedTypes;
};
} // namespace UKControllerPlugin::Integration
3 changes: 3 additions & 0 deletions src/utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})

Expand Down
5 changes: 5 additions & 0 deletions src/utils/api/ApiBootstrap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -42,6 +43,10 @@ namespace UKControllerPluginUtils::Api {
EventHandler::EventHandlerFlags::Async);

SetApiRequestFactory(factory);

// Create an API logger here and set globally
SetApiLoggerInstance(std::make_shared<Log::ApiLogger>());

return factory;
}

Expand Down
86 changes: 86 additions & 0 deletions src/utils/log/ApiLogger.cpp
Original file line number Diff line number Diff line change
@@ -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<std::string>());
})
.Catch([](const Api::ApiRequestException& exception) {
LogError(
"Failed to send log to API, status code was " +
std::to_string(static_cast<uint64_t>(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<uint64_t>(exception.StatusCode())));
});
}
};

ApiLogger::ApiLogger() : impl(std::make_unique<Impl>())
{
}

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
20 changes: 20 additions & 0 deletions src/utils/log/ApiLogger.h
Original file line number Diff line number Diff line change
@@ -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> impl;
};
} // namespace UKControllerPluginUtils::Log
18 changes: 18 additions & 0 deletions src/utils/log/ApiLoggerInterface.h
Original file line number Diff line number Diff line change
@@ -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
35 changes: 32 additions & 3 deletions src/utils/log/LoggerFunctions.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "log/ApiLoggerInterface.h"
#include "log/LoggerFunctions.h"

std::shared_ptr<spdlog::logger> logger;
std::shared_ptr<UKControllerPluginUtils::Log::ApiLoggerInterface> apiLogger;

void LogCritical(std::string message)
{
Expand Down Expand Up @@ -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;
}

Expand All @@ -60,3 +70,22 @@ void LogFatalExceptionAndRethrow(
{
LogFatalExceptionAndRethrow(source + "::" + subsource, exception);
}

void SetApiLoggerInstance(std::shared_ptr<UKControllerPluginUtils::Log::ApiLoggerInterface> 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;
}
6 changes: 6 additions & 0 deletions src/utils/log/LoggerFunctions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -13,4 +18,5 @@ void LogError(std::string message);
void LogInfo(std::string message);
void LogWarning(std::string message);
void SetLoggerInstance(std::shared_ptr<spdlog::logger> instance);
void SetApiLoggerInstance(std::shared_ptr<UKControllerPluginUtils::Log::ApiLoggerInterface> instance);
void ShutdownLogger(void);
3 changes: 3 additions & 0 deletions test/testingutils/test/ApiTestCase.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#pragma once
#include "ApiMethodExpectation.h"
#include "ApiUriExpectation.h"
#include "ApiRequestExpectation.h"
#include "ApiResponseExpectation.h"

namespace UKControllerPluginUtils::Api {
class ApiFactory;
Expand Down
1 change: 1 addition & 0 deletions test/utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})

Expand Down
58 changes: 58 additions & 0 deletions test/utils/log/ApiLoggerTest.cpp
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 66202f0

Please sign in to comment.