1 // Copyright(c) 2015-present, Gabi Melman & spdlog contributors.
2 // Distributed under the MIT License (http://opensource.org/licenses/MIT)
3 
4 #pragma once
5 
6 #include <spdlog/common.h>
7 #include <spdlog/details/file_helper.h>
8 #include <spdlog/details/null_mutex.h>
9 #include <spdlog/fmt/fmt.h>
10 #include <spdlog/sinks/base_sink.h>
11 #include <spdlog/details/os.h>
12 #include <spdlog/details/circular_q.h>
13 #include <spdlog/details/synchronous_factory.h>
14 
15 #include <chrono>
16 #include <cstdio>
17 #include <ctime>
18 #include <mutex>
19 #include <string>
20 
21 namespace spdlog {
22 namespace sinks {
23 
24 /*
25  * Generator of daily log file names in format basename.YYYY-MM-DD.ext
26  */
27 struct daily_filename_calculator
28 {
29     // Create filename for the form basename.YYYY-MM-DD
calc_filenamedaily_filename_calculator30     static filename_t calc_filename(const filename_t &filename, const tm &now_tm)
31     {
32         filename_t basename, ext;
33         std::tie(basename, ext) = details::file_helper::split_by_extension(filename);
34         return fmt::format(
35             SPDLOG_FILENAME_T("{}_{:04d}-{:02d}-{:02d}{}"), basename, now_tm.tm_year + 1900, now_tm.tm_mon + 1, now_tm.tm_mday, ext);
36     }
37 };
38 
39 /*
40  * Rotating file sink based on date.
41  * If truncate != false , the created file will be truncated.
42  * If max_files > 0, retain only the last max_files and delete previous.
43  */
44 template<typename Mutex, typename FileNameCalc = daily_filename_calculator>
45 class daily_file_sink final : public base_sink<Mutex>
46 {
47 public:
48     // create daily file sink which rotates on given time
49     daily_file_sink(filename_t base_filename, int rotation_hour, int rotation_minute, bool truncate = false, uint16_t max_files = 0)
base_filename_(std::move (base_filename))50         : base_filename_(std::move(base_filename))
51         , rotation_h_(rotation_hour)
52         , rotation_m_(rotation_minute)
53         , truncate_(truncate)
54         , max_files_(max_files)
55         , filenames_q_()
56     {
57         if (rotation_hour < 0 || rotation_hour > 23 || rotation_minute < 0 || rotation_minute > 59)
58         {
59             throw_spdlog_ex("daily_file_sink: Invalid rotation time in ctor");
60         }
61 
62         auto now = log_clock::now();
63         auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(now));
64         file_helper_.open(filename, truncate_);
65         rotation_tp_ = next_rotation_tp_();
66 
67         if (max_files_ > 0)
68         {
69             init_filenames_q_();
70         }
71     }
72 
filename()73     filename_t filename()
74     {
75         std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
76         return file_helper_.filename();
77     }
78 
79 protected:
sink_it_(const details::log_msg & msg)80     void sink_it_(const details::log_msg &msg) override
81     {
82         auto time = msg.time;
83         bool should_rotate = time >= rotation_tp_;
84         if (should_rotate)
85         {
86             auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(time));
87             file_helper_.open(filename, truncate_);
88             rotation_tp_ = next_rotation_tp_();
89         }
90         memory_buf_t formatted;
91         base_sink<Mutex>::formatter_->format(msg, formatted);
92         file_helper_.write(formatted);
93 
94         // Do the cleaning only at the end because it might throw on failure.
95         if (should_rotate && max_files_ > 0)
96         {
97             delete_old_();
98         }
99     }
100 
flush_()101     void flush_() override
102     {
103         file_helper_.flush();
104     }
105 
106 private:
init_filenames_q_()107     void init_filenames_q_()
108     {
109         using details::os::path_exists;
110 
111         filenames_q_ = details::circular_q<filename_t>(static_cast<size_t>(max_files_));
112         std::vector<filename_t> filenames;
113         auto now = log_clock::now();
114         while (filenames.size() < max_files_)
115         {
116             auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(now));
117             if (!path_exists(filename))
118             {
119                 break;
120             }
121             filenames.emplace_back(filename);
122             now -= std::chrono::hours(24);
123         }
124         for (auto iter = filenames.rbegin(); iter != filenames.rend(); ++iter)
125         {
126             filenames_q_.push_back(std::move(*iter));
127         }
128     }
129 
now_tm(log_clock::time_point tp)130     tm now_tm(log_clock::time_point tp)
131     {
132         time_t tnow = log_clock::to_time_t(tp);
133         return spdlog::details::os::localtime(tnow);
134     }
135 
next_rotation_tp_()136     log_clock::time_point next_rotation_tp_()
137     {
138         auto now = log_clock::now();
139         tm date = now_tm(now);
140         date.tm_hour = rotation_h_;
141         date.tm_min = rotation_m_;
142         date.tm_sec = 0;
143         auto rotation_time = log_clock::from_time_t(std::mktime(&date));
144         if (rotation_time > now)
145         {
146             return rotation_time;
147         }
148         return {rotation_time + std::chrono::hours(24)};
149     }
150 
151     // Delete the file N rotations ago.
152     // Throw spdlog_ex on failure to delete the old file.
delete_old_()153     void delete_old_()
154     {
155         using details::os::filename_to_str;
156         using details::os::remove_if_exists;
157 
158         filename_t current_file = file_helper_.filename();
159         if (filenames_q_.full())
160         {
161             auto old_filename = std::move(filenames_q_.front());
162             filenames_q_.pop_front();
163             bool ok = remove_if_exists(old_filename) == 0;
164             if (!ok)
165             {
166                 filenames_q_.push_back(std::move(current_file));
167                 throw_spdlog_ex("Failed removing daily file " + filename_to_str(old_filename), errno);
168             }
169         }
170         filenames_q_.push_back(std::move(current_file));
171     }
172 
173     filename_t base_filename_;
174     int rotation_h_;
175     int rotation_m_;
176     log_clock::time_point rotation_tp_;
177     details::file_helper file_helper_;
178     bool truncate_;
179     uint16_t max_files_;
180     details::circular_q<filename_t> filenames_q_;
181 };
182 
183 using daily_file_sink_mt = daily_file_sink<std::mutex>;
184 using daily_file_sink_st = daily_file_sink<details::null_mutex>;
185 
186 } // namespace sinks
187 
188 //
189 // factory functions
190 //
191 template<typename Factory = spdlog::synchronous_factory>
192 inline std::shared_ptr<logger> daily_logger_mt(
193     const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0, bool truncate = false, uint16_t max_files = 0)
194 {
195     return Factory::template create<sinks::daily_file_sink_mt>(logger_name, filename, hour, minute, truncate, max_files);
196 }
197 
198 template<typename Factory = spdlog::synchronous_factory>
199 inline std::shared_ptr<logger> daily_logger_st(
200     const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0, bool truncate = false, uint16_t max_files = 0)
201 {
202     return Factory::template create<sinks::daily_file_sink_st>(logger_name, filename, hour, minute, truncate, max_files);
203 }
204 } // namespace spdlog
205