From 35c6180f1842c10bf67f85bbee8153f007ec939d Mon Sep 17 00:00:00 2001 From: Aster Seker Date: Sun, 14 Sep 2025 05:57:57 +0300 Subject: [PATCH] feat(filelogger): add size-based rotation --- include/logit_cpp/logit/LogMacros.hpp | 40 +++++ include/logit_cpp/logit/config.hpp | 13 ++ .../logit_cpp/logit/loggers/FileLogger.hpp | 154 ++++++++++++++++-- tests/file_logger_rotation_test.cpp | 18 ++ tests/file_logger_test.cpp | 8 +- 5 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 tests/file_logger_rotation_test.cpp diff --git a/include/logit_cpp/logit/LogMacros.hpp b/include/logit_cpp/logit/LogMacros.hpp index daffaca..e932e0a 100644 --- a/include/logit_cpp/logit/LogMacros.hpp +++ b/include/logit_cpp/logit/LogMacros.hpp @@ -922,6 +922,25 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT std::make_unique(LOGIT_FILE_LOGGER_PATTERN), \ true) +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION(dir, async, days, pattern, max_bytes, max_files) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(dir, async, days, max_bytes, max_files), \ + std::make_unique(pattern)) + +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_SINGLE_MODE(dir, async, days, pattern, max_bytes, max_files) \ + logit::Logger::get_instance().add_logger( \ + std::make_unique(dir, async, days, max_bytes, max_files), \ + std::make_unique(pattern), \ + true) + +#define LOGIT_ADD_FILE_LOGGER_DEFAULT_WITH_ROTATION() \ + logit::Logger::get_instance().add_logger( \ + std::make_unique( \ + LOGIT_FILE_LOGGER_PATH, true, LOGIT_FILE_LOGGER_AUTO_DELETE_DAYS, \ + LOGIT_FILE_LOGGER_MAX_FILE_SIZE_BYTES, LOGIT_FILE_LOGGER_MAX_ROTATED_FILES, \ + LOGIT_FILE_LOGGER_COMPRESS_ROTATED, LOGIT_FILE_LOGGER_COMPRESS_CMD), \ + std::make_unique(LOGIT_FILE_LOGGER_PATTERN)) + /// \brief Macro for adding a unique file logger with custom parameters. /// \param directory The directory where log files will be stored. /// \param async Boolean indicating whether logging should be asynchronous (true) or synchronous (false). @@ -1093,6 +1112,27 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT new logit::SimpleLogFormatter(LOGIT_FILE_LOGGER_PATTERN)), \ true) +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION(dir, async, days, pattern, max_bytes, max_files) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::FileLogger( \ + dir, async, days, max_bytes, max_files)), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern))) + +#define LOGIT_ADD_FILE_LOGGER_WITH_ROTATION_SINGLE_MODE(dir, async, days, pattern, max_bytes, max_files) \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::FileLogger( \ + dir, async, days, max_bytes, max_files)), \ + std::unique_ptr(new logit::SimpleLogFormatter(pattern)), \ + true) + +#define LOGIT_ADD_FILE_LOGGER_DEFAULT_WITH_ROTATION() \ + logit::Logger::get_instance().add_logger( \ + std::unique_ptr(new logit::FileLogger( \ + LOGIT_FILE_LOGGER_PATH, true, LOGIT_FILE_LOGGER_AUTO_DELETE_DAYS, \ + LOGIT_FILE_LOGGER_MAX_FILE_SIZE_BYTES, LOGIT_FILE_LOGGER_MAX_ROTATED_FILES, \ + LOGIT_FILE_LOGGER_COMPRESS_ROTATED, LOGIT_FILE_LOGGER_COMPRESS_CMD)), \ + std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_FILE_LOGGER_PATTERN))) + /// \brief Macro for adding a unique file logger with custom parameters. /// \param directory The directory where log files will be stored. /// \param async Boolean indicating whether logging should be asynchronous (true) or synchronous (false). diff --git a/include/logit_cpp/logit/config.hpp b/include/logit_cpp/logit/config.hpp index 8dd9991..921d230 100644 --- a/include/logit_cpp/logit/config.hpp +++ b/include/logit_cpp/logit/config.hpp @@ -111,6 +111,19 @@ #define LOGIT_FILE_LOGGER_PATTERN "[%Y-%m-%d %H:%M:%S.%e] [%-5l] [%60!@] [thread:%t] %SC%v" #endif +#ifndef LOGIT_FILE_LOGGER_MAX_FILE_SIZE_BYTES + #define LOGIT_FILE_LOGGER_MAX_FILE_SIZE_BYTES 0 +#endif +#ifndef LOGIT_FILE_LOGGER_MAX_ROTATED_FILES + #define LOGIT_FILE_LOGGER_MAX_ROTATED_FILES 0 +#endif +#ifndef LOGIT_FILE_LOGGER_COMPRESS_ROTATED + #define LOGIT_FILE_LOGGER_COMPRESS_ROTATED 0 +#endif +#ifndef LOGIT_FILE_LOGGER_COMPRESS_CMD + #define LOGIT_FILE_LOGGER_COMPRESS_CMD "" +#endif + /// \brief Defines the default log pattern for unique file-based loggers. /// If `LOGIT_UNIQUE_FILE_LOGGER_PATTERN` is not defined, it defaults to "%v". #ifndef LOGIT_UNIQUE_FILE_LOGGER_PATTERN diff --git a/include/logit_cpp/logit/loggers/FileLogger.hpp b/include/logit_cpp/logit/loggers/FileLogger.hpp index 64af701..ae338a4 100644 --- a/include/logit_cpp/logit/loggers/FileLogger.hpp +++ b/include/logit_cpp/logit/loggers/FileLogger.hpp @@ -13,6 +13,11 @@ #include #include #include +#include +#include +#include +#include +#include #include namespace logit { @@ -22,14 +27,20 @@ namespace logit { class FileLogger : public ILogger { public: struct Config { - std::string directory = "logs"; - bool async = false; + std::string directory = "logs"; + bool async = false; int auto_delete_days = 30; + uint64_t max_file_size_bytes = 0; + uint32_t max_rotated_files = 0; + bool compress_rotated = false; + std::string compress_cmd; }; FileLogger() { warn(); } FileLogger(const Config&) { warn(); } - FileLogger(const std::string&, const bool& = true, const int& = 30) { warn(); } + FileLogger(const std::string&, const bool& = true, const int& = 30, + const uint64_t& = 0, const uint32_t& = 0, + const bool& = false, std::string = {}) { warn(); } void log(const LogRecord&, const std::string&) override { warn(); } std::string get_string_param(const LoggerParam&) const override { return {}; } @@ -63,9 +74,13 @@ namespace logit { /// \struct Config /// \brief Configuration for the file logger. struct Config { - std::string directory = "logs"; ///< Directory where log files are stored. - bool async = true; ///< Flag indicating whether logging should be asynchronous. - int auto_delete_days = 30; ///< Number of days after which old log files are deleted. + std::string directory = "logs"; ///< Directory where log files are stored. + bool async = true; ///< Flag indicating whether logging should be asynchronous. + int auto_delete_days = 30; ///< Number of days after which old log files are deleted. + uint64_t max_file_size_bytes = 0; ///< Max size for log file before rotation (0 = off). + uint32_t max_rotated_files = 0; ///< Number of rotated files to keep (0 = unlimited). + bool compress_rotated = false; ///< Whether to compress rotated files. + std::string compress_cmd; ///< External command used for compression. }; /// \brief Default constructor that uses default configuration. @@ -93,6 +108,25 @@ namespace logit { start_logging(); } + /// \brief Constructor with directory, size-based rotation and additional options. + FileLogger( + const std::string& directory, + const bool& async, + const int& auto_delete_days, + uint64_t max_file_size_bytes, + uint32_t max_rotated_files, + bool compress_rotated = false, + std::string compress_cmd = {}) { + m_config.directory = directory; + m_config.async = async; + m_config.auto_delete_days = auto_delete_days; + m_config.max_file_size_bytes = max_file_size_bytes; + m_config.max_rotated_files = max_rotated_files; + m_config.compress_rotated = compress_rotated; + m_config.compress_cmd = std::move(compress_cmd); + start_logging(); + } + /// \brief Destructor to stop logging and close file. virtual ~FileLogger() { stop_logging(); @@ -192,6 +226,7 @@ namespace logit { std::string m_file_path; ///< Path of the currently open log file. std::string m_file_name; ///< Name of the currently open log file. int64_t m_current_date_ts = 0; ///< Timestamp of the current log file's date. + uint64_t m_current_file_size = 0; ///< Current size of the log file. std::atomic m_last_log_ts = ATOMIC_VAR_INIT(0); ///< Timestamp of the last log. std::atomic m_log_level = ATOMIC_VAR_INIT(static_cast(LogLevel::LOG_LVL_TRACE)); @@ -254,6 +289,8 @@ namespace logit { if (!m_file.is_open()) { throw std::runtime_error("Failed to open log file: " + m_file_path); } + m_file.seekp(0, std::ios::end); + m_current_file_size = static_cast(m_file.tellp()); } /// \brief Creates a file path for the log file based on the date timestamp. @@ -272,12 +309,111 @@ namespace logit { if (message_date_ts != m_current_date_ts) { open_log_file(message_date_ts); } + if (m_config.max_file_size_bytes > 0) { + const uint64_t add = static_cast(message.size() + 1); + if (m_current_file_size + add > m_config.max_file_size_bytes) { + rotate_current_file(); + } + } if (m_file.is_open()) { m_file << message << std::endl; + m_current_file_size += static_cast(message.size() + 1); } remove_old_logs(); } + void rotate_current_file() { + if (m_file.is_open()) m_file.close(); + + const std::string base = time_shield::to_iso8601_date(m_current_date_ts); + const std::string dir = get_directory_path(); + const std::string cur = dir + "/" + base + ".log"; + std::string rotated; + uint32_t idx = 1; +# if __cplusplus >= 201703L + for (;; ++idx) { + rotated = dir + "/" + base + "." + std::to_string(idx) + ".log"; + if (!fs::exists(rotated)) break; + } +# else + auto file_exists = [](const std::string& path) { +# if defined(_WIN32) + std::ifstream f(utf8_to_ansi(path).c_str()); +# else + std::ifstream f(path.c_str()); +# endif + return f.good(); + }; + for (;; ++idx) { + rotated = dir + "/" + base + "." + std::to_string(idx) + ".log"; + if (!file_exists(rotated)) break; + } +# endif +# if defined(_WIN32) + std::rename(utf8_to_ansi(cur).c_str(), utf8_to_ansi(rotated).c_str()); +# else + std::rename(cur.c_str(), rotated.c_str()); +# endif + + if (m_config.compress_rotated && !m_config.compress_cmd.empty()) { + std::string cmd = m_config.compress_cmd; + size_t pos = cmd.find("{file}"); + if (pos != std::string::npos) cmd.replace(pos, 6, "\"" + rotated + "\""); + std::system(cmd.c_str()); + } + + if (m_config.max_rotated_files > 0) { + enforce_rotation_retention(base, m_config.max_rotated_files, dir); + } + + open_log_file(m_current_date_ts); + m_current_file_size = 0; + } + + void enforce_rotation_retention(const std::string& base, uint32_t max_files, const std::string& dir) { +# if __cplusplus >= 201703L + std::vector> files; + std::regex pattern(base + R"(\.(\d+)\.log(\..*)?)"); + for (const auto& entry : fs::directory_iterator(dir)) { + if (!fs::is_regular_file(entry.status())) continue; + std::smatch m; + std::string name = entry.path().filename().string(); + if (std::regex_match(name, m, pattern)) { + uint32_t idx = static_cast(std::stoul(m[1].str())); + files.emplace_back(idx, entry.path()); + } + } + if (files.size() <= max_files) return; + std::sort(files.begin(), files.end(), [](const std::pair& a, const std::pair& b) { return a.first < b.first; }); + size_t to_remove = files.size() - max_files; + for (size_t i = 0; i < to_remove; ++i) { + fs::remove(files[i].second); + } +# else + std::vector> files; + std::regex pattern(base + R"(\.(\d+)\.log(\..*)?)"); + std::vector file_list = get_list_files(dir); + for (const auto& path : file_list) { + std::string name = path.substr(path.find_last_of("/\\") + 1); + std::smatch m; + if (std::regex_match(name, m, pattern)) { + uint32_t idx = static_cast(std::stoul(m[1].str())); + files.emplace_back(idx, path); + } + } + if (files.size() <= max_files) return; + std::sort(files.begin(), files.end(), [](const std::pair& a, const std::pair& b) { return a.first < b.first; }); + size_t to_remove = files.size() - max_files; + for (size_t i = 0; i < to_remove; ++i) { +# if defined(_WIN32) + remove(utf8_to_ansi(files[i].second).c_str()); +# else + remove(files[i].second.c_str()); +# endif + } +# endif + } + /// \brief Removes old log files based on the auto-delete days configuration. void remove_old_logs() { const int64_t threshold_ts = m_current_date_ts - (time_shield::SEC_PER_DAY * m_config.auto_delete_days); @@ -326,16 +462,14 @@ namespace logit { /// \param filename The filename to check. /// \return True if the filename matches the pattern, false otherwise. bool is_valid_log_filename(const std::string& filename) const { - static const std::regex pattern(R"((\d{4}-\d{2}-\d{2})\.log)"); - return std::regex_match(filename, pattern); + return filename.size() >= 10 && filename[4] == '-' && filename[7] == '-'; } /// \brief Extracts the date timestamp from the log filename. /// \param filename The filename to extract the date from. /// \return The date timestamp. int64_t get_date_ts_from_filename(const std::string& filename) const { - constexpr size_t EXTENSION_LENGTH = sizeof(".log") - 1; - return time_shield::ts(filename.substr(0, filename.size() - EXTENSION_LENGTH)); + return time_shield::ts(filename.substr(0, 10)); } /// \brief Gets the current UTC date timestamp in seconds. diff --git a/tests/file_logger_rotation_test.cpp b/tests/file_logger_rotation_test.cpp new file mode 100644 index 0000000..10c974e --- /dev/null +++ b/tests/file_logger_rotation_test.cpp @@ -0,0 +1,18 @@ +#define LOGIT_FILE_LOGGER_PATH "." +#include +#include +#include + +int main() { + LOGIT_ADD_FILE_LOGGER_WITH_ROTATION(".", true, 30, "%v", 20, 10); + const std::string msg = "0123456789"; // 10 bytes + LOGIT_INFO(msg); + LOGIT_INFO(msg); + LOGIT_WAIT(); + std::string current = LOGIT_GET_LAST_FILE_PATH(0); + std::string rotated = current; + size_t pos = rotated.rfind(".log"); + rotated.insert(pos, ".1"); + std::ifstream in(rotated); + return in.good() ? 0 : 1; +} diff --git a/tests/file_logger_test.cpp b/tests/file_logger_test.cpp index 15b222f..f5266e0 100644 --- a/tests/file_logger_test.cpp +++ b/tests/file_logger_test.cpp @@ -11,6 +11,10 @@ int main() { const std::string log_path = LOGIT_GET_LAST_FILE_PATH(0); std::ifstream in(log_path); std::string line; - std::getline(in, line); - return line.find(message) != std::string::npos ? 0 : 1; + while (std::getline(in, line)) { + if (line.find(message) != std::string::npos) { + return 0; + } + } + return 1; }