Skip to content

Commit 69023b4

Browse files
committed
Fix DailyFileSink file overwrite bug on restart; add unit tests
1 parent b65f228 commit 69023b4

File tree

5 files changed

+635
-79
lines changed

5 files changed

+635
-79
lines changed

CHANGELOG

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
# v1.0.0.4 - 10-02-2025
2+
- Fix critical DailyFileSink bug: prevent file overwrites on program restart
3+
- Remove current_day_index_ member to eliminate restart index reset issue
4+
- Add rotate_files_for_date() helper function to properly rotate existing files
5+
- Implement startup rotation check in constructor to handle old log files
6+
- Fix max_files enforcement to prevent creating files beyond the limit
7+
- Support max_file_size=0 to disable size-based rotation
8+
- Proper handling of dated files as index 0 with indexed files starting from _001
9+
- Add comprehensive unit tests for DailyFileSink rotation scenarios
10+
- Test size rotation with max_files limit enforcement
11+
- Test restart scenarios with old files from previous days
12+
- Test restart with existing rotated files to prevent overwrites
13+
114
# v1.0.0.3 - 10-01-2025
215
- upgrade slick_queue to v1.0.2.1
316

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cmake_minimum_required(VERSION 3.20)
22

3-
set(BUILD_VERSION 1.0.0.3)
3+
set(BUILD_VERSION 1.0.0.4)
44
project(slick_logger VERSION ${BUILD_VERSION} LANGUAGES CXX)
55

66
set(CMAKE_CXX_STANDARD 20)

include/slick_logger/logger.hpp

