Add sqlite-based event journaling library
This commit is contained in:
parent
8099e928dc
commit
c8a58d1de4
7 changed files with 294 additions and 0 deletions
|
|
@ -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)
|
||||
|
|
|
|||
10
libs/journal/CMakeLists.txt
Normal file
10
libs/journal/CMakeLists.txt
Normal file
|
|
@ -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)
|
||||
31
libs/journal/include/psemek/journal/event.hpp
Normal file
31
libs/journal/include/psemek/journal/event.hpp
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace psemek::journal
|
||||
{
|
||||
|
||||
struct event_metadata
|
||||
{
|
||||
std::string source_file;
|
||||
int source_line;
|
||||
std::string name;
|
||||
std::vector<std::string> columns;
|
||||
};
|
||||
|
||||
struct event_data
|
||||
{
|
||||
std::string time;
|
||||
std::vector<std::string> values;
|
||||
};
|
||||
|
||||
struct event
|
||||
{
|
||||
event_metadata metadata;
|
||||
event_data data;
|
||||
};
|
||||
|
||||
std::string current_time();
|
||||
|
||||
}
|
||||
27
libs/journal/include/psemek/journal/journal.hpp
Normal file
27
libs/journal/include/psemek/journal/journal.hpp
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/journal/event.hpp>
|
||||
#include <psemek/util/pimpl.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
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<std::pair<event_metadata const *, event_data>> select(std::string const & query);
|
||||
|
||||
private:
|
||||
psemek_declare_pimpl
|
||||
};
|
||||
|
||||
}
|
||||
25
libs/journal/include/psemek/journal/log_event.hpp
Normal file
25
libs/journal/include/psemek/journal/log_event.hpp
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/journal/journal.hpp>
|
||||
|
||||
#include <boost/preprocessor/seq/for_each.hpp>
|
||||
|
||||
#include <format>
|
||||
|
||||
#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) }, \
|
||||
}})
|
||||
15
libs/journal/source/event.cpp
Normal file
15
libs/journal/source/event.cpp
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#include <psemek/journal/event.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <format>
|
||||
|
||||
namespace psemek::journal
|
||||
{
|
||||
|
||||
std::string current_time()
|
||||
{
|
||||
const auto now = std::chrono::high_resolution_clock::now();
|
||||
return std::format("{:%FT%TZ}", now);
|
||||
}
|
||||
|
||||
}
|
||||
181
libs/journal/source/journal.cpp
Normal file
181
libs/journal/source/journal.cpp
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
#include <psemek/journal/journal.hpp>
|
||||
#include <psemek/log/log.hpp>
|
||||
#include <psemek/util/exception.hpp>
|
||||
#include <psemek/util/hash_table.hpp>
|
||||
|
||||
#include <format>
|
||||
#include <sstream>
|
||||
|
||||
#ifdef PSEMEK_USE_SQLITE
|
||||
#include <sqlite3.h>
|
||||
#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<std::pair<event_metadata const *, event_data>> select(std::string const & query)
|
||||
{
|
||||
std::vector<std::pair<event_metadata const *, event_data>> result;
|
||||
|
||||
for (auto const & table : tables)
|
||||
{
|
||||
std::string table_query = "SELECT * FROM " + table.first + (query.empty() ? "" : " WHERE ") + query + ";";
|
||||
|
||||
struct context
|
||||
{
|
||||
std::vector<std::pair<event_metadata const *, event_data>> * 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<std::string, event_metadata> 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<std::pair<event_metadata const *, event_data>> journal::select(std::string const & query)
|
||||
{
|
||||
return impl().select(query);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue