LogIt++
Header-only, macro-first C++ logging library
Loading...
Searching...
No Matches
FileLogger.hpp
Go to the documentation of this file.
1#pragma once
2#ifndef LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_LOGGERS_FILELOGGER_HPP_INCLUDED
3#define LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_LOGGERS_FILELOGGER_HPP_INCLUDED
4
7
8#include "ILogger.hpp"
9#include <iostream>
10#include <fstream>
11#include <mutex>
12#include <atomic>
13#include <queue>
14#include <functional>
15#include <utility>
16#include <vector>
17#include <algorithm>
18#include <cstdlib>
19#include <cstdio>
20#include <memory>
21#include <sstream>
22#include <iomanip>
23#include <regex>
24#include <time_shield/text/time_parser.hpp>
25
26namespace logit {
27
28#if defined(__EMSCRIPTEN__)
29
30 class FileLogger : public ILogger {
31 public:
32 struct Config {
33 std::string directory = "logs";
34 bool async = false;
35 int auto_delete_days = 30;
36 uint64_t max_file_size_bytes = 0;
37 uint32_t max_rotated_files = 0;
39 int compress_level = 1;
40 bool compress_async = true;
41 std::string external_cmd;
43 uint32_t seq_width = 3;
44 bool use_dedicated_executor = false;
45 std::size_t queue_capacity = 0;
46 QueuePolicy queue_policy = QueuePolicy::Block;
47 };
48
49 FileLogger() { warn(); }
50 FileLogger(const Config&) { warn(); }
51 FileLogger(const std::string&, const bool& = true, const int& = 30,
52 const uint64_t& = 0, const uint32_t& = 0) { warn(); }
53 FileLogger(const std::string&, const bool&, const int&, bool, std::size_t,
54 QueuePolicy) { warn(); }
55 FileLogger(const std::string&, const bool&, const int&, uint64_t, uint32_t,
56 bool, std::size_t, QueuePolicy) { warn(); }
57
58 void log(const LogRecord&, const std::string&) override { warn(); }
59 std::string get_string_param(const LoggerParam&) const override { return {}; }
60 int64_t get_int_param(const LoggerParam&) const override { return 0; }
61 double get_float_param(const LoggerParam&) const override { return 0.0; }
62 void set_log_level(LogLevel) override {}
63 LogLevel get_log_level() const override { return LogLevel::LOG_LVL_TRACE; }
64 void wait() override {}
65
66 private:
67 void warn() const {
68 static bool warned = false;
69 if (!warned) {
70 warned = true;
71 std::cerr << "FileLogger is not supported under Emscripten" << std::endl;
72 }
73 }
74 std::atomic<int> m_log_level = ATOMIC_VAR_INIT(static_cast<int>(LogLevel::LOG_LVL_TRACE));
75 };
76
77#else
78
90 class FileLogger : public ILogger {
91 public:
92
95 struct Config {
96 std::string directory = "logs";
97 bool async = true;
99 uint64_t max_file_size_bytes = 0;
100 uint32_t max_rotated_files = 0;
103 bool compress_async = true;
104 std::string external_cmd;
106 uint32_t seq_width = 3;
108 std::size_t queue_capacity = 0;
109 QueuePolicy queue_policy = QueuePolicy::Block;
110 };
111
115 }
116
119 FileLogger(const Config& config) : m_config(config) {
121 m_executor.reset(new detail::SingleThreadExecutor());
122 m_executor->set_max_queue_size(m_config.queue_capacity);
123 m_executor->set_queue_policy(m_config.queue_policy);
124 }
126 }
127
133 const std::string& directory,
134 const bool& async = true,
135 const int& auto_delete_days = 30) {
136 m_config.directory = directory;
137 m_config.async = async;
138 m_config.auto_delete_days = auto_delete_days;
140 }
141
144 const std::string& directory,
145 const bool& async,
146 const int& auto_delete_days,
147 bool use_dedicated_executor,
148 std::size_t queue_capacity,
149 QueuePolicy queue_policy)
151 directory,
152 async,
153 auto_delete_days,
154 0,
155 0,
156 use_dedicated_executor,
157 queue_capacity,
158 queue_policy)) {}
159
162 const std::string& directory,
163 const bool& async,
164 const int& auto_delete_days,
165 uint64_t max_file_size_bytes,
166 uint32_t max_rotated_files) {
167 m_config.directory = directory;
168 m_config.async = async;
169 m_config.auto_delete_days = auto_delete_days;
170 m_config.max_file_size_bytes = max_file_size_bytes;
171 m_config.max_rotated_files = max_rotated_files;
173 }
174
177 const std::string& directory,
178 const bool& async,
179 const int& auto_delete_days,
180 uint64_t max_file_size_bytes,
181 uint32_t max_rotated_files,
182 bool use_dedicated_executor,
183 std::size_t queue_capacity,
184 QueuePolicy queue_policy)
186 directory,
187 async,
188 auto_delete_days,
189 max_file_size_bytes,
190 max_rotated_files,
191 use_dedicated_executor,
192 queue_capacity,
193 queue_policy)) {}
194
196 virtual ~FileLogger() {
197 shutdown();
198 stop_logging();
199 if (m_compressor) m_compressor->wait();
200 }
201
208 void set_config(const Config& config) {
209 std::unique_ptr<detail::SingleThreadExecutor> old_executor;
210 {
211 std::lock_guard<std::mutex> lifecycle_lock(m_lifecycle_mutex);
212 if (m_shutdown.load(std::memory_order_acquire)) return;
213
214 if (m_config.async) {
215 if (m_executor) {
216 m_executor->wait();
217 } else {
219 }
220 {
221 std::lock_guard<std::mutex> lock(m_mutex);
222 if (m_file.is_open()) m_file.flush();
223 }
224 }
225
226 {
227 std::lock_guard<std::mutex> lock(m_mutex);
228 m_config.async = config.async;
229 m_config.use_dedicated_executor = config.use_dedicated_executor;
230 m_config.queue_capacity = config.queue_capacity;
231 m_config.queue_policy = config.queue_policy;
233 if (!m_executor) {
235 }
236 m_executor->set_max_queue_size(m_config.queue_capacity);
237 m_executor->set_queue_policy(m_config.queue_policy);
238 } else if (m_executor) {
239 old_executor = std::move(m_executor);
240 m_executor.reset();
241 }
242 }
243 }
244 if (old_executor) {
245 old_executor->shutdown();
246 }
247 }
248
257 bool set_queue_config(std::size_t queue_capacity,
258 QueuePolicy queue_policy) {
259 std::lock_guard<std::mutex> lifecycle_lock(m_lifecycle_mutex);
260
261 if (m_shutdown.load(std::memory_order_acquire)) {
262 return false;
263 }
264
265 std::lock_guard<std::mutex> lock(m_mutex);
266
267 m_config.queue_capacity = queue_capacity;
268 m_config.queue_policy = queue_policy;
269
270 if (!m_executor) {
271 return false;
272 }
273
274 m_executor->set_max_queue_size(queue_capacity);
275 m_executor->set_queue_policy(queue_policy);
276 return true;
277 }
278
286 void log(const LogRecord& record, const std::string& message) override {
287 std::lock_guard<std::mutex> lifecycle_lock(m_lifecycle_mutex);
288 if (m_shutdown.load(std::memory_order_acquire)) return;
291 if (!m_config.async) {
292 std::lock_guard<std::mutex> lock(m_mutex);
293 try {
294 write_log(message, record.timestamp_ms);
295 } catch (const std::exception& e) {
296 std::cerr << "Log error: " << e.what() << std::endl;
297 }
298 return;
299 }
300 auto timestamp_ms = record.timestamp_ms;
301 if (m_executor) {
302 m_executor->add_task([this, message, timestamp_ms]() {
303 std::lock_guard<std::mutex> lock(m_mutex);
304 try {
305 write_log(message, timestamp_ms);
306 } catch (const std::exception& e) {
307 std::cerr << "Log async log error: " << e.what() << std::endl;
308 }
309 });
310 } else {
311 detail::TaskExecutor::get_instance().add_task([this, message, timestamp_ms]() {
312 std::lock_guard<std::mutex> lock(m_mutex);
313 try {
314 write_log(message, timestamp_ms);
315 } catch (const std::exception& e) {
316 std::cerr << "Log async log error: " << e.what() << std::endl;
317 }
318 });
319 }
320 }
321
325 std::string get_string_param(const LoggerParam& param) const override {
326 switch (param) {
329 case LoggerParam::LastLogTimestamp: return std::to_string(get_last_log_ts());
330 case LoggerParam::TimeSinceLastLog: return std::to_string(get_time_since_last_log());
331 default:
332 break;
333 };
334 return std::string();
335 }
336
340 int64_t get_int_param(const LoggerParam& param) const override {
341 switch (param) {
344 default:
345 break;
346 };
347 return 0;
348 }
349
353 double get_float_param(const LoggerParam& param) const override {
354 switch (param) {
356 return static_cast<double>(get_last_log_ts()) / 1000.0;
358 return static_cast<double>(get_time_since_last_log()) / 1000.0;
359 default:
360 break;
361 };
362 return 0.0;
363 }
364
366 void set_log_level(LogLevel level) override {
367 m_log_level = static_cast<int>(level);
368 }
369
371 LogLevel get_log_level() const override {
372 return static_cast<LogLevel>(m_log_level.load());
373 }
374
377 std::vector<LogFileInfo> list_log_files() const override {
378 const std::string directory = get_directory_path();
379 const std::string current_file_path = get_last_log_file_path();
380 std::vector<LogFileInfo> files;
381
382# if __cplusplus >= 201703L
383# if defined(_WIN32)
384 fs::path dir_path = fs::u8path(directory);
385# else
386 fs::path dir_path(directory);
387# endif
388 if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
389 return files;
390 }
391
392 for (const auto& entry : fs::directory_iterator(dir_path)) {
393 if (!fs::is_regular_file(entry.status())) continue;
394# if defined(_WIN32)
395 const std::string file_path = entry.path().u8string();
396# else
397 const std::string file_path = entry.path().string();
398# endif
399 LogFileInfo info;
400 if (try_make_log_file_info(file_path, current_file_path, info)) {
401 files.push_back(info);
402 }
403 }
404# else
405 const std::vector<std::string> file_list = get_list_files(directory);
406 for (size_t i = 0; i < file_list.size(); ++i) {
407 LogFileInfo info;
408 if (try_make_log_file_info(file_list[i], current_file_path, info)) {
409 files.push_back(info);
410 }
411 }
412# endif
413
415 return files;
416 }
417
423 LogFileReadResult read_log_file(const std::string& path) const override {
424 const std::vector<LogFileInfo> files = list_log_files();
425 for (size_t i = 0; i < files.size(); ++i) {
426 if (same_path(files[i].path, path)) {
427 return read_log_file_from_info(files[i]);
428 }
429 }
430
431 LogFileReadResult result;
432 result.file.path = path;
433 result.file.name = get_file_name(path);
434 result.ok = false;
435 return result;
436 }
437
441 std::vector<LogFileReadResult> read_log_files(const std::vector<std::string>& paths) const override {
442 const std::vector<LogFileInfo> files = list_log_files();
443 std::vector<LogFileReadResult> results;
444 results.reserve(paths.size());
445
446 for (size_t i = 0; i < paths.size(); ++i) {
447 bool found = false;
448 for (size_t j = 0; j < files.size(); ++j) {
449 if (same_path(files[j].path, paths[i])) {
450 results.push_back(read_log_file_from_info(files[j]));
451 found = true;
452 break;
453 }
454 }
455
456 if (!found) {
457 LogFileReadResult result;
458 result.file.path = paths[i];
459 result.file.name = get_file_name(paths[i]);
460 result.ok = false;
461 results.push_back(result);
462 }
463 }
464
465 return results;
466 }
467
470 (void)options;
471 wait();
472
473 std::lock_guard<std::mutex> lifecycle_lock(m_lifecycle_mutex);
474 LogClearResult result;
475 if (m_shutdown.load(std::memory_order_acquire)) {
476 result.ok = false;
478 result.message = "FileLogger is shut down";
479 return result;
480 }
481
482 try {
483 std::lock_guard<std::mutex> lock(m_mutex);
484 const std::vector<LogFileInfo> files = list_log_files();
485 if (m_file.is_open()) {
486 m_file.flush();
487 m_file.close();
488 }
489 for (size_t i = 0; i < files.size(); ++i) {
490 if (remove_file_path(files[i].path)) {
491 ++result.cleared_records;
492 }
493 }
495 m_last_log_ts.store(0, std::memory_order_release);
496 m_last_log_mono_ts.store(0, std::memory_order_release);
498 result.ok = true;
500 result.message = "cleared";
501 } catch (const std::exception& e) {
502 result.ok = false;
504 result.message = std::string("FileLogger clear error: ") + e.what();
505 } catch (...) {
506 result.ok = false;
508 result.message = "FileLogger clear error";
509 }
510 return result;
511 }
512
514 void wait() override {
515 std::lock_guard<std::mutex> lifecycle_lock(m_lifecycle_mutex);
516 if (!m_config.async) return;
517 if (m_executor) {
518 m_executor->wait();
519 } else {
521 }
522 std::lock_guard<std::mutex> lock(m_mutex);
523 if (m_file.is_open()) m_file.flush();
524 }
525
527 void shutdown() override {
528 {
529 std::lock_guard<std::mutex> lifecycle_lock(m_lifecycle_mutex);
530 if (m_shutdown.exchange(true, std::memory_order_acq_rel)) return;
531 }
532 if (m_executor) {
533 m_executor->shutdown();
534 std::lock_guard<std::mutex> lock(m_mutex);
535 if (m_file.is_open()) m_file.flush();
536 } else if (m_config.async) {
537 wait();
538 }
539 }
540
541 private:
542 mutable std::mutex m_mutex;
543 std::mutex m_lifecycle_mutex;
545 mutable std::ofstream m_file;
546 mutable std::mutex m_file_path_mutex;
547 std::string m_file_path;
548 std::string m_file_name;
549 int64_t m_current_date_ts = 0;
550 uint64_t m_current_file_size = 0;
551 std::unique_ptr<detail::CompressionWorker> m_compressor;
552 std::unique_ptr<detail::SingleThreadExecutor> m_executor;
553 std::atomic<int64_t> m_last_log_ts = ATOMIC_VAR_INIT(0);
554 std::atomic<int64_t> m_last_log_mono_ts = ATOMIC_VAR_INIT(0);
555 std::atomic<int> m_log_level = ATOMIC_VAR_INIT(static_cast<int>(LogLevel::LOG_LVL_TRACE));
556 std::atomic<bool> m_shutdown = ATOMIC_VAR_INIT(false);
557
559 const std::string& directory,
560 bool async,
561 int auto_delete_days,
562 uint64_t max_file_size_bytes,
563 uint32_t max_rotated_files,
564 bool use_dedicated_executor,
565 std::size_t queue_capacity,
566 QueuePolicy queue_policy) {
567 Config config;
568 config.directory = directory;
569 config.async = async;
570 config.auto_delete_days = auto_delete_days;
571 config.max_file_size_bytes = max_file_size_bytes;
572 config.max_rotated_files = max_rotated_files;
573 config.use_dedicated_executor = use_dedicated_executor;
574 config.queue_capacity = queue_capacity;
575 config.queue_policy = queue_policy;
576 return config;
577 }
578
581 // I/O streams (e.g., std::cin, std::cout, std::cerr) may be closed before the program exits.
582 // In this case, calls to functions that use I/O streams (for example, the std::regex constructor)
583 // can lead to undesirable behavior such as hangs or segmentation faults.
584 is_valid_log_filename("2024-01-01.log");
585 std::lock_guard<std::mutex> lock(m_mutex);
586 try {
590 } catch (const std::exception& e) {
591 std::cerr << "Initialization error: " << e.what() << std::endl;
592 }
593 }
594
597 wait();
598 std::lock_guard<std::mutex> lock(m_mutex);
599 if (m_file.is_open()) {
600 m_file.close();
601 }
602 }
603
608
611 std::string get_directory_path() const {
612# if defined(_WIN32)
613 return get_exec_dir() + "\\" + m_config.directory;
614# else
615 return get_exec_dir() + "/" + m_config.directory;
616# endif
617 }
618
621 void open_log_file(const int64_t& date_ts) {
622 if (m_file.is_open()) {
623 m_file.close();
624 }
625 m_current_date_ts = date_ts;
626 std::unique_lock<std::mutex> lock(m_file_path_mutex);
627 m_file_path = create_file_path(date_ts);
629 lock.unlock();
630# if defined(_WIN32)
631 m_file.open(utf8_to_ansi(m_file_path), std::ios_base::app);
632# else
633 m_file.open(m_file_path, std::ios_base::app);
634# endif
635 if (!m_file.is_open()) {
636 throw std::runtime_error("Failed to open log file: " + m_file_path);
637 }
638 m_file.seekp(0, std::ios::end);
639 m_current_file_size = static_cast<uint64_t>(m_file.tellp());
640 }
641
645 std::string create_file_path(int64_t date_ts) const {
646 std::string date_str = time_shield::to_iso8601_date(date_ts);
647 return get_directory_path() + "/" + date_str + ".log";
648 }
649
651 const std::string& file_path,
652 const std::string& current_file_path,
653 LogFileInfo& info) const {
654 if (!is_direct_child_path(file_path, get_directory_path())) {
655 return false;
656 }
657
658 const std::string filename = get_file_name(file_path);
659 if (!is_valid_log_filename(filename)) {
660 return false;
661 }
662
663 info.is_current = same_path(file_path, current_file_path);
664 info.path = info.is_current ? current_file_path : file_path;
665 info.name = filename;
666 info.day_start_ms = time_shield::sec_to_ms(get_date_ts_from_filename(filename));
668 return true;
669 }
670
672 LogFileReadResult result;
673 result.file = info;
674
675 if (info.is_compressed) {
676 result.ok = false;
677 return result;
678 }
679
680 if (info.is_current) {
681 std::lock_guard<std::mutex> lock(m_mutex);
682 if (m_file.is_open()) {
683 m_file.flush();
684 }
685 }
686
687 result.ok = read_plain_file(info.path, result.content);
688 return result;
689 }
690
691 bool read_plain_file(const std::string& file_path, std::string& out) const {
692# if defined(_WIN32)
693 std::ifstream in(utf8_to_ansi(file_path).c_str(), std::ios_base::binary);
694# else
695 std::ifstream in(file_path.c_str(), std::ios_base::binary);
696# endif
697 if (!in.is_open()) {
698 out.clear();
699 return false;
700 }
701
702 std::ostringstream stream;
703 stream << in.rdbuf();
704 out = stream.str();
705 return !in.bad();
706 }
707
708 bool remove_file_path(const std::string& file_path) const {
709# if defined(_WIN32)
710 return std::remove(utf8_to_ansi(file_path).c_str()) == 0;
711# else
712 return std::remove(file_path.c_str()) == 0;
713# endif
714 }
715
716 void sort_log_files_newest_first(std::vector<LogFileInfo>& files) const {
717 std::sort(files.begin(), files.end(), [this](const LogFileInfo& lhs, const LogFileInfo& rhs) {
718 if (lhs.day_start_ms != rhs.day_start_ms) {
719 return lhs.day_start_ms > rhs.day_start_ms;
720 }
721 if (lhs.is_current != rhs.is_current) {
722 return lhs.is_current && !rhs.is_current;
723 }
724 const FileSortKey lhs_key = make_file_sort_key(lhs);
725 const FileSortKey rhs_key = make_file_sort_key(rhs);
726 if (lhs_key.kind_rank != rhs_key.kind_rank) {
727 return lhs_key.kind_rank > rhs_key.kind_rank;
728 }
729 if (lhs_key.primary != rhs_key.primary) {
730 return lhs_key.primary > rhs_key.primary;
731 }
732 if (lhs_key.secondary != rhs_key.secondary) {
733 return lhs_key.secondary > rhs_key.secondary;
734 }
735 if (lhs_key.artifact_name == rhs_key.artifact_name &&
736 lhs.is_compressed != rhs.is_compressed) {
737 return !lhs.is_compressed && rhs.is_compressed;
738 }
739 if (lhs_key.artifact_name != rhs_key.artifact_name) {
740 return lhs_key.artifact_name > rhs_key.artifact_name;
741 }
743 });
744 }
745
746 struct FileSortKey {
747 int kind_rank = 0;
748 long long primary = 0;
749 long long secondary = 0;
750 std::string artifact_name;
751 };
752
754 FileSortKey key;
755 key.artifact_name = strip_compression_suffix_impl(info.name);
756 const std::string& plain_name = key.artifact_name;
757
758 if (plain_name.size() > 15 && plain_name[10] == '_') {
759 key.kind_rank = 2;
760 const std::string rest = plain_name.substr(11, plain_name.size() - 15);
761 const size_t dot = rest.find('.');
762 const std::string time_part = dot == std::string::npos ? rest : rest.substr(0, dot);
763 key.primary = std::strtoll(time_part.c_str(), nullptr, 10);
764 if (dot != std::string::npos) {
765 key.secondary = std::strtoll(rest.substr(dot + 1).c_str(), nullptr, 10);
766 }
767 return key;
768 }
769
770 if (plain_name.size() > 15 && plain_name[10] == '.') {
771 key.kind_rank = 1;
772 key.primary = std::strtoll(
773 plain_name.substr(11, plain_name.size() - 15).c_str(),
774 nullptr,
775 10);
776 }
777 return key;
778 }
779
780 bool is_direct_child_path(const std::string& file_path, const std::string& directory_path) const {
781# if __cplusplus >= 201703L
782# if defined(_WIN32)
783 return fs::u8path(file_path).parent_path().lexically_normal() ==
784 fs::u8path(directory_path).lexically_normal();
785# else
786 return fs::path(file_path).parent_path().lexically_normal() ==
787 fs::path(directory_path).lexically_normal();
788# endif
789# else
790 const size_t pos = file_path.find_last_of("/\\");
791 if (pos == std::string::npos) return false;
792 std::string parent = file_path.substr(0, pos);
793 return parent == directory_path;
794# endif
795 }
796
797 bool same_path(const std::string& lhs, const std::string& rhs) const {
798# if __cplusplus >= 201703L
799# if defined(_WIN32)
800 return fs::u8path(lhs).lexically_normal() == fs::u8path(rhs).lexically_normal();
801# else
802 return fs::path(lhs).lexically_normal() == fs::path(rhs).lexically_normal();
803# endif
804# else
805 return normalize_path_separators(lhs) == normalize_path_separators(rhs);
806# endif
807 }
808
809 std::string normalize_path_separators(const std::string& path) const {
810 std::string normalized = path;
811 for (size_t i = 0; i < normalized.size(); ++i) {
812 if (normalized[i] == '\\') normalized[i] = '/';
813 }
814 return normalized;
815 }
816
817 bool is_compressed_log_filename(const std::string& filename) const {
818 return ends_with(filename, ".gz") || ends_with(filename, ".zst");
819 }
820
821 std::string strip_compression_suffix(const std::string& filename) const {
822 return strip_compression_suffix_impl(filename);
823 }
824
825 static std::string strip_compression_suffix_impl(const std::string& filename) {
826 if (ends_with_impl(filename, ".gz")) {
827 return filename.substr(0, filename.size() - 3);
828 }
829 if (ends_with_impl(filename, ".zst")) {
830 return filename.substr(0, filename.size() - 4);
831 }
832 return filename;
833 }
834
835 bool ends_with(const std::string& value, const std::string& suffix) const {
836 return ends_with_impl(value, suffix);
837 }
838
839 static bool ends_with_impl(const std::string& value, const std::string& suffix) {
840 return value.size() >= suffix.size() &&
841 value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0;
842 }
843
847 void write_log(const std::string& message, const int64_t& timestamp_ms) {
848 const int64_t message_date_ts = time_shield::start_of_day(time_shield::ms_to_sec(timestamp_ms));
849 if (message_date_ts != m_current_date_ts) {
850 open_log_file(message_date_ts);
851 }
852 if (m_config.max_file_size_bytes > 0) {
853 const uint64_t add = static_cast<uint64_t>(message.size() + 1);
854 if (m_current_file_size + add > m_config.max_file_size_bytes) {
855 rotate_current_file();
856 }
857 }
858 if (m_file.is_open()) {
859 m_file << message << '\n';
860 m_current_file_size += static_cast<uint64_t>(message.size() + 1);
861 }
862 remove_old_logs();
863 }
864
866 if (m_file.is_open()) m_file.close();
867
868 const std::string base = time_shield::to_iso8601_date(m_current_date_ts);
869 const std::string dir = get_directory_path();
870 std::string rotated_str;
871# if __cplusplus >= 201703L
872# if defined(_WIN32)
873 fs::path cur = (fs::u8path(dir) / (base + ".log")).lexically_normal();
874 fs::path rotated = fs::u8path(make_rotated_name(base, dir)).lexically_normal();
875# else
876 fs::path cur = (fs::path(dir) / (base + ".log")).lexically_normal();
877 fs::path rotated = fs::path(make_rotated_name(base, dir)).lexically_normal();
878# endif
879 std::error_code ec;
880 fs::rename(cur, rotated, ec);
881 if (ec) {
882 throw std::runtime_error("Failed to rename log file: " + ec.message());
883 }
884# if defined(_WIN32)
885 rotated_str = rotated.u8string();
886# else
887 rotated_str = rotated.string();
888# endif
889# else
890# if defined(_WIN32)
891 const std::string cur = dir + "\\" + base + ".log";
892# else
893 const std::string cur = dir + "/" + base + ".log";
894# endif
895 rotated_str = make_rotated_name(base, dir);
896# if defined(_WIN32)
897 if (std::rename(utf8_to_ansi(cur).c_str(), utf8_to_ansi(rotated_str).c_str()) != 0) {
898# else
899 if (std::rename(cur.c_str(), rotated_str.c_str()) != 0) {
900# endif
901 throw std::runtime_error("Failed to rename log file");
902 }
903# endif
904
905 open_log_file(m_current_date_ts);
906 m_current_file_size = 0;
907
908 if (m_config.compress != CompressType::NONE) {
909 if (m_config.compress_async) {
910 if (!m_compressor) {
911 m_compressor.reset(new detail::CompressionWorker(
912 m_config.compress, m_config.compress_level, m_config.external_cmd));
913 }
914 m_compressor->enqueue(rotated_str);
915 } else {
916 detail::compress_file(m_config.compress, rotated_str, m_config.compress_level, m_config.external_cmd);
917 }
918 }
919
920 if (m_config.max_rotated_files > 0) {
921 enforce_rotation_retention(base, m_config.max_rotated_files, dir);
922 }
923 }
924
925 void enforce_rotation_retention(const std::string& base, uint32_t max_files, const std::string& dir) {
926# if __cplusplus >= 201703L
927 std::vector<fs::path> files;
928 for (const auto& entry : fs::directory_iterator(dir)) {
929 if (!fs::is_regular_file(entry.status())) continue;
930 std::string name = entry.path().filename().string();
931 if (name.rfind(base, 0) == 0 && name != base + ".log") {
932 files.emplace_back(entry.path());
933 }
934 }
935 if (files.size() <= max_files) return;
936 // Extract timestamp and optional index for chronological sorting
937 auto extract = [&base](const std::string& name) {
938 int64_t ts = 0;
939 int idx = 0;
940 std::string rest = name.substr(base.size());
941 if (!rest.empty() && (rest[0] == '_' || rest[0] == '.')) {
942 rest = rest.substr(1);
943 size_t dot = rest.find('.');
944 ts = std::strtoll(rest.substr(0, dot).c_str(), nullptr, 10);
945 if (dot != std::string::npos) {
946 size_t dot2 = rest.find('.', dot + 1);
947 if (dot2 != std::string::npos) {
948 idx = std::atoi(rest.substr(dot + 1, dot2 - dot - 1).c_str());
949 }
950 }
951 }
952 return std::make_pair(ts, idx);
953 };
954 std::sort(files.begin(), files.end(), [&](const fs::path& a, const fs::path& b) {
955 auto pa = extract(a.filename().string());
956 auto pb = extract(b.filename().string());
957 return (pa.first == pb.first) ? (pa.second < pb.second) : (pa.first < pb.first);
958 });
959 size_t to_remove = files.size() - max_files;
960 for (size_t i = 0; i < to_remove; ++i) {
961 fs::remove(files[i]);
962 }
963# else
964 std::vector<std::string> files;
965 std::vector<std::string> file_list = get_list_files(dir);
966 for (const auto& path : file_list) {
967 std::string name = path.substr(path.find_last_of("/\\") + 1);
968 if (name.rfind(base, 0) == 0 && name != base + ".log") {
969 files.emplace_back(path);
970 }
971 }
972 if (files.size() <= max_files) return;
973 // Extract timestamp and optional index for chronological sorting
974 auto extract = [&base](const std::string& name) {
975 int64_t ts = 0;
976 int idx = 0;
977 std::string rest = name.substr(base.size());
978 if (!rest.empty() && (rest[0] == '_' || rest[0] == '.')) {
979 rest = rest.substr(1);
980 size_t dot = rest.find('.');
981 ts = std::strtoll(rest.substr(0, dot).c_str(), nullptr, 10);
982 if (dot != std::string::npos) {
983 size_t dot2 = rest.find('.', dot + 1);
984 if (dot2 != std::string::npos) {
985 idx = std::atoi(rest.substr(dot + 1, dot2 - dot - 1).c_str());
986 }
987 }
988 }
989 return std::make_pair(ts, idx);
990 };
991 std::sort(files.begin(), files.end(), [&](const std::string& a, const std::string& b) {
992 auto pa = extract(a.substr(a.find_last_of("/\\") + 1));
993 auto pb = extract(b.substr(b.find_last_of("/\\") + 1));
994 return (pa.first == pb.first) ? (pa.second < pb.second) : (pa.first < pb.first);
995 });
996 size_t to_remove = files.size() - max_files;
997 for (size_t i = 0; i < to_remove; ++i) {
998# if defined(_WIN32)
999 remove(utf8_to_ansi(files[i]).c_str());
1000# else
1001 remove(files[i].c_str());
1002# endif
1003 }
1004# endif
1005 }
1006
1007 std::string make_rotated_name(const std::string& base, const std::string& dir) const {
1008 switch (m_config.naming) {
1010 return make_sequence_name(base, dir);
1013 return make_timestamp_name(base, dir);
1014 }
1015 return make_sequence_name(base, dir);
1016 }
1017
1018 std::string make_sequence_name(const std::string& base, const std::string& dir) const {
1019 uint32_t idx = 1;
1020 std::string rotated;
1021# if __cplusplus >= 201703L
1022 for (;; ++idx) {
1023 std::ostringstream oss;
1024 oss << dir << "/" << base << '.' << std::setw(m_config.seq_width)
1025 << std::setfill('0') << idx << ".log";
1026 rotated = oss.str();
1027 if (!fs::exists(rotated)) break;
1028 }
1029# else
1030 auto file_exists = [](const std::string& path) {
1031# if defined(_WIN32)
1032 std::ifstream f(utf8_to_ansi(path).c_str());
1033# else
1034 std::ifstream f(path.c_str());
1035# endif
1036 return f.good();
1037 };
1038 for (;; ++idx) {
1039 std::ostringstream oss;
1040 oss << dir << "/" << base << '.' << std::setw(m_config.seq_width)
1041 << std::setfill('0') << idx << ".log";
1042 rotated = oss.str();
1043 if (!file_exists(rotated)) break;
1044 }
1045# endif
1046 return rotated;
1047 }
1048
1049 std::string make_timestamp_name(const std::string& base, const std::string& dir) const {
1050 int64_t ts_ms = LOGIT_CURRENT_TIMESTAMP_MS();
1051 time_t sec = static_cast<time_t>(time_shield::ms_to_sec(ts_ms));
1052 std::tm tm{};
1053# if defined(_WIN32)
1054 gmtime_s(&tm, &sec);
1055# else
1056 gmtime_r(&sec, &tm);
1057# endif
1058 char buf[32];
1059 std::strftime(buf, sizeof(buf), "%H%M%S", &tm);
1060 std::string timepart(buf);
1061 if (m_config.naming == RotationNaming::TimestampMs) {
1062 char msbuf[4];
1063 std::snprintf(msbuf, sizeof(msbuf), "%03d", static_cast<int>(ts_ms % 1000));
1064 timepart += msbuf;
1065 }
1066 std::string rotated = dir + "/" + base + "_" + timepart + ".log";
1067# if __cplusplus >= 201703L
1068 if (!fs::exists(rotated)) return rotated;
1069 uint32_t idx = 1;
1070 for (;; ++idx) {
1071 std::string candidate = rotated.substr(0, rotated.size() - 4) + "." + std::to_string(idx) + ".log";
1072 if (!fs::exists(candidate)) return candidate;
1073 }
1074# else
1075 auto file_exists = [](const std::string& path) {
1076# if defined(_WIN32)
1077 std::ifstream f(utf8_to_ansi(path).c_str());
1078# else
1079 std::ifstream f(path.c_str());
1080# endif
1081 return f.good();
1082 };
1083 if (!file_exists(rotated)) return rotated;
1084 uint32_t idx = 1;
1085 for (;; ++idx) {
1086 std::ostringstream oss;
1087 oss << rotated.substr(0, rotated.size() - 4) << '.' << idx << ".log";
1088 std::string candidate = oss.str();
1089 if (!file_exists(candidate)) return candidate;
1090 }
1091# endif
1092 }
1093
1096 const int64_t threshold_ts = m_current_date_ts - (time_shield::SEC_PER_DAY * m_config.auto_delete_days);
1097# if __cplusplus >= 201703L
1098# ifdef _WIN32
1099 fs::path dir_path = fs::u8path(get_directory_path());
1100# else
1101 fs::path dir_path(get_directory_path());
1102# endif
1103
1104 if (!fs::exists(dir_path) ||
1105 !fs::is_directory(dir_path)) {
1106 return;
1107 }
1108
1109 for (const auto& entry : fs::directory_iterator(dir_path)) {
1110 if (!fs::is_regular_file(entry.status())) continue;
1111 std::string filename = entry.path().filename().string();
1112 if (is_valid_log_filename(filename)) {
1113 const int64_t file_date_ts = get_date_ts_from_filename(filename);
1114 if (file_date_ts < threshold_ts) {
1115 fs::remove(entry.path());
1116 }
1117 }
1118 }
1119# else
1120 std::vector<std::string> file_list = get_list_files(get_directory_path());
1121 for (const auto& file_path : file_list) {
1122 // Extract the file name
1123 std::string filename = file_path.substr(file_path.find_last_of("/\\") + 1);
1124 if (is_valid_log_filename(filename)) {
1125 const int64_t file_date_ts = get_date_ts_from_filename(filename);
1126 if (file_date_ts < threshold_ts) {
1127# if defined(_WIN32)
1128 remove(utf8_to_ansi(file_path).c_str());
1129# else
1130 remove(file_path.c_str());
1131# endif
1132 }
1133 }
1134 }
1135# endif
1136 }
1137
1141 bool is_valid_log_filename(const std::string& filename) const {
1142 const std::string plain_name = strip_compression_suffix(filename);
1143 static const std::regex current_pattern(R"(^\d{4}-\d{2}-\d{2}\.log$)");
1144 static const std::regex sequence_pattern(R"(^\d{4}-\d{2}-\d{2}\.\d+\.log$)");
1145 static const std::regex timestamp_pattern(R"(^\d{4}-\d{2}-\d{2}_\d{6}(\d{3})?(\.\d+)?\.log$)");
1146 return std::regex_match(plain_name, current_pattern) ||
1147 std::regex_match(plain_name, sequence_pattern) ||
1148 std::regex_match(plain_name, timestamp_pattern);
1149 }
1150
1154 int64_t get_date_ts_from_filename(const std::string& filename) const {
1155 return time_shield::ts(filename.substr(0, 10));
1156 }
1157
1160 int64_t get_current_utc_date_ts() const {
1161 return time_shield::start_of_day(time_shield::ms_to_sec(current_timestamp_ms()));
1162 }
1163
1166 int64_t current_timestamp_ms() const {
1168 }
1169
1172 std::string get_last_log_file_path() const {
1173 std::lock_guard<std::mutex> lock(m_file_path_mutex);
1174 return m_file_path;
1175 }
1176
1179 std::string get_last_log_file_name() const {
1180 std::lock_guard<std::mutex> lock(m_file_path_mutex);
1181 return m_file_name;
1182 }
1183
1186 int64_t get_last_log_ts() const {
1187 return m_last_log_ts;
1188 }
1189
1192 int64_t get_time_since_last_log() const {
1193 return LOGIT_MONOTONIC_MS() - m_last_log_mono_ts;
1194 }
1195 }; // FileLogger
1196
1197#endif // defined(__EMSCRIPTEN__)
1198
1199}; // namespace logit
1200
1201#endif // LOGIT_CPP_HEADER_LOGIT_CPP_LOGIT_LOGGERS_FILELOGGER_HPP_INCLUDED
Defines the interface for loggers used in the logging system.
Logs messages to files with date-based rotation and automatic deletion of old logs.
std::mutex m_mutex
Mutex to protect file operations.
int64_t m_current_date_ts
Timestamp of the current log file's date.
static Config make_config(const std::string &directory, bool async, int auto_delete_days, uint64_t max_file_size_bytes, uint32_t max_rotated_files, bool use_dedicated_executor, std::size_t queue_capacity, QueuePolicy queue_policy)
void stop_logging()
Stops the logging process by closing the file and waiting for tasks.
void initialize_directory()
Initializes the logging directory.
std::string make_rotated_name(const std::string &base, const std::string &dir) const
std::string get_last_log_file_name() const
Retrieves the last log file name.
static FileSortKey make_file_sort_key(const LogFileInfo &info)
void remove_old_logs()
Removes old log files based on the auto-delete days configuration.
std::unique_ptr< detail::CompressionWorker > m_compressor
Background compressor.
int64_t get_date_ts_from_filename(const std::string &filename) const
Extracts the date timestamp from the log filename.
void set_config(const Config &config)
Updates executor-related configuration at runtime.
static bool ends_with_impl(const std::string &value, const std::string &suffix)
FileLogger(const Config &config)
Constructor with custom configuration.
int64_t current_timestamp_ms() const
Gets the current timestamp in milliseconds.
Config m_config
Configuration for the file logger.
int64_t get_time_since_last_log() const
Retrieves the time since the last log.
std::string get_last_log_file_path() const
Retrieves the last log file path.
bool try_make_log_file_info(const std::string &file_path, const std::string &current_file_path, LogFileInfo &info) const
FileLogger(const std::string &directory, const bool &async=true, const int &auto_delete_days=30)
Constructor with directory and asynchronous flag.
std::mutex m_file_path_mutex
Mutex to protect file path operations.
std::string normalize_path_separators(const std::string &path) const
uint64_t m_current_file_size
Current size of the log file.
std::string make_sequence_name(const std::string &base, const std::string &dir) const
void set_log_level(LogLevel level) override
Sets the minimal log level for this logger.
bool ends_with(const std::string &value, const std::string &suffix) const
bool is_direct_child_path(const std::string &file_path, const std::string &directory_path) const
int64_t get_int_param(const LoggerParam &param) const override
Retrieves an integer parameter from the logger.
void sort_log_files_newest_first(std::vector< LogFileInfo > &files) const
LogClearResult clear_logs(const LogClearOptions &options=LogClearOptions()) override
Clears managed log files and reopens the current log file.
std::ofstream m_file
Output file stream for logging.
std::atomic< int64_t > m_last_log_ts
Timestamp of the last log.
std::vector< LogFileInfo > list_log_files() const override
Lists persisted log files owned by this backend.
std::string strip_compression_suffix(const std::string &filename) const
std::unique_ptr< detail::SingleThreadExecutor > m_executor
Dedicated executor (null = use global).
virtual ~FileLogger()
Destructor to stop logging and close file.
void start_logging()
Starts the logging process by initializing the file and directory.
bool is_valid_log_filename(const std::string &filename) const
Checks if the filename matches the log file naming pattern.
std::atomic< int64_t > m_last_log_mono_ts
Timestamp of the last log.
bool read_plain_file(const std::string &file_path, std::string &out) const
static std::string strip_compression_suffix_impl(const std::string &filename)
std::string get_string_param(const LoggerParam &param) const override
Retrieves a string parameter from the logger.
int64_t get_last_log_ts() const
Retrieves the timestamp of the last log.
FileLogger(const std::string &directory, const bool &async, const int &auto_delete_days, uint64_t max_file_size_bytes, uint32_t max_rotated_files, bool use_dedicated_executor, std::size_t queue_capacity, QueuePolicy queue_policy)
Constructor with directory, rotation, and dedicated executor options.
std::string m_file_path
Path of the currently open log file.
void log(const LogRecord &record, const std::string &message) override
Logs a message to a file with thread safety.
std::atomic< bool > m_shutdown
FileLogger(const std::string &directory, const bool &async, const int &auto_delete_days, bool use_dedicated_executor, std::size_t queue_capacity, QueuePolicy queue_policy)
Constructor with directory, async flag, and dedicated executor options.
FileLogger()
Default constructor that uses default configuration.
int64_t get_current_utc_date_ts() const
Gets the current UTC date timestamp in seconds.
LogFileReadResult read_log_file(const std::string &path) const override
Reads one persisted log file owned by this backend.
std::string make_timestamp_name(const std::string &base, const std::string &dir) const
void open_log_file(const int64_t &date_ts)
Opens a new log file based on the provided date timestamp.
std::string get_directory_path() const
Gets the full path to the logging directory.
void write_log(const std::string &message, const int64_t &timestamp_ms)
Writes a log message to the file.
void enforce_rotation_retention(const std::string &base, uint32_t max_files, const std::string &dir)
std::vector< LogFileReadResult > read_log_files(const std::vector< std::string > &paths) const override
Reads several persisted log files owned by this backend.
bool remove_file_path(const std::string &file_path) const
void shutdown() override
Stops logger-owned asynchronous resources after draining pending writes.
bool set_queue_config(std::size_t queue_capacity, QueuePolicy queue_policy)
Updates queue_capacity and queue_policy at runtime without draining tasks.
std::string create_file_path(int64_t date_ts) const
Creates a file path for the log file based on the date timestamp.
bool is_compressed_log_filename(const std::string &filename) const
LogLevel get_log_level() const override
Gets the minimal log level for this logger.
std::mutex m_lifecycle_mutex
Serializes direct log() calls with shutdown().
std::atomic< int > m_log_level
void wait() override
Waits for all asynchronous tasks to complete.
std::string m_file_name
Name of the currently open log file.
bool same_path(const std::string &lhs, const std::string &rhs) const
FileLogger(const std::string &directory, const bool &async, const int &auto_delete_days, uint64_t max_file_size_bytes, uint32_t max_rotated_files)
Constructor with directory, size-based rotation and additional options.
double get_float_param(const LoggerParam &param) const override
Retrieves a floating-point parameter from the logger.
LogFileReadResult read_log_file_from_info(const LogFileInfo &info) const
Optional buffered snapshot access returns empty results by default.
Definition ILogger.hpp:47
Background worker performing asynchronous compression.
Per-instance single-thread executor for isolated async logging.
void add_task(std::function< void()> task)
Enqueue a task for asynchronous execution.
void wait()
Block until the queue is empty or a shutdown is requested.
static TaskExecutor & get_instance()
Returns the global executor instance.
#define LOGIT_CURRENT_TIMESTAMP_MS()
Macro to get the current timestamp in milliseconds.
Definition config.hpp:84
#define LOGIT_MONOTONIC_MS()
Definition config.hpp:74
bool compress_file(CompressType type, const std::string &src, int level, const std::string &external_cmd)
Compress a file using the specified compression type.
QueuePolicy
Queue overflow handling policy used by TaskExecutor and SingleThreadExecutor.
The primary namespace for the LogIt++ library.
std::vector< std::string > get_list_files(const std::string &path)
Recursively retrieves a list of all files in a directory.
RotationNaming
Naming policy for rotated log files.
Definition enums.hpp:67
@ Sequence
Append a numeric sequence: YYYY-MM-DD.001.log.
@ TimestampMs
Append HHMMSSmmm timestamp: YYYY-MM-DD_HHMMSSmmm.log.
@ Timestamp
Append HHMMSS timestamp: YYYY-MM-DD_HHMMSS.log.
LogLevel
Logging levels.
Definition enums.hpp:15
@ LOG_LVL_TRACE
Trace level logging.
void create_directories(const std::string &path)
Creates directories recursively for the given path.
LoggerParam
Enumeration for different logger parameters that can be retrieved.
Definition enums.hpp:47
@ TimeSinceLastLog
The time elapsed since the last log in seconds.
@ LastLogTimestamp
The timestamp of the last log.
@ LastFileName
The name of the last file written to.
@ LastFilePath
The full path of the last file written to.
std::string get_file_name(const std::string &file_path)
Extracts the file name from a full file path.
CompressType
Supported compression algorithms for rotated files.
Definition enums.hpp:58
@ NONE
Do not compress rotated files.
detail::QueuePolicy QueuePolicy
Public queue overflow handling policy.
Definition loggers.hpp:20
std::string get_exec_dir()
Retrieves the directory of the executable file.
Configuration for the file logger.
std::size_t queue_capacity
Maximum queue size for the dedicated executor (0 = unlimited).
uint32_t seq_width
Width of sequence index.
uint64_t max_file_size_bytes
Max size for log file before rotation (0 = off).
bool use_dedicated_executor
Use a dedicated executor instead of the global TaskExecutor; native builds create one worker thread p...
uint32_t max_rotated_files
Number of rotated files to keep (0 = unlimited).
QueuePolicy queue_policy
Overflow policy for the dedicated executor.
int auto_delete_days
Number of days after which old log files are deleted.
bool async
Flag indicating whether logging should be asynchronous.
bool compress_async
Run compression in background thread.
std::string external_cmd
External command template.
CompressType compress
Compression algorithm for rotated files.
int compress_level
Compression level.
std::string directory
Directory where log files are stored.
RotationNaming naming
Naming policy for rotated files.
Selects which data categories a logger cleanup should remove.
Definition ILogger.hpp:19
Result returned by logger cleanup operations.
Definition ILogger.hpp:35
std::string message
Optional diagnostic or unsupported reason.
Definition ILogger.hpp:39
std::size_t cleared_records
Number of primary records/files cleared when available.
Definition ILogger.hpp:38
LogClearStatus status
Detailed cleanup status.
Definition ILogger.hpp:37
bool ok
True when the backend completed cleanup.
Definition ILogger.hpp:36
Metadata for a persisted log file owned by a logger backend.
std::string name
File name without the parent directory.
bool is_current
True when this is the file currently used for appends.
int64_t day_start_ms
Start-of-day UTC timestamp derived from the file name.
std::string path
Full path to the file.
bool is_compressed
True when the file is a compressed rotated artifact.
Result of reading a persisted log file from a file-based backend.
LogFileInfo file
Metadata for the requested file.
bool ok
Whether the read succeeded.
std::string content
File contents when the read succeeded.
Stores log metadata and content.
Definition LogRecord.hpp:17
const int64_t timestamp_ms
Timestamp in milliseconds.
Definition LogRecord.hpp:19