Lines changed: 169 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,8 @@ class DailyFileSink : public FileSink {
383383
protected:
384384
void check_rotation();
385385
virtual std::string get_date_string();
386+
void rotate_daily_files();
387+
void rotate_files_for_date(const std::string& date);
386388

387389
std::filesystem::path get_daily_filename();
388390
std::filesystem::path get_dated_filename(const std::string& date);
@@ -392,7 +394,6 @@ class DailyFileSink : public FileSink {
392394
std::filesystem::path base_path_;
393395
std::string current_date_;
394396
size_t current_file_size_;
395-
size_t current_day_index_;
396397
};
397398

398399
/**
@@ -962,23 +963,117 @@ inline std::filesystem::path RotatingFileSink::get_rotated_filename(size_t index
962963
inline DailyFileSink::DailyFileSink(const std::filesystem::path& base_path, const RotationConfig& config,
963964
TimestampFormatter::Format timestamp_format, std::string&& name)
964965
: FileSink(base_path, timestamp_format, std::move(name)), config_(config), base_path_(base_path),
965-
current_file_size_(0), current_day_index_(0) {
966+
current_file_size_(0) {
966967
current_date_ = get_date_string();
967-
// Initialize file size if file already exists
968+
969+
// Check if file already exists and is from a previous day
968970
if (std::filesystem::exists(base_path_)) {
969-
current_file_size_ = std::filesystem::file_size(base_path_);
971+
auto last_write_time = std::filesystem::last_write_time(base_path_);
972+
auto last_write_time_t = std::chrono::system_clock::to_time_t(
973+
std::chrono::time_point_cast<std::chrono::system_clock::duration>(
974+
last_write_time - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()
975+
)
976+
);
977+
std::tm* tm_ptr = std::localtime(&last_write_time_t);
978+
if (tm_ptr) {
979+
char date_str[11];
980+
std::strftime(date_str, sizeof(date_str), "%Y-%m-%d", tm_ptr);
981+
std::string file_date = std::string(date_str);
982+
983+
// If the existing file is from a previous day, rotate it
984+
if (file_date != current_date_) {
985+
file_stream_.close();
986+
987+
// Check if dated file already exists, if so rotate all files for that date
988+
std::filesystem::path dated_file = get_dated_filename(file_date);
989+
if (std::filesystem::exists(dated_file)) {
990+
rotate_files_for_date(file_date);
991+
}
992+
993+
// Now rename current base file to dated filename
994+
std::error_code ec;
995+
std::filesystem::rename(base_path_, dated_file, ec);
996+
if (ec) {
997+
// If rename fails, try copy and remove
998+
std::filesystem::copy_file(base_path_, dated_file, ec);
999+
if (!ec) {
1000+
std::filesystem::remove(base_path_, ec);
1001+
}
1002+
}
1003+
1004+
// Reopen file for today
1005+
file_stream_.open(base_path_, std::ios::out | std::ios::trunc);
1006+
if (!file_stream_) {
1007+
throw std::runtime_error("Failed to reopen daily log file: " + base_path_.string());
1008+
}
1009+
current_file_size_ = 0;
1010+
} else {
1011+
// File is from today, continue appending
1012+
current_file_size_ = std::filesystem::file_size(base_path_);
1013+
}
1014+
} else {
1015+
// Could not get file date, just continue with current file
1016+
current_file_size_ = std::filesystem::file_size(base_path_);
1017+
}
9701018
}
9711019
// Keep logging to base_path (e.g., daily.log) - FileSink constructor already opened it
9721020
}
9731021

9741022
inline DailyFileSink::DailyFileSink(const std::filesystem::path& base_path, const RotationConfig& config,
9751023
const std::string& custom_timestamp_format, std::string&& name)
9761024
: FileSink(base_path, custom_timestamp_format, std::move(name)), config_(config), base_path_(base_path),
977-
current_file_size_(0), current_day_index_(0) {
1025+
current_file_size_(0) {
9781026
current_date_ = get_date_string();
979-
// Initialize file size if file already exists
1027+
1028+
// Check if file already exists and is from a previous day
9801029
if (std::filesystem::exists(base_path_)) {
981-
current_file_size_ = std::filesystem::file_size(base_path_);
1030+
auto last_write_time = std::filesystem::last_write_time(base_path_);
1031+
auto last_write_time_t = std::chrono::system_clock::to_time_t(
1032+
std::chrono::time_point_cast<std::chrono::system_clock::duration>(
1033+
last_write_time - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()
1034+
)
1035+
);
1036+
std::tm* tm_ptr = std::localtime(&last_write_time_t);
1037+
if (tm_ptr) {
1038+
char date_str[11];
1039+
std::strftime(date_str, sizeof(date_str), "%Y-%m-%d", tm_ptr);
1040+
std::string file_date = std::string(date_str);
1041+
1042+
// If the existing file is from a previous day, rotate it
1043+
if (file_date != current_date_) {
1044+
file_stream_.close();
1045+
1046+
// Check if dated file already exists, if so rotate all files for that date
1047+
std::filesystem::path dated_file = get_dated_filename(file_date);
1048+
if (std::filesystem::exists(dated_file)) {
1049+
rotate_files_for_date(file_date);
1050+
}
1051+
1052+
// Now rename current base file to dated filename
1053+
std::error_code ec;
1054+
std::filesystem::rename(base_path_, dated_file, ec);
1055+
if (ec) {
1056+
// If rename fails, try copy and remove
1057+
std::filesystem::copy_file(base_path_, dated_file, ec);
1058+
if (!ec) {
1059+
std::filesystem::remove(base_path_, ec);
1060+
}
1061+
}
1062+
1063+
// Reopen file for today
1064+
file_stream_.open(base_path_, std::ios::out | std::ios::trunc);
1065+
if (!file_stream_) {
1066+
throw std::runtime_error("Failed to reopen daily log file: " + base_path_.string());
1067+
}
1068+
current_file_size_ = 0;
1069+
} else {
1070+
// File is from today, continue appending
1071+
current_file_size_ = std::filesystem::file_size(base_path_);
1072+
}
1073+
} else {
1074+
// Could not get file date, just continue with current file
1075+
current_file_size_ = std::filesystem::file_size(base_path_);
1076+
}
9821077
}
9831078
// Keep logging to base_path (e.g., daily.log) - FileSink constructor already opened it
9841079
}
@@ -1015,9 +1110,6 @@ inline void DailyFileSink::check_rotation() {
10151110
}
10161111
}
10171112

1018-
// Reset index for new day
1019-
current_day_index_ = 0;
1020-
10211113
// Reopen base file for new day's logs
10221114
file_stream_.open(base_path_, std::ios::out | std::ios::trunc); // Start fresh for new day
10231115
if (!file_stream_) {
@@ -1029,36 +1121,81 @@ inline void DailyFileSink::check_rotation() {
10291121
}
10301122

10311123
// Check for size-based rotation
1032-
if (current_file_size_ >= config_.max_file_size) {
1033-
// Close current file
1034-
file_stream_.close();
1124+
if (config_.max_file_size && current_file_size_ >= config_.max_file_size) {
1125+
rotate_daily_files();
1126+
}
1127+
}
10351128

1036-
// Rename current base file to dated indexed filename (e.g., daily.log -> daily_2025-08-24_001.log)
1037-
std::filesystem::path old_dated_file = get_dated_indexed_filename(current_date_, current_day_index_);
1038-
std::error_code ec;
1039-
if (std::filesystem::exists(base_path_)) {
1040-
std::filesystem::rename(base_path_, old_dated_file, ec);
1041-
if (ec) {
1042-
// If rename fails, try copy and remove
1043-
std::filesystem::copy_file(base_path_, old_dated_file, ec);
1044-
if (!ec) {
1045-
std::filesystem::remove(base_path_, ec);
1046-
}
1129+
inline void DailyFileSink::rotate_files_for_date(const std::string& date) {
1130+
// Remove the oldest file if it exists and max_files is configured
1131+
// Files are: daily_DATE.log (index 0), daily_DATE_001.log (index 1), ..., daily_DATE_<max_files-1>.log (index max_files-1)
1132+
1133+
auto dated_file = get_dated_filename(date);
1134+
1135+
if (config_.max_files == 1) {
1136+
// If max_files is 1, we only keep the dated file (no indexed files)
1137+
// Just remove the old dated file - it will be replaced by the new one
1138+
if (std::filesystem::exists(dated_file)) {
1139+
std::filesystem::remove(dated_file);
1140+
}
1141+
} else if (config_.max_files > 1) {
1142+
// Remove the oldest indexed file
1143+
auto oldest_file = get_dated_indexed_filename(date, config_.max_files - 1);
1144+
if (std::filesystem::exists(oldest_file)) {
1145+
std::filesystem::remove(oldest_file);
1146+
}
1147+
1148+
// Rotate existing indexed files for the given date
1149+
// Start from the highest index and work down to 2
1150+
for (size_t i = config_.max_files - 1; i >= 2; --i) {
1151+
auto src = get_dated_indexed_filename(date, i - 1);
1152+
auto dst = get_dated_indexed_filename(date, i);
1153+
1154+
if (std::filesystem::exists(src)) {
1155+
std::filesystem::rename(src, dst);
10471156
}
10481157
}
10491158

1050-
// Increment index for next file
1051-
++current_day_index_;
1159+
// Rotate the dated file (without index) to _001.log
1160+
if (std::filesystem::exists(dated_file)) {
1161+
auto indexed_file_1 = get_dated_indexed_filename(date, 1);
1162+
std::filesystem::rename(dated_file, indexed_file_1);
1163+
}
1164+
}
1165+
}
10521166

1053-
// Reopen base file for new logs
1054-
file_stream_.open(base_path_, std::ios::out | std::ios::trunc);
1055-
if (!file_stream_) {
1056-
throw std::runtime_error("Failed to reopen daily log file: " + base_path_.string());
1167+
inline void DailyFileSink::rotate_daily_files() {
1168+
file_stream_.close();
1169+
1170+
// Rotate all existing files for the current date (if any)
1171+
std::filesystem::path dated_file = get_dated_filename(current_date_);
1172+
1173+
// Always rotate if dated file exists, which will shift:
1174+
// daily_2025-10-02.log -> _001.log
1175+
// _001.log -> _002.log, etc.
1176+
if (std::filesystem::exists(dated_file)) {
1177+
rotate_files_for_date(current_date_);
1178+
}
1179+
1180+
// Rename current base file to dated filename (e.g., daily.log -> daily_2025-10-02.log)
1181+
std::error_code ec;
1182+
if (std::filesystem::exists(base_path_)) {
1183+
std::filesystem::rename(base_path_, dated_file, ec);
1184+
if (ec) {
1185+
// If rename fails, try copy and remove
1186+
std::filesystem::copy_file(base_path_, dated_file, ec);
1187+
if (!ec) {
1188+
std::filesystem::remove(base_path_, ec);
1189+
}
10571190
}
1191+
}
10581192

1059-
current_file_size_ = 0;
1060-
return; // Don't check date rotation if we just did size rotation
1193+
// Create new current file
1194+
file_stream_.open(base_path_, std::ios::out | std::ios::trunc);
1195+
if (!file_stream_) {
1196+
throw std::runtime_error("Failed to reopen daily log file: " + base_path_.string());
10611197
}
1198+
current_file_size_ = 0;
10621199
}
10631200

10641201
inline std::filesystem::path DailyFileSink::get_daily_filename() {

0 commit comments

Comments
 (0)