From 15425d849e010d3b42186d826f9448457b08b3a4 Mon Sep 17 00:00:00 2001 From: Matheus Medeiros Sarmento Date: Tue, 29 Mar 2022 11:06:16 -0300 Subject: [PATCH] Add new rotating file sink with different naming formation This is a proposal for existing rotating file sink. In this sink, files will be renamed appending the date in which they were rotated, having so there is no file limit. There is also a protection against multiple files rotating at the same time, by appending ".{index}" --- example/example.cpp | 9 + .../spdlog/sinks/rotating2_file_sink-inl.h | 155 ++++++++++++++++++ include/spdlog/sinks/rotating2_file_sink.h | 77 +++++++++ src/file_sinks.cpp | 4 + tests/includes.h | 1 + tests/test_file_logging.cpp | 65 ++++++++ 6 files changed, 311 insertions(+) create mode 100644 include/spdlog/sinks/rotating2_file_sink-inl.h create mode 100644 include/spdlog/sinks/rotating2_file_sink.h diff --git a/example/example.cpp b/example/example.cpp index c379c746..401e809f 100644 --- a/example/example.cpp +++ b/example/example.cpp @@ -11,6 +11,7 @@ void load_levels_example(); void stdout_logger_example(); void basic_example(); void rotating_example(); +void rotating2_example(); void daily_example(); void async_example(); void binary_example(); @@ -71,6 +72,7 @@ int main(int, char *[]) stdout_logger_example(); basic_example(); rotating_example(); + rotating2_example(); daily_example(); async_example(); binary_example(); @@ -129,6 +131,13 @@ void rotating_example() auto rotating_logger = spdlog::rotating_logger_mt("some_logger_name", "logs/rotating.txt", 1048576 * 5, 3); } +#include "spdlog/sinks/rotating2_file_sink.h" +void rotating2_example() +{ + // Create a file rotating logger with 5mb size max and 3 rotated files. + auto rotating2_logger = spdlog::rotating2_logger_mt("rotating2_logger", "logs/rotating2.txt", 1048576 * 5); +} + #include "spdlog/sinks/daily_file_sink.h" void daily_example() { diff --git a/include/spdlog/sinks/rotating2_file_sink-inl.h b/include/spdlog/sinks/rotating2_file_sink-inl.h new file mode 100644 index 00000000..46c7e734 --- /dev/null +++ b/include/spdlog/sinks/rotating2_file_sink-inl.h @@ -0,0 +1,155 @@ +// Copyright(c) 2015-present, Gabi Melman & spdlog contributors. +// Distributed under the MIT License (http://opensource.org/licenses/MIT) + +#pragma once + +#ifndef SPDLOG_HEADER_ONLY +# include +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace spdlog { +namespace sinks { + +template +SPDLOG_INLINE rotating2_file_sink::rotating2_file_sink(filename_t base_filename, std::size_t max_size, bool rotate_on_open, + pattern_time_type time_type, const file_event_handlers &event_handlers) + : base_filename_(std::move(base_filename)) + , max_size_(max_size) + , time_type_(time_type) + , file_helper_{event_handlers} +{ + if (max_size == 0) + { + throw_spdlog_ex("rotating sink constructor: max_size arg cannot be zero"); + } + + file_helper_.open(base_filename_); + current_size_ = file_helper_.size(); // expensive. called only once + if (rotate_on_open && current_size_ > 0) + { + rotate_(); + current_size_ = 0; + } +} + +// Create filename for the form basename.YYYY-MM-DD-hh-mm-ss +template +filename_t rotating2_file_sink::calc_filename(const filename_t &filename, const tm &now_tm) +{ + filename_t basename, ext; + std::tie(basename, ext) = details::file_helper::split_by_extension(filename); + return fmt_lib::format(SPDLOG_FILENAME_T("{}_{:04d}-{:02d}-{:02d}-{:02d}-{:02d}-{:02d}{}"), basename, now_tm.tm_year + 1900, + now_tm.tm_mon + 1, now_tm.tm_mday, now_tm.tm_hour, now_tm.tm_min, now_tm.tm_sec, ext); +} + +template +SPDLOG_INLINE filename_t rotating2_file_sink::filename() +{ + std::lock_guard lock(base_sink::mutex_); + return file_helper_.filename(); +} + +template +SPDLOG_INLINE void rotating2_file_sink::sink_it_(const details::log_msg &msg) +{ + memory_buf_t formatted; + base_sink::formatter_->format(msg, formatted); + auto new_size = current_size_ + formatted.size(); + + // rotate if the new estimated file size exceeds max size. + // rotate only if the real size > 0 to better deal with full disk (see issue #2261). + // we only check the real size when new_size > max_size_ because it is relatively expensive. + if (new_size > max_size_) + { + file_helper_.flush(); + if (file_helper_.size() > 0) + { + rotate_(); + new_size = formatted.size(); + } + } + file_helper_.write(formatted); + current_size_ = new_size; +} + +template +SPDLOG_INLINE void rotating2_file_sink::flush_() +{ + file_helper_.flush(); +} + +template +SPDLOG_INLINE void rotating2_file_sink::rotate_() +{ + using details::os::filename_to_str; + + file_helper_.close(); + + filename_t src = base_filename_; + if (!details::os::path_exists(src)) + { + return; + } + + auto now = (time_type_ == pattern_time_type::local) ? details::os::localtime(log_clock::to_time_t(log_clock::now())) + : details::os::gmtime(log_clock::to_time_t(log_clock::now())); + + filename_t target = calc_filename(base_filename_, now); + + if (!rename_file_(src, target)) + { + // if failed try again after a small delay. + // this is a workaround to a windows issue, where very high rotation + // rates can cause the rename to fail with permission denied (because of antivirus?). + details::os::sleep_for_millis(100); + if (!rename_file_(src, target)) + { + file_helper_.reopen(true); // truncate the log file anyway to prevent it to grow beyond its limit! + current_size_ = 0; + throw_spdlog_ex("rotating2_file_sink: failed renaming " + filename_to_str(src) + " to " + filename_to_str(target), errno); + } + } + + file_helper_.reopen(true); +} + +// return true on success, false otherwise. +template +SPDLOG_INLINE bool rotating2_file_sink::rename_file_(const filename_t &src_filename, const filename_t &target_filename) +{ + const auto calc_target_filename = [](filename_t filename, int index) { + if (index > 0) + { + filename_t basename, ext; + std::tie(basename, ext) = details::file_helper::split_by_extension(filename); + return fmt_lib::format(SPDLOG_FILENAME_T("{}.{}{}"), basename, index, ext); + } + + return filename; + }; + + int index = 0; + + while (details::os::path_exists(calc_target_filename(target_filename, index))) + { + ++index; + } + + return details::os::rename(src_filename, calc_target_filename(target_filename, index)) == 0; +} + +} // namespace sinks +} // namespace spdlog \ No newline at end of file diff --git a/include/spdlog/sinks/rotating2_file_sink.h b/include/spdlog/sinks/rotating2_file_sink.h new file mode 100644 index 00000000..feb1348b --- /dev/null +++ b/include/spdlog/sinks/rotating2_file_sink.h @@ -0,0 +1,77 @@ +// Copyright(c) 2015-present, Gabi Melman & spdlog contributors. +// Distributed under the MIT License (http://opensource.org/licenses/MIT) + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace spdlog { +namespace sinks { + +// +// Rotating file sink based on size +// +template +class rotating2_file_sink final : public base_sink +{ +public: + rotating2_file_sink(filename_t base_filename, std::size_t max_size, bool rotate_on_open = false, + pattern_time_type time_type = pattern_time_type::local, const file_event_handlers &event_handlers = {}); + static filename_t calc_filename(const filename_t &filename, const tm &now_tm); + filename_t filename(); + +protected: + void sink_it_(const details::log_msg &msg) override; + void flush_() override; + +private: + void rotate_(); + + // delete the target if exists, and rename the src file to target + // return true on success, false otherwise. + bool rename_file_(const filename_t &src_filename, const filename_t &target_filename); + + filename_t base_filename_; + std::size_t max_size_; + std::size_t current_size_; + pattern_time_type time_type_; + details::file_helper file_helper_; +}; + +using rotating2_file_sink_mt = rotating2_file_sink; +using rotating2_file_sink_st = rotating2_file_sink; + +} // namespace sinks + +// +// factory functions +// + +template +inline std::shared_ptr rotating2_logger_mt(const std::string &logger_name, const filename_t &filename, size_t max_file_size, + bool rotate_on_open = false, pattern_time_type time_type = pattern_time_type::local, const file_event_handlers &event_handlers = {}) +{ + return Factory::template create( + logger_name, filename, max_file_size, rotate_on_open, time_type, event_handlers); +} + +template +inline std::shared_ptr rotating2_logger_st(const std::string &logger_name, const filename_t &filename, size_t max_file_size, + bool rotate_on_open = false, pattern_time_type time_type = pattern_time_type::local, const file_event_handlers &event_handlers = {}) +{ + return Factory::template create( + logger_name, filename, max_file_size, rotate_on_open, time_type, event_handlers); +} +} // namespace spdlog + +#ifdef SPDLOG_HEADER_ONLY +# include "rotating2_file_sink-inl.h" +#endif \ No newline at end of file diff --git a/src/file_sinks.cpp b/src/file_sinks.cpp index 10ffba60..3654aa19 100644 --- a/src/file_sinks.cpp +++ b/src/file_sinks.cpp @@ -18,3 +18,7 @@ template class SPDLOG_API spdlog::sinks::basic_file_sink template class SPDLOG_API spdlog::sinks::rotating_file_sink; template class SPDLOG_API spdlog::sinks::rotating_file_sink; + +#include +template class SPDLOG_API spdlog::sinks::rotating2_file_sink; +template class SPDLOG_API spdlog::sinks::rotating2_file_sink; diff --git a/tests/includes.h b/tests/includes.h index 7285873a..e8108c23 100644 --- a/tests/includes.h +++ b/tests/includes.h @@ -22,5 +22,6 @@ #include "spdlog/sinks/null_sink.h" #include "spdlog/sinks/ostream_sink.h" #include "spdlog/sinks/rotating_file_sink.h" +#include "spdlog/sinks/rotating2_file_sink.h" #include "spdlog/sinks/stdout_color_sinks.h" #include "spdlog/pattern_formatter.h" \ No newline at end of file diff --git a/tests/test_file_logging.cpp b/tests/test_file_logging.cpp index 1c7a1853..32b4db0d 100644 --- a/tests/test_file_logging.cpp +++ b/tests/test_file_logging.cpp @@ -5,6 +5,7 @@ #define SIMPLE_LOG "test_logs/simple_log" #define ROTATING_LOG "test_logs/rotating_log" +#define ROTATING2_LOG "test_logs/rotating2_log" TEST_CASE("simple_file_logger", "[simple_logger]]") { @@ -107,3 +108,67 @@ TEST_CASE("rotating_file_logger3", "[rotating_logger]]") spdlog::filename_t basename = SPDLOG_FILENAME_T(ROTATING_LOG); REQUIRE_THROWS_AS(spdlog::rotating_logger_mt("logger", basename, max_size, 0), spdlog::spdlog_ex); } + +TEST_CASE("rotating2_file_logger1", "[rotating2_logger]]") +{ + prepare_logdir(); + size_t max_size = 1024 * 10; + spdlog::filename_t basename = SPDLOG_FILENAME_T(ROTATING2_LOG); + auto logger = spdlog::rotating2_logger_mt("logger", basename, max_size, 0); + + for (int i = 0; i < 10; ++i) + { + logger->info("Test message {}", i); + } + + logger->flush(); + require_message_count(ROTATING2_LOG, 10); +} + +TEST_CASE("rotating2_file_logger2", "[rotating2_logger]]") +{ + prepare_logdir(); + size_t max_size = 1024 * 10; + spdlog::filename_t basename = SPDLOG_FILENAME_T(ROTATING2_LOG); + + { + // make an initial logger to create the first output file + auto logger = spdlog::rotating2_logger_mt("logger", basename, max_size, true); + for (int i = 0; i < 10; ++i) + { + logger->info("Test message {}", i); + } + // drop causes the logger destructor to be called, which is required so the + // next logger can rename the first output file. + spdlog::drop(logger->name()); + } + + auto logger = spdlog::rotating2_logger_mt("logger", basename, max_size, true); + for (int i = 0; i < 10; ++i) + { + logger->info("Test message {}", i); + } + + logger->flush(); + + require_message_count(ROTATING2_LOG, 10); + + for (int i = 0; i < 1000; i++) + { + + logger->info("Test message {}", i); + } + + logger->flush(); + REQUIRE(get_filesize(ROTATING2_LOG) <= max_size); + // REQUIRE(get_filesize(ROTATING2_LOG ".1") <= max_size); +} + +// test that passing max_size=0 throws +TEST_CASE("rotating2_file_logger3", "[rotating2_logger]]") +{ + prepare_logdir(); + size_t max_size = 0; + spdlog::filename_t basename = SPDLOG_FILENAME_T(ROTATING2_LOG); + REQUIRE_THROWS_AS(spdlog::rotating2_logger_mt("logger", basename, max_size, 0), spdlog::spdlog_ex); +} \ No newline at end of file