Logger System 1.0.0
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
8#include <filesystem>
9#include <algorithm>
10#include <regex>
11#include <ctime>
12#include <iostream>
13
14namespace kcenon::logger {
15
17 size_t max_size,
18 size_t max_files,
19 size_t check_interval)
20 : file_writer(filename, true)
21 , rotation_type_(rotation_type::size)
22 , max_size_(max_size)
23 , max_files_(max_files)
24 , check_interval_(check_interval)
25 , last_rotation_time_(std::chrono::system_clock::now())
26 , current_period_start_(std::chrono::system_clock::now()) {
27
28 // Extract base filename and extension
29 std::filesystem::path path(filename);
30 base_filename_ = path.stem().string();
31 file_extension_ = path.extension().string();
32 if (file_extension_.empty()) {
33 file_extension_ = ".log";
34 }
35}
36
38 rotation_type type,
39 size_t max_files,
40 size_t check_interval)
41 : file_writer(filename, true)
42 , rotation_type_(type)
43 , max_size_(0)
44 , max_files_(max_files)
45 , check_interval_(check_interval)
46 , last_rotation_time_(std::chrono::system_clock::now())
47 , current_period_start_(std::chrono::system_clock::now()) {
48
49 // Extract base filename and extension
50 std::filesystem::path path(filename);
51 base_filename_ = path.stem().string();
52 file_extension_ = path.extension().string();
53 if (file_extension_.empty()) {
54 file_extension_ = ".log";
55 }
56}
57
59 rotation_type type,
60 size_t max_size,
61 size_t max_files,
62 size_t check_interval)
63 : file_writer(filename, true)
64 , rotation_type_(type)
65 , max_size_(max_size)
66 , max_files_(max_files)
67 , check_interval_(check_interval)
68 , last_rotation_time_(std::chrono::system_clock::now())
69 , current_period_start_(std::chrono::system_clock::now()) {
70
71 if (type != rotation_type::size_and_time) {
72 throw std::invalid_argument("This constructor is only for size_and_time rotation");
73 }
74
75 // Extract base filename and extension
76 std::filesystem::path path(filename);
77 base_filename_ = path.stem().string();
78 file_extension_ = path.extension().string();
79 if (file_extension_.empty()) {
80 file_extension_ = ".log";
81 }
82}
83
85 std::lock_guard<std::mutex> lock(get_mutex());
86
87 // Check precondition
88 if (!file_stream_.is_open()) {
89 return make_logger_void_result(logger_error_code::file_write_failed, "File stream is not open");
90 }
91
92 // Format and write - preserves all structured fields
93 std::string formatted = format_entry(entry);
94
95 // Apply integrity signature (Issue #612) if the parent file_writer
96 // was configured with a policy. We reach into the parent's protected
97 // integrity_policy_ to keep the behavior consistent across size-based
98 // and time-based rotation paths.
100 formatted.append(
102 }
103
104 file_stream_ << formatted << '\n';
105 bytes_written_.fetch_add(formatted.size() + 1);
106
107 // Verify stream state
108 if (file_stream_.fail()) {
110 }
111
112 // Periodic rotation check optimization (Phase 2)
114
116 if (should_rotate()) {
118 }
120 }
121
122 return common::ok();
123}
124
126 // Public API for manual rotation
127 std::lock_guard<std::mutex> lock(get_mutex());
129}
130
132 switch (rotation_type_) {
134 return get_file_size() >= max_size_;
135
138 return should_rotate_by_time();
139
142
143 default:
144 return false;
145 }
146}
147
149 // IMPORTANT: Caller must hold the mutex before calling this method
150 // This ensures thread safety for all file operations and mutable state modifications
151
152 // Close current file
153 if (file_stream_.is_open()) {
154 file_stream_.flush();
155 file_stream_.close();
156 }
157
158 // Generate new filename for the current log
159 std::string rotated_name = generate_rotated_filename();
160
161 // Rename current file (with error handling)
162 auto rename_result = utils::try_write_operation([&]() -> common::VoidResult {
163 if (std::filesystem::exists(filename_)) {
164 std::filesystem::rename(filename_, rotated_name);
165 }
166 return common::ok();
168
169 if (rename_result.is_err()) {
170 std::cerr << "Failed to rotate log file: " << rename_result.error().message << std::endl;
171 }
172
173 // Clean up old files
175
176 // Open new file (with error handling)
177 auto open_result = utils::try_open_operation([&]() -> common::VoidResult {
178 std::filesystem::path file_path(filename_);
179 std::filesystem::path dir = file_path.parent_path();
180
181 auto dir_result = utils::ensure_directory_exists(dir);
182 if (dir_result.is_err()) return dir_result;
183
184 auto mode = append_mode_ ? std::ios::app : std::ios::trunc;
185 file_stream_.open(filename_, std::ios::out | mode);
186
187 if (file_stream_.is_open()) {
188 bytes_written_ = 0;
189 }
190
191 return common::ok();
192 });
193
194 if (open_result.is_err()) {
195 std::cerr << "Failed to open new log file: " << open_result.error().message << std::endl;
196 }
197
198 // Update rotation time - protected by mutex
199 last_rotation_time_ = std::chrono::system_clock::now();
201
202 // Reset write counter after rotation
204}
205
207 std::ostringstream oss;
208 std::filesystem::path dir = std::filesystem::path(filename_).parent_path();
209
210 if (!dir.empty()) {
211 oss << dir.string() << "/";
212 }
213
214 // Build: base + extension + separator + timestamp/index
215 // Example: "test.log.1" instead of "test.1.log"
217
218 // Add timestamp or index
219 auto now = std::chrono::system_clock::now();
220 auto time_t = std::chrono::system_clock::to_time_t(now);
221
222 // Use thread-safe time conversion
223 std::tm tm_buf{};
224#ifdef _WIN32
225 localtime_s(&tm_buf, &time_t); // Windows thread-safe version
226#else
227 localtime_r(&time_t, &tm_buf); // POSIX thread-safe version
228#endif
229
230 switch (rotation_type_) {
232 if (index >= 0) {
233 oss << "." << index;
234 } else {
235 // Find next available index
236 std::string base_with_ext = oss.str();
237 int next_index = 1;
238 while (std::filesystem::exists(base_with_ext + "." + std::to_string(next_index))) {
239 next_index++;
240 }
241 oss << "." << next_index;
242 }
243 break;
244
246 oss << "." << std::put_time(&tm_buf, "%Y%m%d");
247 break;
248
250 oss << "." << std::put_time(&tm_buf, "%Y%m%d_%H");
251 break;
252
254 oss << "." << std::put_time(&tm_buf, "%Y%m%d_%H%M%S");
255 break;
256 }
257
258 return oss.str();
259}
260
262 auto backup_files = get_backup_files();
263
264 if (backup_files.size() > max_files_) {
265 // Sort by modification time (oldest first)
266 std::sort(backup_files.begin(), backup_files.end(),
267 [](const std::string& a, const std::string& b) {
268 return std::filesystem::last_write_time(a) <
269 std::filesystem::last_write_time(b);
270 });
271
272 // Remove oldest files
273 size_t files_to_remove = backup_files.size() - max_files_;
274 for (size_t i = 0; i < files_to_remove; ++i) {
275 auto remove_result = utils::try_write_operation([&]() -> common::VoidResult {
276 std::filesystem::remove(backup_files[i]);
277 return common::ok();
279
280 if (remove_result.is_err()) {
281 std::cerr << "Failed to remove old log file: " << remove_result.error().message << std::endl;
282 }
283 }
284 }
285}
286
287std::vector<std::string> rotating_file_writer::get_backup_files() const {
288 std::vector<std::string> files;
289 std::filesystem::path dir = std::filesystem::path(filename_).parent_path();
290 if (dir.empty()) {
291 dir = ".";
292 }
293
294 // Create regex pattern for backup files
295 // Pattern: base_filename + file_extension + "." + (number or timestamp)
296 // Example: "test_rotating.log.1" or "test.log.20250108"
297 std::string escaped_ext = std::regex_replace(file_extension_, std::regex(R"(\.)"), R"(\\.)");
298 std::string pattern = base_filename_ + escaped_ext + R"(\.(\d+|\d{8}|\d{8}_\d{2}|\d{8}_\d{6}))";
299 std::regex backup_regex(pattern);
300
301 // Use error handling utility for directory iteration
303 for (const auto& entry : std::filesystem::directory_iterator(dir)) {
304 if (entry.is_regular_file()) {
305 std::string filename = entry.path().filename().string();
306 if (std::regex_match(filename, backup_regex)) {
307 files.push_back(entry.path().string());
308 }
309 }
310 }
311 return common::ok();
313
314 if (result.is_err()) {
315 std::cerr << "Error listing backup files: " << result.error().message << std::endl;
316 }
317
318 return files;
319}
320
322 auto now = std::chrono::system_clock::now();
323
324 switch (rotation_type_) {
327 // Check if we're in a new day
328 auto now_time_t = std::chrono::system_clock::to_time_t(now);
329 auto start_time_t = std::chrono::system_clock::to_time_t(current_period_start_);
330
331 // Use thread-safe time conversion
332 std::tm now_tm{};
333 std::tm start_tm{};
334#ifdef _WIN32
335 localtime_s(&now_tm, &now_time_t);
336 localtime_s(&start_tm, &start_time_t);
337#else
338 localtime_r(&now_time_t, &now_tm);
339 localtime_r(&start_time_t, &start_tm);
340#endif
341
342 return now_tm.tm_year != start_tm.tm_year ||
343 now_tm.tm_mon != start_tm.tm_mon ||
344 now_tm.tm_mday != start_tm.tm_mday;
345 }
346
348 // Check if we're in a new hour
349 auto duration = now - current_period_start_;
350 return duration >= std::chrono::hours(1);
351 }
352
353 default:
354 return false;
355 }
356}
357
359 // IMPORTANT: This method should only be called while holding the mutex
360 // to avoid race conditions with concurrent writes and file rotation.
361 //
362 // Race condition example:
363 // - Thread A: Calls should_rotate() -> get_file_size() reads filesystem
364 // - Thread B: Simultaneously writing, bytes_written_ is updating
365 // - Result: get_file_size() returns stale data, rotation check is incorrect
366 //
367 // Better approach: Always use bytes_written_.load() which is atomic and
368 // thread-safe. Only call filesystem functions during actual rotation when
369 // the caller already holds the mutex.
370
371 if (!file_stream_.is_open()) {
372 return 0;
373 }
374
375 // Prefer atomic counter for thread safety
376 // Only fall back to filesystem if absolutely necessary
377 std::size_t atomic_size = bytes_written_.load(std::memory_order_relaxed);
378
379 // Optional: Validate with filesystem size for debugging
380 // (Remove in production for performance)
381 #ifdef DEBUG_FILE_SIZE_VALIDATION
382 std::error_code ec;
383 auto fs_size = std::filesystem::file_size(filename_, ec);
384 if (!ec && fs_size != atomic_size) {
385 // Log discrepancy for debugging
386 std::cerr << "File size mismatch: atomic=" << atomic_size
387 << " filesystem=" << fs_size << std::endl;
388 }
389 #endif
390
391 return atomic_size;
392}
393
394} // namespace kcenon::logger
Core file writer for logging to files.
Definition file_writer.h:46
std::shared_ptr< security::integrity_policy > integrity_policy_
Integrity policy shared with derived writers (e.g. rotating_file_writer).
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.
Tamper-evident log signing policies for writers.
VoidResult ok()
std::string format_signature_suffix(const integrity_policy &policy, const std::string &record)
Format a signature line suitable for appending to a text log record.
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