diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f68e2e0..43d106e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,11 @@ if(PSEMEK_USE_FREETYPE) list(APPEND PSEMEK_DEFINITIONS "-DPSEMEK_USE_FREETYPE=1") endif() +option(PSEMEK_USE_SQLITE "Include sqlite3 database support for journaling" OFF) +if(PSEMEK_USE_SQLITE) + list(APPEND PSEMEK_DEFINITIONS "-DPSEMEK_USE_SQLITE=1") +endif() + set(PSEMEK_CXX_FLAGS) if((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") OR (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")) list(APPEND PSEMEK_CXX_FLAGS -Wall -Werror -Wextra -pedantic -Wno-narrowing -Wno-sign-compare) diff --git a/libs/journal/CMakeLists.txt b/libs/journal/CMakeLists.txt new file mode 100644 index 00000000..8cca1e28 --- /dev/null +++ b/libs/journal/CMakeLists.txt @@ -0,0 +1,10 @@ +file(GLOB_RECURSE PSEMEK_JOURNAL_HEADERS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "include/*.hpp") +file(GLOB_RECURSE PSEMEK_JOURNAL_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "source/*.cpp") + +if(PSEMEK_USE_SQLITE) + find_package(SQLite3 REQUIRED) +endif() + +psemek_add_library(psemek-journal ${PSEMEK_JOURNAL_HEADERS} ${PSEMEK_JOURNAL_SOURCES}) +target_include_directories(psemek-journal PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ${SQLite3_INCLUDE_DIRS}) +target_link_libraries(psemek-journal PUBLIC psemek-util psemek-log SQLite::SQLite3) diff --git a/libs/journal/include/psemek/journal/event.hpp b/libs/journal/include/psemek/journal/event.hpp new file mode 100644 index 00000000..7a8a7272 --- /dev/null +++ b/libs/journal/include/psemek/journal/event.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +namespace psemek::journal +{ + + struct event_metadata + { + std::string source_file; + int source_line; + std::string name; + std::vector columns; + }; + + struct event_data + { + std::string time; + std::vector values; + }; + + struct event + { + event_metadata metadata; + event_data data; + }; + + std::string current_time(); + +} diff --git a/libs/journal/include/psemek/journal/journal.hpp b/libs/journal/include/psemek/journal/journal.hpp new file mode 100644 index 00000000..01c3496b --- /dev/null +++ b/libs/journal/include/psemek/journal/journal.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include + +namespace psemek::journal +{ + + struct journal + { + journal(std::filesystem::path const & path); + ~journal(); + + bool enabled() const; + void set_enabled(bool enabled); + + void log_event(event const & event); + + std::vector> select(std::string const & query); + + private: + psemek_declare_pimpl + }; + +} diff --git a/libs/journal/include/psemek/journal/log_event.hpp b/libs/journal/include/psemek/journal/log_event.hpp new file mode 100644 index 00000000..7804f5ee --- /dev/null +++ b/libs/journal/include/psemek/journal/log_event.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +#include + +#define psemek_journal_log_event_extract_key(key, value) key +#define psemek_journal_log_event_extract_value(key, value) std::format("\"{}\"", value) + +#define psemek_journal_log_event_single_attribute_key(r, data, elem) psemek_journal_log_event_extract_key elem, +#define psemek_journal_log_event_single_attribute_value(r, data, elem) psemek_journal_log_event_extract_value elem, + +#define psemek_journal_log_event(JOURNAL, NAME, ATTRIBUTES) \ + if ((JOURNAL).enabled()) \ + (JOURNAL).log_event({{ \ + .source_file = __FILE__, \ + .source_line = __LINE__, \ + .name = NAME, \ + .columns = { BOOST_PP_SEQ_FOR_EACH(psemek_journal_log_event_single_attribute_key, _, ATTRIBUTES) }, \ + }, { \ + .time = ::psemek::journal::current_time(), \ + .values = { BOOST_PP_SEQ_FOR_EACH(psemek_journal_log_event_single_attribute_value, _, ATTRIBUTES) }, \ + }}) diff --git a/libs/journal/source/event.cpp b/libs/journal/source/event.cpp new file mode 100644 index 00000000..4ebc68c6 --- /dev/null +++ b/libs/journal/source/event.cpp @@ -0,0 +1,15 @@ +#include + +#include +#include + +namespace psemek::journal +{ + + std::string current_time() + { + const auto now = std::chrono::high_resolution_clock::now(); + return std::format("{:%FT%TZ}", now); + } + +} diff --git a/libs/journal/source/journal.cpp b/libs/journal/source/journal.cpp new file mode 100644 index 00000000..7ced403f --- /dev/null +++ b/libs/journal/source/journal.cpp @@ -0,0 +1,181 @@ +#include +#include +#include +#include + +#include +#include + +#ifdef PSEMEK_USE_SQLITE + #include +#endif + +namespace psemek::journal +{ + +#ifdef PSEMEK_USE_SQLITE + + struct journal::impl + { + impl(std::filesystem::path const & path) + { + if (sqlite3_open(path.c_str(), &database) != SQLITE_OK) + { + auto error_message = std::format("Failed to open sqlite database at {}: {}", path.c_str(), sqlite3_errmsg(database)); + sqlite3_close(database); + throw util::exception(std::move(error_message)); + } + } + + ~impl() + { + sqlite3_close(database); + } + + void log_event(event const & event) + { + auto table_it = tables.find(event.metadata.name); + + if (table_it == tables.end()) + { + std::ostringstream command; + + command << "PRAGMA synchronous = OFF;"; + + command << "CREATE TABLE IF NOT EXISTS " << event.metadata.name << "(timestamp TEXT"; + + table_it = tables.insert({event.metadata.name, event.metadata}).first; + table_it->second.source_file = event.metadata.source_file; + table_it->second.source_line = event.metadata.source_line; + + for (auto const & attribute : event.metadata.columns) + command << ", " << attribute << " TEXT"; + + command << ");"; + + auto command_str = command.str(); + + char * error = nullptr; + if (sqlite3_exec(database, command_str.data(), nullptr, nullptr, &error) != SQLITE_OK) + { + auto error_message = std::string("Failed to create sqlite table: ") + error + std::string("\nSQL command: ") + command_str; + sqlite3_free(error); + throw util::exception(std::move(error_message)); + } + } + + { + std::ostringstream command; + + command << "PRAGMA synchronous = OFF; INSERT INTO " << event.metadata.name << "(timestamp"; + + for (auto const & attribute : event.metadata.columns) + command << ", " << attribute; + + command << ") VALUES (\"" << event.data.time << "\""; + + for (auto const & attribute : event.data.values) + command << ", " << attribute; + + command << ");"; + + auto command_str = command.str(); + + char * error = nullptr; + if (sqlite3_exec(database, command_str.data(), nullptr, nullptr, &error) != SQLITE_OK) + { + auto error_message = std::string("Failed to insert into sqlite table: ") + error + std::string("\nSQL command: ") + command_str; + sqlite3_free(error); + throw util::exception(std::move(error_message)); + } + } + } + + std::vector> select(std::string const & query) + { + std::vector> result; + + for (auto const & table : tables) + { + std::string table_query = "SELECT * FROM " + table.first + (query.empty() ? "" : " WHERE ") + query + ";"; + + struct context + { + std::vector> * result; + event_metadata const * metadata; + }; + + context ctx{&result, &table.second}; + + auto callback = [](void * pcontext, int columns, char ** values, char **) -> int + { + auto & ctx = *(context *)pcontext; + + auto & event = ctx.result->emplace_back(std::pair{ctx.metadata, event_data{}}); + + event.second.time = values[0]; + + for (int i = 1; i < columns; ++i) + event.second.values.push_back(values[i]); + + return 0; + }; + + char * error = nullptr; + if (sqlite3_exec(database, table_query.c_str(), callback, &ctx, &error) != SQLITE_OK) + sqlite3_free(error); + } + + std::sort(result.begin(), result.end(), [](auto const & e1, auto const & e2){ return e1.second.time < e2.second.time; }); + + return result; + } + + sqlite3 * database = nullptr; + bool enabled = true; + + util::hash_map tables; + }; + +#else + + struct journal::impl + { + impl(std::filesystem::path const &) + {} + + void log_event(event const &) + {} + + bool enabled = true; + }; + +#endif + + journal::journal(std::filesystem::path const & path) + : pimpl_(make_impl(path)) + {} + + journal::~journal() = default; + + bool journal::enabled() const + { + return impl().enabled; + } + + void journal::set_enabled(bool enabled) + { + impl().enabled = enabled; + } + + void journal::log_event(event const & event) + { + impl().log_event(event); + } + + std::vector> journal::select(std::string const & query) + { + return impl().select(query); + } + +}