Logger System 0.1.3
High-performance C++20 thread-safe logging system with asynchronous capabilities
Loading...
Searching...
No Matches
rotating_file_writer.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
7#include <filesystem>
8#include <algorithm>
9#include <regex>
10#include <ctime>
11#include <iostream>
12
13namespace kcenon::logger {
14
16 size_t max_size,
17 size_t max_files,
18 size_t check_interval)
19 : file_writer(filename, true)
20 , rotation_type_(rotation_type::size)
21 , max_size_(max_size)
22 , max_files_(max_files)
23 , check_interval_(check_interval)
24 , last_rotation_time_(std::chrono::system_clock::now())
25 , current_period_start_(std::chrono::system_clock::now()) {
26
27 // Extract base filename and extension
28 std::filesystem::path path(filename);
29 base_filename_ = path.stem().string();
30 file_extension_ = path.extension().string();
31 if (file_extension_.empty()) {
32 file_extension_ = ".log";
33 }
34}
35
37 rotation_type type,
38 size_t max_files,
39 size_t check_interval)
40 : file_writer(filename, true)
41 , rotation_type_(type)
42 , max_size_(0)
43 , max_files_(max_files)
44 , check_interval_(check_interval)
45 , last_rotation_time_(std::chrono::system_clock::now())
46 , current_period_start_(std::chrono::system_clock::now()) {
47
48 // Extract base filename and extension
49 std::filesystem::path path(filename);
50 base_filename_ = path.stem().string();
51 file_extension_ = path.extension().string();
52 if (file_extension_.empty()) {
53 file_extension_ = ".log";
54 }
55}
56
58 rotation_type type,
59 size_t max_size,
60 size_t max_files,
61 size_t check_interval)
62 : file_writer(filename, true)
63 , rotation_type_(type)
64 , max_size_(max_size)
65 , max_files_(max_files)
66 , check_interval_(check_interval)
67 , last_rotation_time_(std::chrono::system_clock::now())
68 , current_period_start_(std::chrono::system_clock::now()) {
69
70 if (type != rotation_type::size_and_time) {
71 throw std::invalid_argument("This constructor is only for size_and_time rotation");
72 }
73
74 // Extract base filename and extension
75 std::filesystem::path path(filename);
76 base_filename_ = path.stem().string();
77 file_extension_ = path.extension().string();
78 if (file_extension_.empty()) {
79 file_extension_ = ".log";
80 }
81}
82
84 std::lock_guard<std::mutex> lock(get_mutex());
85
86 // Check precondition
87 if (!file_stream_.is_open()) {
88 return make_logger_void_result(logger_error_code::file_write_failed, "File stream is not open");
89 }
90
91 // Format and write - preserves all structured fields
92 std::string formatted = format_entry(entry);
93 file_stream_ << formatted << '\n';
94 bytes_written_.fetch_add(formatted.size() + 1);
95
96 // Verify stream state
97 if (file_stream_.fail()) {
99 }
100
101 // Periodic rotation check optimization (Phase 2)
103
105 if (should_rotate()) {
107 }
109 }
110
111 return common::ok();
112}
113
115 // Public API for manual rotation
116 std::lock_guard<std::mutex> lock(get_mutex());
118}
119
121 switch (rotation_type_) {
123 return get_file_size() >= max_size_;
124
127 return should_rotate_by_time();
128
131
132 default:
133 return false;
134 }
135}
136
138 // IMPORTANT: Caller must hold the mutex before calling this method
139 // This ensures thread safety for all file operations and mutable state modifications
140
141 // Close current file
142 if (file_stream_.is_open()) {
143 file_stream_.flush();
144 file_stream_.close();
145 }
146
147 // Generate new filename for the current log
148 std::string rotated_name = generate_rotated_filename();
149
150 // Rename current file (with error handling)
151 auto rename_result = utils::try_write_operation([&]() -> common::VoidResult {
152 if (std::filesystem::exists(filename_)) {
153 std::filesystem::rename(filename_, rotated_name);
154 }
155 return common::ok();
157
158 if (rename_result.is_err()) {
159 std::cerr << "Failed to rotate log file: " << rename_result.error().message << std::endl;
160 }
161
162 // Clean up old files
164
165 // Open new file (with error handling)
166 auto open_result = utils::try_open_operation([&]() -> common::VoidResult {
167 std::filesystem::path file_path(filename_);
168 std::filesystem::path dir = file_path.parent_path();
169
170 auto dir_result = utils::ensure_directory_exists(dir);
171 if (dir_result.is_err()) return dir_result;
172
173 auto mode = append_mode_ ? std::ios::app : std::ios::trunc;
174 file_stream_.open(filename_, std::ios::out | mode);
175
176 if (file_stream_.is_open()) {
177 bytes_written_ = 0;
178 }
179
180 return common::ok();
181 });
182
183 if (open_result.is_err()) {
184 std::cerr << "Failed to open new log file: " << open_result.error().message << std::endl;
185 }
186
187 // Update rotation time - protected by mutex
188 last_rotation_time_ = std::chrono::system_clock::now();
190
191 // Reset write counter after rotation
193}
194
196 std::ostringstream oss;
197 std::filesystem::path dir = std::filesystem::path(filename_).parent_path();
198
199 if (!dir.empty()) {
200 oss << dir.string() << "/";
201 }
202
203 // Build: base + extension + separator + timestamp/index
204 // Example: "test.log.1" instead of "test.1.log"
206
207 // Add timestamp or index
208 auto now = std::chrono::system_clock::now();
209 auto time_t = std::chrono::system_clock::to_time_t(now);
210
211 // Use thread-safe time conversion
212 std::tm tm_buf{};
213#ifdef _WIN32
214 localtime_s(&tm_buf, &time_t); // Windows thread-safe version
215#else
216 localtime_r(&time_t, &tm_buf); // POSIX thread-safe version
217#endif
218
219 switch (rotation_type_) {
221 if (index >= 0) {
222 oss << "." << index;
223 } else {
224 // Find next available index
225 std::string base_with_ext = oss.str();
226 int next_index = 1;
227 while (std::filesystem::exists(base_with_ext + "." + std::to_string(next_index))) {
228 next_index++;
229 }
230 oss << "." << next_index;
231 }
232 break;
233
235 oss << "." << std::put_time(&tm_buf, "%Y%m%d");
236 break;
237
239 oss << "." << std::put_time(&tm_buf, "%Y%m%d_%H");
240 break;
241
243 oss << "." << std::put_time(&tm_buf, "%Y%m%d_%H%M%S");
244 break;
245 }
246
247 return oss.str();
248}
249
251 auto backup_files = get_backup_files();
252
253 if (backup_files.size() > max_files_) {
254 // Sort by modification time (oldest first)
255 std::sort(backup_files.begin(), backup_files.end(),
256 [](const std::string& a, const std::string& b) {
257 return std::filesystem::last_write_time(a) <
258 std::filesystem::last_write_time(b);
259 });
260
261 // Remove oldest files
262 size_t files_to_remove = backup_files.size() - max_files_;
263 for (size_t i = 0; i < files_to_remove; ++i) {
264 auto remove_result = utils::try_write_operation([&]() -> common::VoidResult {
265 std::filesystem::remove(backup_files[i]);
266 return common::ok();
268
269 if (remove_result.is_err()) {
270 std::cerr << "Failed to remove old log file: " << remove_result.error().message << std::endl;
271 }
272 }
273 }
274}
275
276std::vector<std::string> rotating_file_writer::get_backup_files() const {
277 std::vector<std::string> files;
278 std::filesystem::path dir = std::filesystem::path(filename_).parent_path();
279 if (dir.empty()) {
280 dir = ".";
281 }
282
283 // Create regex pattern for backup files
284 // Pattern: base_filename + file_extension + "." + (number or timestamp)
285 // Example: "test_rotating.log.1" or "test.log.20250108"
286 std::string escaped_ext = std::regex_replace(file_extension_, std::regex(R"(\.)"), R"(\\.)");
287 std::string pattern = base_filename_ + escaped_ext + R"(\.(\d+|\d{8}|\d{8}_\d{2}|\d{8}_\d{6}))";
288 std::regex backup_regex(pattern);
289
290 // Use error handling utility for directory iteration
292 for (const auto& entry : std::filesystem::directory_iterator(dir)) {
293 if (entry.is_regular_file()) {
294 std::string filename = entry.path().filename().string();
295 if (std::regex_match(filename, backup_regex)) {
296 files.push_back(entry.path().string());
297 }
298 }
299 }
300 return common::ok();
302
303 if (result.is_err()) {
304 std::cerr << "Error listing backup files: " << result.error().message << std::endl;
305 }
306
307 return files;
308}
309
311 auto now = std::chrono::system_clock::now();
312
313 switch (rotation_type_) {
316 // Check if we're in a new day
317 auto now_time_t = std::chrono::system_clock::to_time_t(now);
318 auto start_time_t = std::chrono::system_clock::to_time_t(current_period_start_);
319
320 // Use thread-safe time conversion
321 std::tm now_tm{};
322 std::tm start_tm{};
323#ifdef _WIN32
324 localtime_s(&now_tm, &now_time_t);
325 localtime_s(&start_tm, &start_time_t);
326#else
327 localtime_r(&now_time_t, &now_tm);
328 localtime_r(&start_time_t, &start_tm);
329#endif
330
331 return now_tm.tm_year != start_tm.tm_year ||
332 now_tm.tm_mon != start_tm.tm_mon ||
333 now_tm.tm_mday != start_tm.tm_mday;
334 }
335
337 // Check if we're in a new hour
338 auto duration = now - current_period_start_;
339 return duration >= std::chrono::hours(1);
340 }
341
342 default:
343 return false;
344 }
345}
346
348 // IMPORTANT: This method should only be called while holding the mutex
349 // to avoid race conditions with concurrent writes and file rotation.
350 //
351 // Race condition example:
352 // - Thread A: Calls should_rotate() -> get_file_size() reads filesystem
353 // - Thread B: Simultaneously writing, bytes_written_ is updating
354 // - Result: get_file_size() returns stale data, rotation check is incorrect
355 //
356 // Better approach: Always use bytes_written_.load() which is atomic and
357 // thread-safe. Only call filesystem functions during actual rotation when
358 // the caller already holds the mutex.
359
360 if (!file_stream_.is_open()) {
361 return 0;
362 }
363
364 // Prefer atomic counter for thread safety
365 // Only fall back to filesystem if absolutely necessary
366 std::size_t atomic_size = bytes_written_.load(std::memory_order_relaxed);
367
368 // Optional: Validate with filesystem size for debugging
369 // (Remove in production for performance)
370 #ifdef DEBUG_FILE_SIZE_VALIDATION
371 std::error_code ec;
372 auto fs_size = std::filesystem::file_size(filename_, ec);
373 if (!ec && fs_size != atomic_size) {
374 // Log discrepancy for debugging
375 std::cerr << "File size mismatch: atomic=" << atomic_size
376 << " filesystem=" << fs_size << std::endl;
377 }
378 #endif
379
380 return atomic_size;
381}
382
383} // namespace kcenon::logger
Core file writer for logging to files.
Definition file_writer.h:44
std::atomic< size_t > bytes_written_
std::string format_entry(const log_entry &entry) const
Format a log entry using the current formatter.
std::mutex & get_mutex() const
Access the writer mutex for extended operations.
std::chrono::system_clock::time_point current_period_start_
void rotate()
Manually trigger log rotation (thread-safe)
bool should_rotate_by_time() const
Check if time-based rotation should occur.
bool should_rotate() const
Check if rotation should occur.
rotating_file_writer(const std::string &filename, size_t max_size, size_t max_files, size_t check_interval=100)
Construct with size-based rotation.
size_t writes_since_check_
Counter for writes since last check.
std::vector< std::string > get_backup_files() const
Get list of existing backup files.
void perform_rotation()
Perform the actual rotation operation.
void cleanup_old_files()
Remove old backup files beyond max_files limit.
std::chrono::system_clock::time_point last_rotation_time_
common::VoidResult write(const log_entry &entry) override
Write operation with automatic rotation check.
std::string generate_rotated_filename(int index=-1) const
Generate filename for rotated log.
size_t check_interval_
Number of writes between rotation checks.
std::size_t get_file_size() const
Get current file size from filesystem.
Structured error context for debugging log system failures.
VoidResult ok()
common::VoidResult try_open_operation(F &&operation)
Error handling helper for file open operations.
common::VoidResult try_write_operation(F &&operation, logger_error_code default_error_code=logger_error_code::file_write_failed)
Error handling helper for write operations.
common::VoidResult ensure_directory_exists(const std::filesystem::path &dir)
Directory creation helper.
common::VoidResult make_logger_void_result(logger_error_code code, const std::string &message="")
rotation_type
Determines when log rotation should occur.
@ hourly
Rotate every hour.
@ size_and_time
Rotate based on both size and time.
@ daily
Rotate daily at midnight.
@ size
Rotate based on file size only.
Rotating file writer with size and time-based rotation.
Represents a single log entry with all associated metadata.
Definition log_entry.h:155