// // Created by WolverinDEV on 26/06/2020. // #include "ActionLogger.h" #include "../client/ConnectedClient.h" #include #include using namespace ts::server; using namespace ts::server::log; Invoker log::kInvokerSystem{"System", "system", 0}; ConstLogGroupSettings log::kLogGroupSettingTrue{}; ConstLogGroupSettings log::kLogGroupSettingFalse{}; void AbstractActionLogger::do_log(ServerId sid, Action action, sql::command &command) const { if(!this->enabled_) return; command.executeLater().waitAndGetLater([sid, action](const sql::result& result) { if(!result) { logError(sid, "Failed to log action {}: {}", kActionName[(int) action], result.fmtStr()); } }, sql::result{}); } template <> void TypedActionLogger<>::compile_model(sql::model &result, const std::string &command, const ActionLogger *logger) { std::exchange(result, sql::model{logger->sql_manager(), command}); } template <> void TypedActionLogger<>::register_invoker(ActionLogger *logger, const Invoker &invoker) { logger->register_invoker(invoker); } template <> std::string TypedActionLogger<>::generate_insert(const std::string &table_name, const FixedHeaderKeys& header, DatabaseColumn *payload_columns, size_t payload_columns_length) { std::string result{"INSERT INTO `"}; result.reserve(256); result += table_name + "` ("; result += "`" + header.timestamp.name + "`,"; result += "`" + header.server_id.name + "`,"; result += "`" + header.invoker_id.name + "`,"; result += "`" + header.invoker_name.name + "`,"; result += "`" + header.action.name + "`"; for(size_t index{0}; index < payload_columns_length; index++) result += ",`" + payload_columns[index].name + "`"; result += ") VALUES (:h0, :h1, :h2, :h3, :h4"; for(size_t index{0}; index < payload_columns_length; index++) result += ", " + TypedActionLogger::value_binding_name(index); result += ");"; return result; } template <> sql::command TypedActionLogger<>::compile_query( ActionLogger *logger, uint64_t server_id, const std::chrono::system_clock::time_point &begin_timestamp, const std::chrono::system_clock::time_point &end_timestamp, size_t limit, const std::string& table_name, const FixedHeaderKeys& header, DatabaseColumn *payload_columns, size_t payload_columns_length) { std::string command{"SELECT "}; command.reserve(256); command += "`" + header.timestamp.name + "`,"; command += "`" + header.server_id.name + "`,"; command += "`" + header.invoker_id.name + "`,"; command += "`" + header.invoker_name.name + "`,"; command += "`" + header.action.name + "`"; for(size_t index{0}; index < payload_columns_length; index++) command += ",`" + payload_columns[index].name + "`"; command += " FROM `" + table_name + "` WHERE `" + header.server_id.name + "` = " + std::to_string(server_id); if(begin_timestamp.time_since_epoch().count() > 0) command += " AND `" + header.timestamp.name + "` <= " + std::to_string(std::chrono::ceil(begin_timestamp.time_since_epoch()).count()); if(begin_timestamp.time_since_epoch().count() > 0) command += " AND `" + header.timestamp.name + "` >= " + std::to_string(std::chrono::floor(end_timestamp.time_since_epoch()).count()); command += " ORDER BY `timestamp` DESC"; if(limit > 0) command += " LIMIT " + std::to_string(limit); command += ";"; debugMessage(0, "Log query: {}", command); return sql::command{logger->sql_manager(), command}; } template <> bool TypedActionLogger<>::create_table( std::string& error, ActionLogger *logger, const std::string& table_name, const FixedHeaderKeys& header, DatabaseColumn *payload_columns, size_t payload_columns_length) { std::string command{}; /* ATTENTION: If I implement MySQL add "CHARACTER SET=utf8" AT THE END! */ if(logger->sql_manager()->getType() != sql::TYPE_SQLITE) { error = "unsupported database type"; return false; } command.reserve(256); command += "CREATE TABLE `" + table_name + "` ("; command += "`id` INTEGER PRIMARY KEY NOT NULL,"; command += "`" + header.timestamp.name + "` " + header.timestamp.type + ","; command += "`" + header.server_id.name + "` " + header.server_id.type + ","; command += "`" + header.invoker_id.name + "` " + header.invoker_id.type + ","; command += "`" + header.invoker_name.name + "` " + header.invoker_name.type + ","; command += "`" + header.action.name + "` " + header.action.type; for(size_t index{0}; index < payload_columns_length; index++) command += ",`" + payload_columns[index].name + "` " + payload_columns[index].type; command += ");"; auto result = sql::command{logger->sql_manager(), command}.execute(); if(!result) { error = result.fmtStr(); return false; } return true; } template <> bool TypedActionLogger<>::parse_fixed_header(FixedHeaderValues &result, std::string *values, size_t values_length) { if(values_length< 5) return false; int64_t timestamp; try { timestamp = std::stoll(values[0]); result.server_id = std::stoull(values[1]); result.invoker.database_id = std::stoull(values[2]); } catch (std::exception&) { return false; } result.timestamp = std::chrono::system_clock::time_point{} + std::chrono::milliseconds{timestamp}; result.invoker.name = values[3]; result.action = Action::UNKNOWN; for(size_t index{0}; index < (size_t) Action::MAX; index++) { auto action = static_cast(index); if(kActionName[index] == values[4]) { result.action = action; } } return result.action != Action::UNKNOWN; } template <> void TypedActionLogger<>::bind_fixed_header(sql::command &command, const FixedHeaderValues &fhvalues) { command.value(":h0", std::chrono::floor(fhvalues.timestamp).time_since_epoch().count()); command.value(":h1", fhvalues.server_id); command.value(":h2", fhvalues.invoker.database_id); command.value(":h3", fhvalues.invoker.name); command.value(":h4", kActionName[(int) fhvalues.action]); } ActionLogger::ActionLogger() = default; ActionLogger::~ActionLogger() { this->finalize(); } #define CURRENT_VERSION 1 bool ActionLogger::initialize(std::string &error) { int db_version{0}; this->sql_handle = new sql::sqlite::SqliteManager{}; auto result = this->sql_handle->connect("sqlite://InstanceLogs.sqlite"); if(!result) { error = result.fmtStr(); goto error_exit; } { if(sql_handle->getType() == sql::TYPE_MYSQL) { sql::command(this->sql_handle, "SET NAMES utf8").execute(); //sql::command(this->manager, "DEFAULT CHARSET=utf8").execute(); } else if(sql_handle->getType() == sql::TYPE_SQLITE) { sql::command(this->sql_handle, "PRAGMA locking_mode = EXCLUSIVE;").execute(); sql::command(this->sql_handle, "PRAGMA synchronous = NORMAL;").execute(); sql::command(this->sql_handle, "PRAGMA journal_mode = WAL;").execute(); sql::command(this->sql_handle, "PRAGMA encoding = \"UTF-8\";").execute(); } } /* begin transaction, if available */ if(this->sql_handle->getType() == sql::TYPE_SQLITE) { result = sql::command(this->sql_handle, "BEGIN TRANSACTION;").execute(); if(!result) { error = "failed to begin transaction (" + result.fmtStr() + ")"; goto error_exit; } } { result = sql::command{this->sql_handle, "CREATE TABLE IF NOT EXISTS general(`key` VARCHAR(256), `value` TEXT);"}.execute(); if(!result) { error = "failed to create if not exists the general table: " + result.fmtStr(); goto error_exit; } sql::command{this->sql_handle, "SELECT `value` FROM `general` WHERE `key` = 'version';"}.query([&](int, std::string* values, std::string*) { db_version = std::stoi(values[0]); }); } switch (db_version) { case 0: /* initial setup */ case CURRENT_VERSION: /* current version */ break; default: error = CURRENT_VERSION < db_version ? "database version is newer than supported (supported: " + std::to_string(CURRENT_VERSION) + ", database: " + std::to_string(db_version) + ")" : "invalid database version (" + std::to_string(db_version) + ")"; } if(!this->server_logger.setup(db_version, error)) { error = "server logger: " + error; goto error_exit; } if(!this->server_edit_logger.setup(db_version, error)) { error = "server edit logger: " + error; goto error_exit; } if(!this->channel_logger.setup(db_version, error)) { error = "channel logger: " + error; goto error_exit; } if(!this->permission_logger.setup(db_version, error)) { error = "permission logger: " + error; goto error_exit; } if(!this->group_logger.setup(db_version, error)) { error = "group logger: " + error; goto error_exit; } if(!this->group_assignment_logger.setup(db_version, error)) { error = "group assignment logger: " + error; goto error_exit; } if(!this->client_channel_logger.setup(db_version, error)) { error = "client channel logger: " + error; goto error_exit; } if(!this->client_edit_logger.setup(db_version, error)) { error = "client edit logger: " + error; goto error_exit; } if(!this->file_logger.setup(db_version, error)) { error = "file logger: " + error; goto error_exit; } if(!this->custom_logger.setup(db_version, error)) { error = "custom logger: " + error; goto error_exit; } if(!this->query_logger.setup(db_version, error)) { error = "query logger: " + error; goto error_exit; } if(!this->query_authenticate_logger.setup(db_version, error)) { error = "query authenticate logger: " + error; goto error_exit; } /* update the version */ { std::string command{}; if(db_version == 0) command = "INSERT INTO `general` (`key`, `value`) VALUES ('version', :version);"; else command = "UPDATE `general` SET `value` = :version WHERE `key` = 'version';"; auto res = sql::command{this->sql_handle, command, variable{":version", CURRENT_VERSION}}.execute(); if(!res) { logCritical(LOG_INSTANCE, "Failed to increment action log database version!"); goto error_exit; } } if(this->sql_handle->getType() == sql::TYPE_SQLITE) { result = sql::command(this->sql_handle, "COMMIT;").execute(); if(!result) { error = "failed to commit changes"; return false; } } /* enable all loggers */ this->server_logger.set_enabled(true); this->server_edit_logger.set_enabled(true); this->channel_logger.set_enabled(true); this->permission_logger.set_enabled(true); this->group_logger.set_enabled(true); this->group_assignment_logger.set_enabled(true); this->client_channel_logger.set_enabled(true); this->client_edit_logger.set_enabled(true); this->file_logger.set_enabled(true); this->custom_logger.set_enabled(true); this->query_logger.set_enabled(true); this->query_authenticate_logger.set_enabled(true); return true; error_exit: if(this->sql_handle && this->sql_handle->connected()) { result = sql::command(this->sql_handle, "ROLLBACK;").execute(); if (!result) { logCritical(LOG_GENERAL, "Failed to rollback log database after transaction."); } else { debugMessage(LOG_GENERAL, "Rollbacked log database successfully."); } } this->finalize(); return false; } void ActionLogger::finalize() { if(!this->sql_handle) return; this->server_logger.finalize(); this->server_edit_logger.finalize(); this->channel_logger.finalize(); this->permission_logger.finalize(); this->group_logger.finalize(); this->group_assignment_logger.finalize(); this->client_channel_logger.finalize(); this->client_edit_logger.finalize(); this->custom_logger.finalize(); this->file_logger.finalize(); this->query_logger.finalize(); this->query_authenticate_logger.finalize(); this->sql_handle->pool->threads()->wait_for(std::chrono::seconds{1}); this->sql_handle->disconnect(); delete this->sql_handle; this->sql_handle = nullptr; } void ActionLogger::register_invoker(const Invoker &) {} std::vector ActionLogger::query( std::vector groups, uint64_t server, const std::chrono::system_clock::time_point &begin, const std::chrono::system_clock::time_point &end, size_t limit) { std::vector result{}; result.reserve(limit == 0 ? 1024 * 8 : limit * 2); std::vector> intresult{}; intresult.reserve(8); for(size_t index{0}; index < (size_t) LoggerGroup::MAX; index++) { auto group = static_cast(index); if(!groups.empty() && std::find(groups.begin(), groups.end(), group) == groups.end()) continue; intresult.clear(); switch (group) { case LoggerGroup::SERVER: std::exchange(intresult.emplace_back(), this->server_logger.query(server, begin, end, limit)); std::exchange(intresult.emplace_back(), this->server_edit_logger.query(server, begin, end, limit)); std::exchange(intresult.emplace_back(), this->group_logger.query(server, begin, end, limit)); std::exchange(intresult.emplace_back(), this->group_assignment_logger.query(server, begin, end, limit)); break; case LoggerGroup::CHANNEL: std::exchange(intresult.emplace_back(), this->channel_logger.query(server, begin, end, limit)); break; case LoggerGroup::CLIENT: std::exchange(intresult.emplace_back(), this->client_channel_logger.query(server, begin, end, limit)); std::exchange(intresult.emplace_back(), this->client_edit_logger.query(server, begin, end, limit)); break; case LoggerGroup::FILES: std::exchange(intresult.emplace_back(), this->file_logger.query(server, begin, end, limit)); break; case LoggerGroup::PERMISSION: std::exchange(intresult.emplace_back(), this->permission_logger.query(server, begin, end, limit)); break; case LoggerGroup::CUSTOM: std::exchange(intresult.emplace_back(), this->custom_logger.query(server, begin, end, limit)); break; case LoggerGroup::QUERY: std::exchange(intresult.emplace_back(), this->query_logger.query(server, begin, end, limit)); std::exchange(intresult.emplace_back(), this->query_authenticate_logger.query(server, begin, end, limit)); break; case LoggerGroup::MAX: default: assert(false); break; } for(auto& qresult : intresult) { if(qresult.empty()) continue; /* qresult is already sorted */ result.insert(result.begin(), qresult.begin(), qresult.begin() + std::min(qresult.size(), limit)); std::sort(result.begin(), result.end(), [](const LogEntryInfo& a, const LogEntryInfo& b){ if(a.timestamp == b.timestamp) return &a > &b; return a.timestamp > b.timestamp; }); if(limit > 0 && result.size() > limit) result.erase(result.begin() + limit, result.end()); } } return result; } void ActionLogger::toggle_logging_group(ServerId server_id, LoggerGroup group, bool flag) { switch (group) { case LoggerGroup::SERVER: this->log_group_server_.toggle_activated(server_id, flag); break; case LoggerGroup::PERMISSION: this->log_group_permissions_.toggle_activated(server_id, flag); break; case LoggerGroup::QUERY: this->log_group_query_.toggle_activated(server_id, flag); break; case LoggerGroup::FILES: this->log_group_file_transfer_.toggle_activated(server_id, flag); break; case LoggerGroup::CLIENT: this->log_group_client_.toggle_activated(server_id, flag); break; case LoggerGroup::CHANNEL: this->log_group_channel_.toggle_activated(server_id, flag); break; case LoggerGroup::CUSTOM: case LoggerGroup::MAX: default: break; } }