diff --git a/dev/error_code.h b/dev/error_code.h index 58568621..29e5b08a 100644 --- a/dev/error_code.h +++ b/dev/error_code.h @@ -39,6 +39,7 @@ namespace sqlite_orm { index_is_out_of_bounds, value_is_null, no_tables_specified, + failure_to_init_logfile, }; } diff --git a/dev/sql_auditing.h b/dev/sql_auditing.h new file mode 100644 index 00000000..32f8cbe2 --- /dev/null +++ b/dev/sql_auditing.h @@ -0,0 +1,88 @@ +#pragma once +#include +#include "error_code.h" +#include +#include // For std::localtime and std::tm +#include // For std::put_time +#include + +#include "sql_auditing.h" + +enum class auditing_behavior : signed char { OFF = 0, ON = 1 }; + +class sql_auditor_settings; + +class sql_auditor { + + std::ofstream log_file; + friend class sql_auditor_settings; + sql_auditor(); + void open(); + inline static sql_auditor& auditor() { + static sql_auditor auditor{}; + return auditor; + } + + public: + static void log(const std::string& message); +}; + +class sql_auditor_settings { + std::string destination_file = "sql_auditor.txt"; + friend class sql_auditor; + sql_auditor_settings() {} + inline static sql_auditor_settings& settings() { + static sql_auditor_settings sql_settings; + return sql_settings; + } + auditing_behavior behavior = auditing_behavior::ON; + std::string format_str = "%Y-%m-%d %H:%M:%S"; + + public: + static void set_destination_file(const std::string& filename); + static void set_behavior(auditing_behavior behavior) { + settings().behavior = behavior; + } + static void set_format(const std::string& format_str) { + settings().format_str = format_str; + } +}; + +inline sql_auditor::sql_auditor() { + open(); +} + +inline void sql_auditor_settings::set_destination_file(const std::string& filename) { + settings().destination_file = filename; + sql_auditor::auditor().open(); +} + +inline void sql_auditor::open() { + using namespace std; + if(!log_file.is_open()) { + log_file.open(sql_auditor_settings::settings().destination_file, ios::trunc | ios::out); + if(!log_file.good()) { + throw std::system_error{sqlite_orm::orm_error_code::failure_to_init_logfile}; + } + } +} + +inline void sql_auditor::log(const std::string& message) { + // guard and exit if off + if(sql_auditor_settings::settings().behavior == auditing_behavior::OFF) + return; + + // would use format if C++ 20 + auto now = std::chrono::system_clock::now(); + + std::time_t now_time = std::chrono::system_clock::to_time_t(now); + + // Convert to local time (std::tm structure) + // WARNING: localtime is not thread safe! + std::tm local_time = *std::localtime(&now_time); + + // Print the local time in a human-readable format + auditor().log_file << "@: " << std::put_time(&local_time, sql_auditor_settings::settings().format_str.c_str()) + << " = "; + auditor().log_file << message << std::endl; +} diff --git a/dev/statement_serializer.h b/dev/statement_serializer.h index c616d906..47ab3693 100644 --- a/dev/statement_serializer.h +++ b/dev/statement_serializer.h @@ -47,6 +47,7 @@ #include "table_type_of.h" #include "util.h" #include "error_code.h" +#include "sql_auditing.h" #include "schema/triggers.h" #include "schema/column.h" #include "schema/index.h" diff --git a/dev/util.h b/dev/util.h index f162eeda..7886e317 100644 --- a/dev/util.h +++ b/dev/util.h @@ -5,6 +5,7 @@ #include // std::move #include "error_code.h" +#include "sql_auditing.h" namespace sqlite_orm { @@ -61,6 +62,7 @@ namespace sqlite_orm { if(sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { throw_translated_sqlite_error(db); } + sql_auditor::log(query); return stmt; } @@ -69,6 +71,7 @@ namespace sqlite_orm { if(rc != SQLITE_OK) { throw_translated_sqlite_error(db); } + sql_auditor::log(query); } inline void perform_exec(sqlite3* db, @@ -79,6 +82,7 @@ namespace sqlite_orm { if(rc != SQLITE_OK) { throw_translated_sqlite_error(db); } + sql_auditor::log(query); } inline void perform_exec(sqlite3* db, diff --git a/include/sqlite_orm/sqlite_orm.h b/include/sqlite_orm/sqlite_orm.h index e7709575..1e3615c7 100644 --- a/include/sqlite_orm/sqlite_orm.h +++ b/include/sqlite_orm/sqlite_orm.h @@ -2863,6 +2863,7 @@ namespace sqlite_orm { index_is_out_of_bounds, value_is_null, no_tables_specified, + failure_to_init_logfile, }; } @@ -13447,6 +13448,97 @@ namespace sqlite_orm { // #include "error_code.h" +// #include "sql_auditing.h" + +#include +// #include "error_code.h" + +#include +#include // For std::localtime and std::tm +#include // For std::put_time +#include + +// #include "sql_auditing.h" + +enum class auditing_behavior : signed char { OFF = 0, ON = 1 }; + +class sql_auditor_settings; + +class sql_auditor { + + std::ofstream log_file; + friend class sql_auditor_settings; + sql_auditor(); + void open(); + inline static sql_auditor& auditor() { + static sql_auditor auditor{}; + return auditor; + } + + public: + static void log(const std::string& message); +}; + +class sql_auditor_settings { + std::string destination_file = "sql_auditor.txt"; + friend class sql_auditor; + sql_auditor_settings() {} + inline static sql_auditor_settings& settings() { + static sql_auditor_settings sql_settings; + return sql_settings; + } + auditing_behavior behavior = auditing_behavior::ON; + std::string format_str = "%Y-%m-%d %H:%M:%S"; + + public: + static void set_destination_file(const std::string& filename); + static void set_behavior(auditing_behavior behavior) { + settings().behavior = behavior; + } + static void set_format(const std::string& format_str) { + settings().format_str = format_str; + } +}; + +inline sql_auditor::sql_auditor() { + open(); +} + +inline void sql_auditor_settings::set_destination_file(const std::string& filename) { + settings().destination_file = filename; + sql_auditor::auditor().open(); +} + +inline void sql_auditor::open() { + using namespace std; + if(!log_file.is_open()) { + log_file.open(sql_auditor_settings::settings().destination_file, ios::trunc | ios::out); + if(!log_file.good()) { + throw std::system_error{sqlite_orm::orm_error_code::failure_to_init_logfile}; + } + } +} + +inline void sql_auditor::log(const std::string& message) { + // guard and exit if off + if(sql_auditor_settings::settings().behavior == auditing_behavior::OFF) + return; + + // would use format if C++ 20 + auto now = std::chrono::system_clock::now(); + + std::time_t now_time = std::chrono::system_clock::to_time_t(now); + + // Convert to local time (std::tm structure) + // WARNING: localtime is not thread safe! + std::tm local_time = *std::localtime(&now_time); + + // Print the local time in a human-readable format + auditor().log_file << "@: " << std::put_time(&local_time, sql_auditor_settings::settings().format_str.c_str()) + << " = "; + auditor().log_file << message << std::endl; +} + namespace sqlite_orm { /** @@ -13502,6 +13594,7 @@ namespace sqlite_orm { if(sqlite3_prepare_v2(db, query.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { throw_translated_sqlite_error(db); } + sql_auditor::log(query); return stmt; } @@ -13510,6 +13603,7 @@ namespace sqlite_orm { if(rc != SQLITE_OK) { throw_translated_sqlite_error(db); } + sql_auditor::log(query); } inline void perform_exec(sqlite3* db, @@ -13520,6 +13614,7 @@ namespace sqlite_orm { if(rc != SQLITE_OK) { throw_translated_sqlite_error(db); } + sql_auditor::log(query); } inline void perform_exec(sqlite3* db, @@ -19119,6 +19214,8 @@ namespace sqlite_orm { // #include "error_code.h" +// #include "sql_auditing.h" + // #include "schema/triggers.h" #include diff --git a/tests/prepared_statement_tests/select.cpp b/tests/prepared_statement_tests/select.cpp index 135956f5..9524298b 100644 --- a/tests/prepared_statement_tests/select.cpp +++ b/tests/prepared_statement_tests/select.cpp @@ -14,6 +14,13 @@ TEST_CASE("Prepared select") { auto filename = "prepared.sqlite"; remove(filename); + +#define JD_AUDITING_SETTINGS +#ifdef JD_AUDITING_SETTINGS + sql_auditor_settings::set_destination_file("log.txt"); + sql_auditor_settings::set_behavior(auditing_behavior::ON); + sql_auditor_settings::set_format("%H:%M:%S"); +#endif auto storage = make_storage(filename, make_index("user_id_index", &User::id), make_table("users", diff --git a/tests/statement_serializer_tests/statements/insert_replace.cpp b/tests/statement_serializer_tests/statements/insert_replace.cpp index b3882fe2..1a470b2e 100644 --- a/tests/statement_serializer_tests/statements/insert_replace.cpp +++ b/tests/statement_serializer_tests/statements/insert_replace.cpp @@ -80,7 +80,7 @@ TEST_CASE("statement_serializer insert/replace") { expected = R"(REPLACE INTO "users" SELECT "users_backup"."id", "users_backup"."name" FROM "users_backup")"; } -#if(SQLITE_VERSION_NUMBER >= 3008003) && defined(SQLITE_ORM_WITH_CTE) +#if (SQLITE_VERSION_NUMBER >= 3008003) && defined(SQLITE_ORM_WITH_CTE) #ifdef SQLITE_ORM_WITH_CPP20_ALIASES SECTION("With clause") { constexpr orm_cte_moniker auto data = "data"_cte; @@ -374,7 +374,7 @@ TEST_CASE("statement_serializer insert/replace") { } } SECTION("With clause") { -#if(SQLITE_VERSION_NUMBER >= 3008003) && defined(SQLITE_ORM_WITH_CTE) +#if (SQLITE_VERSION_NUMBER >= 3008003) && defined(SQLITE_ORM_WITH_CTE) #ifdef SQLITE_ORM_WITH_CPP20_ALIASES constexpr orm_cte_moniker auto data = "data"_cte; constexpr auto cteExpression = cte().as(select(asterisk())); diff --git a/tests/statement_serializer_tests/statements/remove_all.cpp b/tests/statement_serializer_tests/statements/remove_all.cpp index ba30b9c0..44c62a9d 100644 --- a/tests/statement_serializer_tests/statements/remove_all.cpp +++ b/tests/statement_serializer_tests/statements/remove_all.cpp @@ -33,7 +33,7 @@ TEST_CASE("statement_serializer remove_all") { value = serialize(expression, context); expected = R"(DELETE FROM "users" WHERE ("id" = 1) LIMIT 1)"; } -#if(SQLITE_VERSION_NUMBER >= 3008003) && defined(SQLITE_ORM_WITH_CTE) +#if (SQLITE_VERSION_NUMBER >= 3008003) && defined(SQLITE_ORM_WITH_CTE) #ifdef SQLITE_ORM_WITH_CPP20_ALIASES SECTION("With clause") { constexpr orm_cte_moniker auto data = "data"_cte; diff --git a/tests/statement_serializer_tests/statements/update_all.cpp b/tests/statement_serializer_tests/statements/update_all.cpp index 006255c8..160e1c94 100644 --- a/tests/statement_serializer_tests/statements/update_all.cpp +++ b/tests/statement_serializer_tests/statements/update_all.cpp @@ -60,7 +60,7 @@ TEST_CASE("statement_serializer update_all") { expected = R"(UPDATE "contacts" SET "phone" = (SELECT "customers"."Phone" FROM "customers" WHERE ("customers"."CustomerId" = 1)) WHERE ("contact_id" = 1))"; } -#if(SQLITE_VERSION_NUMBER >= 3008003) && defined(SQLITE_ORM_WITH_CTE) +#if (SQLITE_VERSION_NUMBER >= 3008003) && defined(SQLITE_ORM_WITH_CTE) #ifdef SQLITE_ORM_WITH_CPP20_ALIASES SECTION("With clause") { constexpr orm_cte_moniker auto data = "data"_cte;