Teaspeak-Server/server/src/manager/ActionLogger.cpp
2020-09-15 01:18:35 +02:00

484 lines
17 KiB
C++

//
// Created by WolverinDEV on 26/06/2020.
//
#include "ActionLogger.h"
#include "../client/ConnectedClient.h"
#include <sql/sqlite/SqliteSQL.h>
#include <log/LogUtils.h>
using namespace ts::server;
using namespace ts::server::log;
Invoker log::kInvokerSystem{"System", "system", 0};
ConstLogGroupSettings<true> log::kLogGroupSettingTrue{};
ConstLogGroupSettings<false> 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<std::chrono::milliseconds>(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<std::chrono::milliseconds>(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<Action>(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<std::chrono::milliseconds>(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<LogEntryInfo> ActionLogger::query(
std::vector<LoggerGroup> 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<LogEntryInfo> result{};
result.reserve(limit == 0 ? 1024 * 8 : limit * 2);
std::vector<std::deque<LogEntryInfo>> intresult{};
intresult.reserve(8);
for(size_t index{0}; index < (size_t) LoggerGroup::MAX; index++) {
auto group = static_cast<LoggerGroup>(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;
}
}