From 894d792e2346c0b4b8aa0ca1cef6b32883876597 Mon Sep 17 00:00:00 2001 From: Dmitry K Date: Wed, 22 Jan 2025 13:55:36 +0000 Subject: [PATCH] Config option to limit the number of log files --- LogManager.cpp | 128 ++++++++++++++++++++++++++++++++++++++++++++++--- LogManager.h | 4 +- 2 files changed, 124 insertions(+), 8 deletions(-) diff --git a/LogManager.cpp b/LogManager.cpp index e854f298..220a0d9a 100644 --- a/LogManager.cpp +++ b/LogManager.cpp @@ -9,6 +9,7 @@ #include "LogManager.h" +#include #include #include #include @@ -18,6 +19,9 @@ const char* LogManager::log_codes[] = {"FATAL:", "ERROR:", "Warning:", "Info:", "Verbose:", "Debug:", "Trace:", "Dialog:"}; +const char* TimestampPattern = "%04d%02d%02d_%02d%02d%02d"; +const char* TimestampRegex = "[0-9]{8}_[0-9]{6}"; // relies on the structure of the template above + LogManager::LogManager() { base_clock = std::chrono::steady_clock::now(); @@ -56,21 +60,41 @@ unsigned int LogManager::getLoglevel() void LogManager::configure(json config, const filesystem::path& defaultDir) { - std::lock_guard grd(entry_mutex); + std::lock_guard grd(entry_mutex); /*-------------------------------------------------*\ | If the log is not open, create a new log file | \*-------------------------------------------------*/ if(!log_stream.is_open()) { + /*----------------------------------------------------*\ + | If a limit is declared in the config for the maximum | + | number of log files, respect the limit | + | Log rotation will remove the files matching the | + | current "logfile", starting with the oldest ones | + | (according to the timestamp in their filename) | + | i.e. with the lexicographically smallest filename | + | 0 or less equals no limit (default) | + \*----------------------------------------------------*/ + int loglimit = 0; + if(config.contains("file_count_limit") && config["file_count_limit"].is_number_integer()) + { + loglimit = config["file_count_limit"]; + } + if(config.contains("log_file")) { log_file_enabled = config["log_file"]; } + /*-----------------------------------------*\ + | Default template for the logfile name | + | The # symbol is replaced with a timestamp | + \*-----------------------------------------*/ + std::string logtempl = "OpenRGB_#.log"; + if(log_file_enabled) { - std::string logname = "OpenRGB_#.log"; /*-------------------------------------------------*\ | If the logfile is defined in the configuration, | @@ -84,11 +108,10 @@ void LogManager::configure(json config, const filesystem::path& defaultDir) std::string tmpname = config["logfile"]; if(!tmpname.empty()) { - logname = tmpname; + logtempl = tmpname; } } } - /*-------------------------------------------------*\ | If the # symbol is found in the log file name, | | replace it with a timestamp | @@ -96,8 +119,9 @@ void LogManager::configure(json config, const filesystem::path& defaultDir) time_t t = time(0); struct tm* tmp = localtime(&t); char time_string[64]; - snprintf(time_string, 64, "%04d%02d%02d_%02d%02d%02d", 1900 + tmp->tm_year, tmp->tm_mon + 1, tmp->tm_mday, tmp->tm_hour, tmp->tm_min, tmp->tm_sec); + snprintf(time_string, 64, TimestampPattern, 1900 + tmp->tm_year, tmp->tm_mon + 1, tmp->tm_mday, tmp->tm_hour, tmp->tm_min, tmp->tm_sec); + std::string logname = logtempl; size_t oct = logname.find("#"); if(oct != logname.npos) { @@ -114,6 +138,12 @@ void LogManager::configure(json config, const filesystem::path& defaultDir) } filesystem::create_directories(p.parent_path()); + /*----------------------------------------------*\ + | "Log rotation": remove old log files exceeding | + | the current configured limit | + \*----------------------------------------------*/ + rotate_logs(p.parent_path(), filesystem::u8path(logtempl).filename(), loglimit); + /*-------------------------------------------------*\ | Open the logfile | \*-------------------------------------------------*/ @@ -200,7 +230,7 @@ void LogManager::_flush() void LogManager::flush() { - std::lock_guard grd(entry_mutex); + std::lock_guard grd(entry_mutex); _flush(); } @@ -298,7 +328,7 @@ void LogManager::append(const char* filename, int line, unsigned int level, cons va_list va; va_start(va, fmt); - std::lock_guard grd(entry_mutex); + std::lock_guard grd(entry_mutex); _append(filename, line, level, fmt, va); va_end(va); @@ -367,3 +397,87 @@ void LogManager::UnregisterDialogShowCallback(LogDialogShowCallback callback, vo } } } + +void LogManager::rotate_logs(const filesystem::path& folder, const filesystem::path& templ, int max_count) +{ + if(max_count < 1) + { + return; + } + + std::string templ2 = templ.filename().generic_u8string(); + + // Process the templ2 into a usable regex + // The # symbol is replaced with a timestamp regex + // Any regex-unfriendly symbols are escaped with a backslash + std::string regex_templ = "^"; + for(size_t i = 0; i < templ2.size(); ++i) + { + switch(templ2[i]) + { + // Symbols that have special meanings in regex'es need backslash escaping + case '.': + case '^': + case '$': + case '(': + case ')': + case '{': + case '}': + case '+': + case '[': + case ']': + case '*': + case '-': + case '\\': // Should have been filtered out by the filesystem processing, but... who knows + regex_templ.push_back('\\'); + regex_templ.push_back(templ2[i]); + break; + + // The # symbol is reserved for the timestamp and thus is replaced with the timestamp regex template + case '#': + regex_templ.append(TimestampRegex); + break; + + default: + regex_templ.push_back(templ2[i]); + break; + } + } + regex_templ.push_back('$'); + + std::regex r(regex_templ); + + std::vector valid_paths; + std::filesystem::directory_iterator it(folder); + for(; it != filesystem::end(it); ++it) + { + if(it->is_regular_file()) + { + std::string fname = it->path().filename().u8string(); + if(std::regex_match(fname, r)) + { + valid_paths.push_back(it->path()); + } + } + } + std::sort(valid_paths.begin(), valid_paths.end()); + + size_t remove_count = valid_paths.size() - max_count + 1; // NOTE: the "1" extra file to remove creates space for the one we're about to create + if(remove_count > valid_paths.size()) // for max_count <= 0 and to prevent any possible errors in the above logic + { + remove_count = valid_paths.size(); + } + + for(size_t i = 0; i < remove_count; ++i) + { + std::error_code ec; // Uses error code to force the `remove` call to be `noexcept` + if(filesystem::remove(valid_paths[i], ec)) + { + LOG_VERBOSE("[LogManager] Removed log file [%s] during rotation", valid_paths[i].u8string().c_str()); + } + else + { + LOG_WARNING("[LogManager] Failed to remove log file [%s] during rotation: %s", valid_paths[i].u8string().c_str(), ec.message().c_str()); + } + } +} diff --git a/LogManager.h b/LogManager.h index 55e656b8..95ffe9d5 100644 --- a/LogManager.h +++ b/LogManager.h @@ -60,7 +60,7 @@ private: LogManager(const LogManager&) = delete; LogManager(LogManager&&) = delete; ~LogManager(); - std::mutex entry_mutex; + std::recursive_mutex entry_mutex; std::mutex section_mutex; std::ofstream log_stream; @@ -91,6 +91,8 @@ private: // A non-guarded flush() void _flush(); + void rotate_logs(const filesystem::path& folder, const filesystem::path& templ, int max_count); + public: static LogManager* get(); void configure(json config, const filesystem::path & defaultDir);