diff --git a/CMakeLists.txt b/CMakeLists.txt index ed53776..518bb15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,9 @@ project(log-it-cpp VERSION 1.0.0 LANGUAGES CXX) option(LOG_IT_CPP_BUILD_TESTS "Build log-it-cpp tests" ${PROJECT_IS_TOP_LEVEL}) option(LOG_IT_CPP_BUILD_EXAMPLES "Build log-it-cpp examples" OFF) +option(LOGIT_WITH_GZIP "Enable gzip via zlib" OFF) +option(LOGIT_WITH_ZSTD "Enable zstd" OFF) +option(LOGIT_USE_SUBMODULES "Allow bundled third_party fallback" OFF) if(NOT DEFINED CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 11) @@ -30,6 +33,50 @@ target_include_directories(log-it-cpp INTERFACE target_link_libraries(log-it-cpp INTERFACE time_shield::time_shield) +# ---------- GZIP (zlib) ---------- +if(LOGIT_WITH_GZIP) + if(NOT TARGET ZLIB::ZLIB) + find_package(ZLIB QUIET) + endif() + if(NOT TARGET ZLIB::ZLIB AND LOGIT_USE_SUBMODULES) + add_subdirectory(libs/zlib EXCLUDE_FROM_ALL) + if(TARGET zlibstatic) + add_library(ZLIB::ZLIB ALIAS zlibstatic) + elseif(TARGET zlib) + add_library(ZLIB::ZLIB ALIAS zlib) + endif() + endif() + if(NOT TARGET ZLIB::ZLIB) + message(FATAL_ERROR "ZLIB not found. Enable LOGIT_USE_SUBMODULES or install zlib.") + endif() + target_compile_definitions(log-it-cpp INTERFACE LOGIT_HAS_ZLIB=1) + target_link_libraries(log-it-cpp INTERFACE ZLIB::ZLIB) +endif() + +# ---------- ZSTD ---------- +if(LOGIT_WITH_ZSTD) + if(NOT TARGET ZSTD::ZSTD) + find_package(ZSTD QUIET) + endif() + if(NOT TARGET ZSTD::ZSTD AND LOGIT_USE_SUBMODULES) + add_subdirectory(libs/zstd/build/cmake EXCLUDE_FROM_ALL) + if(TARGET libzstd_static) + add_library(ZSTD::ZSTD ALIAS libzstd_static) + elseif(TARGET libzstd_shared) + add_library(ZSTD::ZSTD ALIAS libzstd_shared) + endif() + endif() + if(NOT TARGET ZSTD::ZSTD) + message(FATAL_ERROR "ZSTD not found. Enable LOGIT_USE_SUBMODULES or install zstd.") + endif() + target_compile_definitions(log-it-cpp INTERFACE LOGIT_HAS_ZSTD=1) + target_link_libraries(log-it-cpp INTERFACE ZSTD::ZSTD) +endif() + +if(DEFINED VCPKG_TARGET_TRIPLET) + set_property(CACHE LOGIT_USE_SUBMODULES PROPERTY VALUE OFF) +endif() + if(LOG_IT_CPP_BUILD_TESTS) enable_testing() add_subdirectory(tests) diff --git a/README.md b/README.md index 9c873c2..28787d7 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,11 @@ auto now = std::chrono::system_clock::now(); LOGIT_PRINT_INFO("TimePoint example: ", now); ``` -- **Support for Multiple Backends**: +- **Rotating File Logs**: + + Automatic file rotation based on size with optional asynchronous compression using gzip or zstd. + +- **Support for Multiple Backends**: Easily configure loggers for console and file output. If necessary, add support for sending messages to servers or databases by creating custom backends. diff --git a/include/logit_cpp/logit/LogMacros.hpp b/include/logit_cpp/logit/LogMacros.hpp index e932e0a..e37ab63 100644 --- a/include/logit_cpp/logit/LogMacros.hpp +++ b/include/logit_cpp/logit/LogMacros.hpp @@ -937,8 +937,7 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT 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), \ + LOGIT_FILE_LOGGER_MAX_FILE_SIZE_BYTES, LOGIT_FILE_LOGGER_MAX_ROTATED_FILES), \ std::make_unique(LOGIT_FILE_LOGGER_PATTERN)) /// \brief Macro for adding a unique file logger with custom parameters. @@ -1129,8 +1128,7 @@ static_assert(LOGIT_LEVEL_FATAL == static_cast(logit::LogLevel::LOG_LVL_FAT 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)), \ + LOGIT_FILE_LOGGER_MAX_FILE_SIZE_BYTES, LOGIT_FILE_LOGGER_MAX_ROTATED_FILES)), \ std::unique_ptr(new logit::SimpleLogFormatter(LOGIT_FILE_LOGGER_PATTERN))) /// \brief Macro for adding a unique file logger with custom parameters. diff --git a/include/logit_cpp/logit/config.hpp b/include/logit_cpp/logit/config.hpp index 921d230..7a01f8c 100644 --- a/include/logit_cpp/logit/config.hpp +++ b/include/logit_cpp/logit/config.hpp @@ -117,12 +117,6 @@ #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". diff --git a/include/logit_cpp/logit/detail/CompressionWorker.hpp b/include/logit_cpp/logit/detail/CompressionWorker.hpp new file mode 100644 index 0000000..366ab81 --- /dev/null +++ b/include/logit_cpp/logit/detail/CompressionWorker.hpp @@ -0,0 +1,231 @@ +#pragma once +#ifndef _LOGIT_COMPRESSION_WORKER_HPP_INCLUDED +#define _LOGIT_COMPRESSION_WORKER_HPP_INCLUDED + +/// \file CompressionWorker.hpp +/// \brief Background worker that compresses rotated log files. + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(LOGIT_HAS_ZLIB) +# include +#endif +#if defined(LOGIT_HAS_ZSTD) +# include +#endif + +#include "../enums.hpp" + +namespace logit { namespace detail { + + /// \brief Compress a file using the specified compression type. + /// \param type Compression algorithm to use. + /// \param src Path to the source file. + /// \param level Compression level. + /// \param external_cmd Command template for CompressType::EXTERNAL_CMD. + /// \return true on success. + bool compress_file(CompressType type, + const std::string& src, + int level, + const std::string& external_cmd); + + /// \class CompressionWorker + /// \brief Background worker performing asynchronous compression. + class CompressionWorker { + public: + CompressionWorker(CompressType type, + int level, + std::string external_cmd); + ~CompressionWorker(); + + /// \brief Enqueue a file for compression. + void enqueue(std::string path); + + /// \brief Wait until all queued files are processed. + void wait(); + + private: + void run(); + + CompressType m_type; + int m_level; + std::string m_external_cmd; + std::queue m_q; + std::thread m_thread; + std::mutex m_mx; + std::condition_variable m_cv; + std::condition_variable m_cv_idle; + bool m_stop = false; + bool m_busy = false; + }; + + inline int clamp_level(int v, int lo, int hi) { + if (v < lo) return lo; + if (v > hi) return hi; + return v; + } + + inline bool compress_file_gzip(const std::string& src, + const std::string& dst_tmp, + int level) { +# if defined(LOGIT_HAS_ZLIB) + std::ifstream in(src.c_str(), std::ios::binary); + if (!in) return false; + + char mode[8] = "wb6"; + mode[2] = char('0' + clamp_level(level, 1, 9)); + gzFile out = gzopen(dst_tmp.c_str(), mode); + if (!out) return false; + gzbuffer(out, 256 * 1024); + + std::vector buf(256 * 1024); + while (in) { + in.read(&buf[0], buf.size()); + std::streamsize got = in.gcount(); + if (got > 0) { + int written = gzwrite(out, &buf[0], static_cast(got)); + if (written == 0) { gzclose(out); return false; } + } + } + return gzclose(out) == Z_OK; +# else + (void)src; (void)dst_tmp; (void)level; return false; +# endif + } + + inline bool compress_file_zstd(const std::string& src, + const std::string& dst_tmp, + int level) { +# if defined(LOGIT_HAS_ZSTD) + std::ifstream in(src.c_str(), std::ios::binary); + std::ofstream out(dst_tmp.c_str(), std::ios::binary | std::ios::trunc); + if (!in || !out) return false; + + ZSTD_CCtx* cctx = ZSTD_createCCtx(); + if (!cctx) return false; + ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, clamp_level(level, 1, 19)); + + const size_t inChunk = ZSTD_CStreamInSize(); + const size_t outChunk = ZSTD_CStreamOutSize(); + std::vector inBuf(inChunk), outBuf(outChunk); + + for (;;) { + in.read(&inBuf[0], inBuf.size()); + size_t rd = static_cast(in.gcount()); + ZSTD_inBuffer zin = { &inBuf[0], rd, 0 }; + const int last = in.eof() ? 1 : 0; + + while (zin.pos < zin.size || last) { + ZSTD_outBuffer zout = { &outBuf[0], outBuf.size(), 0 }; + size_t ret = last + ? ZSTD_compressStream2(cctx, &zout, &zin, ZSTD_e_end) + : ZSTD_compressStream2(cctx, &zout, &zin, ZSTD_e_continue); + if (ZSTD_isError(ret)) { ZSTD_freeCCtx(cctx); return false; } + out.write(&outBuf[0], static_cast(zout.pos)); + if (!out) { ZSTD_freeCCtx(cctx); return false; } + if (last && ret == 0 && zin.pos == zin.size) { + ZSTD_freeCCtx(cctx); + out.flush(); + return out.good(); + } + if (!last && zin.pos == zin.size) break; + } + } +# else + (void)src; (void)dst_tmp; (void)level; return false; +# endif + } + + inline bool compress_file_external(const std::string& src, + const std::string& cmd_tpl, + int level) { + if (cmd_tpl.empty()) return false; + std::string cmd = cmd_tpl; + size_t pos = cmd.find("{file}"); + if (pos != std::string::npos) cmd.replace(pos, 6, "\"" + src + "\""); + pos = cmd.find("{level}"); + if (pos != std::string::npos) cmd.replace(pos, 7, std::to_string(level)); + return std::system(cmd.c_str()) == 0; + } + + inline bool compress_file(CompressType type, + const std::string& src, + int level, + const std::string& external_cmd) { + if (type == CompressType::NONE) return true; + if (type == CompressType::EXTERNAL_CMD) { + return compress_file_external(src, external_cmd, level); + } + std::string dst = src + (type == CompressType::GZIP ? ".gz" : ".zst"); + std::string tmp = dst + ".tmp"; + bool ok = false; + if (type == CompressType::GZIP) ok = compress_file_gzip(src, tmp, level); + else ok = compress_file_zstd(src, tmp, level); + if (!ok) { std::remove(tmp.c_str()); return false; } + if (std::rename(tmp.c_str(), dst.c_str()) != 0) { std::remove(tmp.c_str()); return false; } + std::remove(src.c_str()); + return true; + } + + inline CompressionWorker::CompressionWorker(CompressType type, + int level, + std::string external_cmd) + : m_type(type), m_level(level), m_external_cmd(std::move(external_cmd)) { + if (m_type != CompressType::NONE) { + m_thread = std::thread(&CompressionWorker::run, this); + } + } + + inline CompressionWorker::~CompressionWorker() { + { + std::lock_guard lk(m_mx); + m_stop = true; + m_cv.notify_all(); + } + if (m_thread.joinable()) m_thread.join(); + } + + inline void CompressionWorker::enqueue(std::string path) { + std::lock_guard lk(m_mx); + m_q.push(std::move(path)); + m_cv.notify_one(); + } + + inline void CompressionWorker::wait() { + std::unique_lock lk(m_mx); + m_cv_idle.wait(lk, [this]{ return m_q.empty() && !m_busy; }); + } + + inline void CompressionWorker::run() { + for (;;) { + std::string src; + { + std::unique_lock lk(m_mx); + m_cv.wait(lk, [this]{ return m_stop || !m_q.empty(); }); + if (m_stop && m_q.empty()) break; + src = std::move(m_q.front()); + m_q.pop(); + m_busy = true; + } + + compress_file(m_type, src, m_level, m_external_cmd); + + { + std::lock_guard lk(m_mx); + m_busy = false; + if (m_q.empty()) m_cv_idle.notify_all(); + } + } + } + +}} // namespace logit::detail + +#endif // _LOGIT_COMPRESSION_WORKER_HPP_INCLUDED + diff --git a/include/logit_cpp/logit/enums.hpp b/include/logit_cpp/logit/enums.hpp index 52994cf..1918585 100644 --- a/include/logit_cpp/logit/enums.hpp +++ b/include/logit_cpp/logit/enums.hpp @@ -51,6 +51,23 @@ namespace logit { TimeSinceLastLog ///< The time elapsed since the last log in seconds. }; + /// \enum CompressType + /// \brief Supported compression algorithms for rotated files. + enum class CompressType { + NONE, ///< Do not compress rotated files. + GZIP, ///< Compress using gzip (zlib). + ZSTD, ///< Compress using zstd. + EXTERNAL_CMD ///< Use an external command for compression. + }; + + /// \enum RotationNaming + /// \brief Naming policy for rotated log files. + enum class RotationNaming { + Sequence, ///< Append a numeric sequence: YYYY-MM-DD.001.log + Timestamp, ///< Append HHMMSS timestamp: YYYY-MM-DD_HHMMSS.log + TimestampMs ///< Append HHMMSSmmm timestamp: YYYY-MM-DD_HHMMSSmmm.log + }; + /// \brief Convert LogLevel to a C-style string representation. /// \param level The log level. /// \param mode The output mode (0 for full name, 1 for abbreviation). diff --git a/include/logit_cpp/logit/loggers/FileLogger.hpp b/include/logit_cpp/logit/loggers/FileLogger.hpp index ae338a4..ba749c1 100644 --- a/include/logit_cpp/logit/loggers/FileLogger.hpp +++ b/include/logit_cpp/logit/loggers/FileLogger.hpp @@ -6,11 +6,14 @@ /// \brief File logger implementation that outputs logs to files with rotation and deletion of old logs. #include "ILogger.hpp" +#include "../enums.hpp" +#ifndef __EMSCRIPTEN__ +#include "../detail/CompressionWorker.hpp" +#endif #include #include #include #include -#include #include #include #include @@ -18,6 +21,9 @@ #include #include #include +#include +#include +#include #include namespace logit { @@ -32,15 +38,18 @@ namespace logit { 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; + CompressType compress = CompressType::NONE; + int compress_level = 1; + bool compress_async = true; + std::string external_cmd; + RotationNaming naming = RotationNaming::Sequence; + uint32_t seq_width = 3; }; FileLogger() { warn(); } FileLogger(const Config&) { 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(); } + const uint64_t& = 0, const uint32_t& = 0) { warn(); } void log(const LogRecord&, const std::string&) override { warn(); } std::string get_string_param(const LoggerParam&) const override { return {}; } @@ -79,8 +88,12 @@ namespace logit { 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. + CompressType compress = CompressType::NONE; ///< Compression algorithm for rotated files. + int compress_level = 1; ///< Compression level. + bool compress_async = true; ///< Run compression in background thread. + std::string external_cmd; ///< External command template. + RotationNaming naming = RotationNaming::Sequence; ///< Naming policy for rotated files. + uint32_t seq_width = 3; ///< Width of sequence index. }; /// \brief Default constructor that uses default configuration. @@ -114,22 +127,19 @@ namespace logit { 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 = {}) { + uint32_t max_rotated_files) { 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(); + if (m_compressor) m_compressor->wait(); } /// \brief Logs a message to a file with thread safety. @@ -227,6 +237,7 @@ namespace logit { 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::unique_ptr m_compressor; ///< Background compressor. 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)); @@ -327,93 +338,163 @@ namespace logit { 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; - } +# if defined(_WIN32) + const std::string cur = dir + "\\" + base + ".log"; # 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; - } + const std::string cur = dir + "/" + base + ".log"; # endif + std::string rotated = make_rotated_name(base, dir); # 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()); + open_log_file(m_current_date_ts); + m_current_file_size = 0; + + if (m_config.compress != CompressType::NONE) { + if (m_config.compress_async) { + if (!m_compressor) { + m_compressor.reset(new detail::CompressionWorker( + m_config.compress, m_config.compress_level, m_config.external_cmd)); + } + m_compressor->enqueue(rotated); + } else { + detail::compress_file(m_config.compress, rotated, m_config.compress_level, m_config.external_cmd); + } } 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(\..*)?)"); + std::vector files; 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 (name.rfind(base, 0) == 0 && name != base + ".log") { + files.emplace_back(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; }); + std::sort(files.begin(), files.end()); size_t to_remove = files.size() - max_files; for (size_t i = 0; i < to_remove; ++i) { - fs::remove(files[i].second); + fs::remove(files[i]); } # else - std::vector> files; - std::regex pattern(base + R"(\.(\d+)\.log(\..*)?)"); + std::vector files; 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 (name.rfind(base, 0) == 0 && name != base + ".log") { + files.emplace_back(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; }); + std::sort(files.begin(), files.end()); 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()); + remove(utf8_to_ansi(files[i]).c_str()); # else - remove(files[i].second.c_str()); + remove(files[i].c_str()); # endif } # endif } + std::string make_rotated_name(const std::string& base, const std::string& dir) const { + switch (m_config.naming) { + case RotationNaming::Sequence: + return make_sequence_name(base, dir); + case RotationNaming::Timestamp: + case RotationNaming::TimestampMs: + return make_timestamp_name(base, dir); + } + return make_sequence_name(base, dir); + } + + std::string make_sequence_name(const std::string& base, const std::string& dir) const { + uint32_t idx = 1; + std::string rotated; +# if __cplusplus >= 201703L + for (;; ++idx) { + std::ostringstream oss; + oss << dir << "/" << base << '.' << std::setw(m_config.seq_width) + << std::setfill('0') << idx << ".log"; + rotated = oss.str(); + 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) { + std::ostringstream oss; + oss << dir << "/" << base << '.' << std::setw(m_config.seq_width) + << std::setfill('0') << idx << ".log"; + rotated = oss.str(); + if (!file_exists(rotated)) break; + } +# endif + return rotated; + } + + std::string make_timestamp_name(const std::string& base, const std::string& dir) const { + int64_t ts_ms = LOGIT_CURRENT_TIMESTAMP_MS(); + time_t sec = static_cast(time_shield::ms_to_sec(ts_ms)); + std::tm tm{}; +# if defined(_WIN32) + gmtime_s(&tm, &sec); +# else + gmtime_r(&sec, &tm); +# endif + char buf[32]; + std::strftime(buf, sizeof(buf), "%H%M%S", &tm); + std::string timepart(buf); + if (m_config.naming == RotationNaming::TimestampMs) { + char msbuf[4]; + std::snprintf(msbuf, sizeof(msbuf), "%03d", static_cast(ts_ms % 1000)); + timepart += msbuf; + } + std::string rotated = dir + "/" + base + "_" + timepart + ".log"; +# if __cplusplus >= 201703L + if (!fs::exists(rotated)) return rotated; + uint32_t idx = 1; + for (;; ++idx) { + std::string candidate = rotated.substr(0, rotated.size() - 4) + "." + std::to_string(idx) + ".log"; + if (!fs::exists(candidate)) return candidate; + } +# 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(); + }; + if (!file_exists(rotated)) return rotated; + uint32_t idx = 1; + for (;; ++idx) { + std::ostringstream oss; + oss << rotated.substr(0, rotated.size() - 4) << '.' << idx << ".log"; + std::string candidate = oss.str(); + if (!file_exists(candidate)) return candidate; + } +# 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); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fe9a0a6..adbc584 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,4 +1,11 @@ file(GLOB TEST_SOURCES *.cpp) +if(NOT LOGIT_WITH_GZIP) + list(REMOVE_ITEM TEST_SOURCES ${CMAKE_CURRENT_LIST_DIR}/file_logger_gzip_compression_test.cpp) + list(REMOVE_ITEM TEST_SOURCES ${CMAKE_CURRENT_LIST_DIR}/file_logger_external_cmd_compression_test.cpp) +endif() +if(NOT LOGIT_WITH_ZSTD) + list(REMOVE_ITEM TEST_SOURCES ${CMAKE_CURRENT_LIST_DIR}/file_logger_zstd_compression_test.cpp) +endif() foreach(test_src ${TEST_SOURCES}) get_filename_component(test_name ${test_src} NAME_WE) add_executable(${test_name} ${test_src}) diff --git a/tests/file_logger_external_cmd_compression_test.cpp b/tests/file_logger_external_cmd_compression_test.cpp new file mode 100644 index 0000000..51804b3 --- /dev/null +++ b/tests/file_logger_external_cmd_compression_test.cpp @@ -0,0 +1,42 @@ +#include +#if defined(LOGIT_HAS_ZLIB) +#include +#include +#include + +int main() { + std::system("rm -rf ext_cmd_test"); + logit::FileLogger::Config cfg; + cfg.directory = "ext_cmd_test"; + cfg.compress = logit::CompressType::EXTERNAL_CMD; + cfg.external_cmd = "gzip -k \"{file}\""; + cfg.max_file_size_bytes = 20; + cfg.compress_async = false; + logit::Logger::get_instance().add_logger( + std::unique_ptr(new logit::FileLogger(cfg)), + std::unique_ptr(new logit::SimpleLogFormatter("%v"))); + + const std::string msg = "0123456789"; + LOGIT_INFO(msg); + LOGIT_INFO(msg); + LOGIT_WAIT(); + std::string current = LOGIT_GET_LAST_FILE_PATH(0); + LOGIT_SHUTDOWN(); + + std::string rotated = current; + size_t pos = rotated.rfind(".log"); + rotated.insert(pos, ".001"); + std::string gz = rotated + ".gz"; + + gzFile gzfile = gzopen(gz.c_str(), "rb"); + if (!gzfile) return 1; + char buf[128]; + std::string out; + int n; + while ((n = gzread(gzfile, buf, sizeof(buf))) > 0) out.append(buf, n); + gzclose(gzfile); + return out.find(msg) != std::string::npos ? 0 : 1; +} +#else +int main() { return 0; } +#endif diff --git a/tests/file_logger_gzip_compression_test.cpp b/tests/file_logger_gzip_compression_test.cpp new file mode 100644 index 0000000..9a57bea --- /dev/null +++ b/tests/file_logger_gzip_compression_test.cpp @@ -0,0 +1,41 @@ +#include +#if defined(LOGIT_HAS_ZLIB) +#include +#include +#include + +int main() { + std::system("rm -rf gzip_test"); + logit::FileLogger::Config cfg; + cfg.directory = "gzip_test"; + cfg.compress = logit::CompressType::GZIP; + cfg.max_file_size_bytes = 20; + cfg.compress_async = false; + logit::Logger::get_instance().add_logger( + std::unique_ptr(new logit::FileLogger(cfg)), + std::unique_ptr(new logit::SimpleLogFormatter("%v"))); + + const std::string msg = "0123456789"; + LOGIT_INFO(msg); + LOGIT_INFO(msg); + LOGIT_WAIT(); + std::string current = LOGIT_GET_LAST_FILE_PATH(0); + LOGIT_SHUTDOWN(); + + std::string rotated = current; + size_t pos = rotated.rfind(".log"); + rotated.insert(pos, ".001"); + rotated += ".gz"; + + gzFile gz = gzopen(rotated.c_str(), "rb"); + if (!gz) return 1; + char buf[128]; + std::string out; + int n; + while ((n = gzread(gz, buf, sizeof(buf))) > 0) out.append(buf, n); + gzclose(gz); + return out.find(msg) != std::string::npos ? 0 : 1; +} +#else +int main() { return 0; } +#endif diff --git a/tests/file_logger_rotation_test.cpp b/tests/file_logger_rotation_test.cpp index 10c974e..3d4b2e2 100644 --- a/tests/file_logger_rotation_test.cpp +++ b/tests/file_logger_rotation_test.cpp @@ -12,7 +12,7 @@ int main() { std::string current = LOGIT_GET_LAST_FILE_PATH(0); std::string rotated = current; size_t pos = rotated.rfind(".log"); - rotated.insert(pos, ".1"); + rotated.insert(pos, ".001"); std::ifstream in(rotated); return in.good() ? 0 : 1; } diff --git a/tests/file_logger_zstd_compression_test.cpp b/tests/file_logger_zstd_compression_test.cpp new file mode 100644 index 0000000..7c2b96f --- /dev/null +++ b/tests/file_logger_zstd_compression_test.cpp @@ -0,0 +1,50 @@ +#include +#if defined(LOGIT_HAS_ZSTD) +#include +#include +#include +#include +#include + +int main() { + std::system("rm -rf zstd_test"); + logit::FileLogger::Config cfg; + cfg.directory = "zstd_test"; + cfg.compress = logit::CompressType::ZSTD; + cfg.compress_level = 3; + cfg.max_file_size_bytes = 20; + cfg.compress_async = false; + logit::Logger::get_instance().add_logger( + std::unique_ptr(new logit::FileLogger(cfg)), + std::unique_ptr(new logit::SimpleLogFormatter("%v"))); + + const std::string msg = "0123456789"; + LOGIT_INFO(msg); + LOGIT_INFO(msg); + LOGIT_WAIT(); + std::string current = LOGIT_GET_LAST_FILE_PATH(0); + LOGIT_SHUTDOWN(); + + std::string rotated = current; + size_t pos = rotated.rfind(".log"); + rotated.insert(pos, ".001"); + rotated += ".zst"; + + std::ifstream in(rotated.c_str(), std::ios::binary | std::ios::ate); + if (!in) return 1; + std::streamsize size = in.tellg(); + in.seekg(0, std::ios::beg); + std::vector compressed(static_cast(size)); + if (!in.read(compressed.data(), size)) return 1; + + unsigned long long raw_size = ZSTD_getFrameContentSize(compressed.data(), compressed.size()); + if (raw_size == ZSTD_CONTENTSIZE_ERROR || raw_size == ZSTD_CONTENTSIZE_UNKNOWN) return 1; + std::vector decompressed(static_cast(raw_size)); + size_t ret = ZSTD_decompress(decompressed.data(), raw_size, compressed.data(), compressed.size()); + if (ZSTD_isError(ret)) return 1; + std::string out(decompressed.data(), ret); + return out.find(msg) != std::string::npos ? 0 : 1; +} +#else +int main() { return 0; } +#endif