diff --git a/include/spdlog/sinks/daily_file_sink.h b/include/spdlog/sinks/daily_file_sink.h index 7c025855..91446b53 100644 --- a/include/spdlog/sinks/daily_file_sink.h +++ b/include/spdlog/sinks/daily_file_sink.h @@ -17,6 +17,7 @@ #include #include #include +#include namespace spdlog { namespace sinks { @@ -58,6 +59,26 @@ struct daily_filename_calculator return SPDLOG_FILENAME_T(""); } + static std::map calc_dates_to_filenames(const filename_t &base_filename) + { + const filename_t dir = details::os::dir_name(base_filename); + const std::vector dir_files = details::os::get_directory_files(dir); + + // lexicographical order ensures files are sorted by created date. + std::map dates_to_filenames; + + for(const filename_t& dir_file : dir_files) + { + const filename_t date_suffix = daily_filename_calculator::extract_date_suffix(base_filename, dir_file); + if(!date_suffix.empty()) + { + dates_to_filenames[date_suffix] = dir_file; + } + } + + return dates_to_filenames; + } + static filename_t filename_prefix_symbol() { return SPDLOG_FILENAME_T("_"); @@ -74,7 +95,9 @@ class daily_file_sink final : public base_sink { public: // create daily file sink which rotates on given time - daily_file_sink(filename_t base_filename, int rotation_hour, int rotation_minute, bool truncate = false, uint16_t max_files = 0) + // initial_file_tp - useful for testing especially when we want to verify the delete_old_files_on_init behaviour + daily_file_sink(filename_t base_filename, int rotation_hour, int rotation_minute, bool truncate = false, uint16_t max_files = 0, bool delete_old_files_on_init = false, + log_clock::time_point initial_file_tp = log_clock::now()) : base_filename_(std::move(base_filename)) , rotation_h_(rotation_hour) , rotation_m_(rotation_minute) @@ -87,14 +110,13 @@ public: throw_spdlog_ex("daily_file_sink: Invalid rotation time in ctor"); } - auto now = log_clock::now(); - auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(now)); + auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(initial_file_tp)); file_helper_.open(filename, truncate_); rotation_tp_ = next_rotation_tp_(); if (max_files_ > 0) { - init_filenames_q_(); + init_filenames_q_(delete_old_files_on_init); } } @@ -132,26 +154,45 @@ protected: } private: - void init_filenames_q_() + void init_filenames_q_(bool delete_old_file_on_init) { using details::os::path_exists; + using details::os::remove_if_exists; filenames_q_ = details::circular_q(static_cast(max_files_)); - std::vector filenames; - auto now = log_clock::now(); - while (filenames.size() < max_files_) + + // Because the map key is the date in yyyy-mm--dd, it is sorted by lexicographical order. + const std::map dates_to_filenames = daily_filename_calculator::calc_dates_to_filenames(base_filename_); + + const auto first_valid_file_pos = max_files_ > 0 && dates_to_filenames.size() > max_files_ ? dates_to_filenames.size() - max_files_ : 0; + + auto first_valid_file_iter = dates_to_filenames.begin(); + + if(first_valid_file_pos > 0) + { + std::advance(first_valid_file_iter, first_valid_file_pos); + } + + std::vector recent_files; + for(auto iter = first_valid_file_iter; iter != dates_to_filenames.end(); ++iter) { - auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(now)); - if (!path_exists(filename)) + recent_files.emplace_back(iter->second); + } + + for (auto iter = recent_files.begin(); iter != recent_files.end(); ++iter) + { + if(path_exists(*iter)) { - break; + filenames_q_.push_back(std::move(*iter)); } - filenames.emplace_back(filename); - now -= std::chrono::hours(24); } - for (auto iter = filenames.rbegin(); iter != filenames.rend(); ++iter) + + if(max_files_ > 0 && delete_old_file_on_init) { - filenames_q_.push_back(std::move(*iter)); + for(auto iter = dates_to_filenames.begin(); iter != first_valid_file_iter; ++iter) + { + remove_if_exists(iter->second); + } } } @@ -218,15 +259,17 @@ using daily_file_sink_st = daily_file_sink; // template inline std::shared_ptr daily_logger_mt( - const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0, bool truncate = false, uint16_t max_files = 0) + const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0, bool truncate = false, uint16_t max_files = 0, bool delete_old_files_on_init = false, + log_clock::time_point initial_file_tp = log_clock::now()) { - return Factory::template create(logger_name, filename, hour, minute, truncate, max_files); + return Factory::template create(logger_name, filename, hour, minute, truncate, max_files, delete_old_files_on_init, initial_file_tp); } template inline std::shared_ptr daily_logger_st( - const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0, bool truncate = false, uint16_t max_files = 0) + const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0, bool truncate = false, uint16_t max_files = 0, bool delete_old_files_on_init = false, + log_clock::time_point initial_file_tp = log_clock::now()) { - return Factory::template create(logger_name, filename, hour, minute, truncate, max_files); + return Factory::template create(logger_name, filename, hour, minute, truncate, max_files, delete_old_files_on_init, initial_file_tp); } } // namespace spdlog diff --git a/tests/test_daily_logger.cpp b/tests/test_daily_logger.cpp index 98cbfe54..c21af8d8 100644 --- a/tests/test_daily_logger.cpp +++ b/tests/test_daily_logger.cpp @@ -168,4 +168,34 @@ TEST_CASE("daily_logger rotate", "[daily_file_sink]") test_rotate(days_to_run, 10, 10); test_rotate(days_to_run, 11, 10); test_rotate(days_to_run, 20, 10); +} + +TEST_CASE("daily_logger should delete oldest file on init", "[daily_file_sink]") +{ + using spdlog::log_clock; + using spdlog::details::log_msg; + using spdlog::sinks::daily_file_sink_st; + + prepare_logdir(); + + std::string basename = "test_logs/daily_rotate.txt"; + + daily_file_sink_st sink{basename, 2, 30, false, 8, true}; + + // simulate messages with 24 intervals. create more than max files of second sink below + for (int i = 0; i < 8; i++) + { + auto offset = std::chrono::seconds{24 * 3600 * i}; + sink.log(create_msg(offset)); + } + + REQUIRE(count_files("test_logs") == static_cast(8)); + + // This will create a 9th file in logdir, therefore the ctor wil delete the oldest two files. + auto initial_file_tp = log_clock::now() + std::chrono::seconds{24 * 3600 * 8}; + + // Second sink should load existing files in the logdir and delete the oldest because the max files count is 7 + daily_file_sink_st sink2{basename, 2, 30, false, 7, true, initial_file_tp}; + + REQUIRE(count_files("test_logs") == static_cast(7)); } \ No newline at end of file