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/fmt/chrono.h> |
11 | #include <spdlog/sinks/base_sink.h> |
12 | #include <spdlog/details/os.h> |
13 | #include <spdlog/details/circular_q.h> |
14 | #include <spdlog/details/synchronous_factory.h> |
15 | |
16 | #include <chrono> |
17 | #include <cstdio> |
18 | #include <ctime> |
19 | #include <mutex> |
20 | #include <string> |
21 | |
22 | namespace spdlog { |
23 | namespace sinks { |
24 | |
25 | /* |
26 | * Generator of daily log file names in format basename.YYYY-MM-DD.ext |
27 | */ |
28 | struct daily_filename_calculator |
29 | { |
30 | // Create filename for the form basename.YYYY-MM-DD |
31 | static filename_t calc_filename(const filename_t &filename, const tm &now_tm) |
32 | { |
33 | filename_t basename, ext; |
34 | std::tie(basename, ext) = details::file_helper::split_by_extension(filename); |
35 | return fmt_lib::format(SPDLOG_FMT_STRING(SPDLOG_FILENAME_T("{}_{:04d}-{:02d}-{:02d}{}" )), basename, now_tm.tm_year + 1900, |
36 | now_tm.tm_mon + 1, now_tm.tm_mday, ext); |
37 | } |
38 | }; |
39 | |
40 | /* |
41 | * Generator of daily log file names with strftime format. |
42 | * Usages: |
43 | * auto sink = std::make_shared<spdlog::sinks::daily_file_format_sink_mt>("myapp-%Y-%m-%d:%H:%M:%S.log", hour, minute);" |
44 | * auto logger = spdlog::daily_logger_format_mt("loggername, "myapp-%Y-%m-%d:%X.log", hour, minute)" |
45 | * |
46 | */ |
47 | struct daily_filename_format_calculator |
48 | { |
49 | static filename_t calc_filename(const filename_t &filename, const tm &now_tm) |
50 | { |
51 | #ifdef SPDLOG_USE_STD_FORMAT |
52 | // adapted from fmtlib: https://github.com/fmtlib/fmt/blob/8.0.1/include/fmt/chrono.h#L522-L546 |
53 | |
54 | filename_t tm_format; |
55 | tm_format.append(filename); |
56 | // By appending an extra space we can distinguish an empty result that |
57 | // indicates insufficient buffer size from a guaranteed non-empty result |
58 | // https://github.com/fmtlib/fmt/issues/2238 |
59 | tm_format.push_back(' '); |
60 | |
61 | const size_t MIN_SIZE = 10; |
62 | filename_t buf; |
63 | buf.resize(MIN_SIZE); |
64 | for (;;) |
65 | { |
66 | size_t count = strftime(buf.data(), buf.size(), tm_format.c_str(), &now_tm); |
67 | if (count != 0) |
68 | { |
69 | // Remove the extra space. |
70 | buf.resize(count - 1); |
71 | break; |
72 | } |
73 | buf.resize(buf.size() * 2); |
74 | } |
75 | |
76 | return buf; |
77 | #else |
78 | // generate fmt datetime format string, e.g. {:%Y-%m-%d}. |
79 | filename_t fmt_filename = fmt::format(SPDLOG_FMT_STRING(SPDLOG_FILENAME_T("{{:{}}}" )), filename); |
80 | |
81 | // MSVC doesn't allow fmt::runtime(..) with wchar, with fmtlib versions < 9.1.x |
82 | # if defined(_MSC_VER) && defined(SPDLOG_WCHAR_FILENAMES) && FMT_VERSION < 90101 |
83 | return fmt::format(fmt_filename, now_tm); |
84 | # else |
85 | return fmt::format(SPDLOG_FMT_RUNTIME(fmt_filename), now_tm); |
86 | # endif |
87 | |
88 | #endif |
89 | } |
90 | |
91 | private: |
92 | #if defined __GNUC__ |
93 | # pragma GCC diagnostic push |
94 | # pragma GCC diagnostic ignored "-Wformat-nonliteral" |
95 | #endif |
96 | |
97 | static size_t strftime(char *str, size_t count, const char *format, const std::tm *time) |
98 | { |
99 | return std::strftime(str, count, format, time); |
100 | } |
101 | |
102 | static size_t strftime(wchar_t *str, size_t count, const wchar_t *format, const std::tm *time) |
103 | { |
104 | return std::wcsftime(str, count, format, time); |
105 | } |
106 | |
107 | #if defined(__GNUC__) |
108 | # pragma GCC diagnostic pop |
109 | #endif |
110 | }; |
111 | |
112 | /* |
113 | * Rotating file sink based on date. |
114 | * If truncate != false , the created file will be truncated. |
115 | * If max_files > 0, retain only the last max_files and delete previous. |
116 | */ |
117 | template<typename Mutex, typename FileNameCalc = daily_filename_calculator> |
118 | class daily_file_sink final : public base_sink<Mutex> |
119 | { |
120 | public: |
121 | // create daily file sink which rotates on given time |
122 | daily_file_sink(filename_t base_filename, int rotation_hour, int rotation_minute, bool truncate = false, uint16_t max_files = 0, |
123 | const file_event_handlers &event_handlers = {}) |
124 | : base_filename_(std::move(base_filename)) |
125 | , rotation_h_(rotation_hour) |
126 | , rotation_m_(rotation_minute) |
127 | , file_helper_{event_handlers} |
128 | , truncate_(truncate) |
129 | , max_files_(max_files) |
130 | , filenames_q_() |
131 | { |
132 | if (rotation_hour < 0 || rotation_hour > 23 || rotation_minute < 0 || rotation_minute > 59) |
133 | { |
134 | throw_spdlog_ex("daily_file_sink: Invalid rotation time in ctor" ); |
135 | } |
136 | |
137 | auto now = log_clock::now(); |
138 | auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(now)); |
139 | file_helper_.open(filename, truncate_); |
140 | rotation_tp_ = next_rotation_tp_(); |
141 | |
142 | if (max_files_ > 0) |
143 | { |
144 | init_filenames_q_(); |
145 | } |
146 | } |
147 | |
148 | filename_t filename() |
149 | { |
150 | std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_); |
151 | return file_helper_.filename(); |
152 | } |
153 | |
154 | protected: |
155 | void sink_it_(const details::log_msg &msg) override |
156 | { |
157 | auto time = msg.time; |
158 | bool should_rotate = time >= rotation_tp_; |
159 | if (should_rotate) |
160 | { |
161 | auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(time)); |
162 | file_helper_.open(filename, truncate_); |
163 | rotation_tp_ = next_rotation_tp_(); |
164 | } |
165 | memory_buf_t formatted; |
166 | base_sink<Mutex>::formatter_->format(msg, formatted); |
167 | file_helper_.write(formatted); |
168 | |
169 | // Do the cleaning only at the end because it might throw on failure. |
170 | if (should_rotate && max_files_ > 0) |
171 | { |
172 | delete_old_(); |
173 | } |
174 | } |
175 | |
176 | void flush_() override |
177 | { |
178 | file_helper_.flush(); |
179 | } |
180 | |
181 | private: |
182 | void init_filenames_q_() |
183 | { |
184 | using details::os::path_exists; |
185 | |
186 | filenames_q_ = details::circular_q<filename_t>(static_cast<size_t>(max_files_)); |
187 | std::vector<filename_t> filenames; |
188 | auto now = log_clock::now(); |
189 | while (filenames.size() < max_files_) |
190 | { |
191 | auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(now)); |
192 | if (!path_exists(filename)) |
193 | { |
194 | break; |
195 | } |
196 | filenames.emplace_back(filename); |
197 | now -= std::chrono::hours(24); |
198 | } |
199 | for (auto iter = filenames.rbegin(); iter != filenames.rend(); ++iter) |
200 | { |
201 | filenames_q_.push_back(std::move(*iter)); |
202 | } |
203 | } |
204 | |
205 | tm now_tm(log_clock::time_point tp) |
206 | { |
207 | time_t tnow = log_clock::to_time_t(tp); |
208 | return spdlog::details::os::localtime(tnow); |
209 | } |
210 | |
211 | log_clock::time_point next_rotation_tp_() |
212 | { |
213 | auto now = log_clock::now(); |
214 | tm date = now_tm(now); |
215 | date.tm_hour = rotation_h_; |
216 | date.tm_min = rotation_m_; |
217 | date.tm_sec = 0; |
218 | auto rotation_time = log_clock::from_time_t(std::mktime(&date)); |
219 | if (rotation_time > now) |
220 | { |
221 | return rotation_time; |
222 | } |
223 | return {rotation_time + std::chrono::hours(24)}; |
224 | } |
225 | |
226 | // Delete the file N rotations ago. |
227 | // Throw spdlog_ex on failure to delete the old file. |
228 | void delete_old_() |
229 | { |
230 | using details::os::filename_to_str; |
231 | using details::os::remove_if_exists; |
232 | |
233 | filename_t current_file = file_helper_.filename(); |
234 | if (filenames_q_.full()) |
235 | { |
236 | auto old_filename = std::move(filenames_q_.front()); |
237 | filenames_q_.pop_front(); |
238 | bool ok = remove_if_exists(old_filename) == 0; |
239 | if (!ok) |
240 | { |
241 | filenames_q_.push_back(std::move(current_file)); |
242 | throw_spdlog_ex("Failed removing daily file " + filename_to_str(old_filename), errno); |
243 | } |
244 | } |
245 | filenames_q_.push_back(std::move(current_file)); |
246 | } |
247 | |
248 | filename_t base_filename_; |
249 | int rotation_h_; |
250 | int rotation_m_; |
251 | log_clock::time_point rotation_tp_; |
252 | details::file_helper file_helper_; |
253 | bool truncate_; |
254 | uint16_t max_files_; |
255 | details::circular_q<filename_t> filenames_q_; |
256 | }; |
257 | |
258 | using daily_file_sink_mt = daily_file_sink<std::mutex>; |
259 | using daily_file_sink_st = daily_file_sink<details::null_mutex>; |
260 | using daily_file_format_sink_mt = daily_file_sink<std::mutex, daily_filename_format_calculator>; |
261 | using daily_file_format_sink_st = daily_file_sink<details::null_mutex, daily_filename_format_calculator>; |
262 | |
263 | } // namespace sinks |
264 | |
265 | // |
266 | // factory functions |
267 | // |
268 | template<typename Factory = spdlog::synchronous_factory> |
269 | inline std::shared_ptr<logger> daily_logger_mt(const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0, |
270 | bool truncate = false, uint16_t max_files = 0, const file_event_handlers &event_handlers = {}) |
271 | { |
272 | return Factory::template create<sinks::daily_file_sink_mt>(logger_name, filename, hour, minute, truncate, max_files, event_handlers); |
273 | } |
274 | |
275 | template<typename Factory = spdlog::synchronous_factory> |
276 | inline std::shared_ptr<logger> daily_logger_format_mt(const std::string &logger_name, const filename_t &filename, int hour = 0, |
277 | int minute = 0, bool truncate = false, uint16_t max_files = 0, const file_event_handlers &event_handlers = {}) |
278 | { |
279 | return Factory::template create<sinks::daily_file_format_sink_mt>( |
280 | logger_name, filename, hour, minute, truncate, max_files, event_handlers); |
281 | } |
282 | |
283 | template<typename Factory = spdlog::synchronous_factory> |
284 | inline std::shared_ptr<logger> daily_logger_st(const std::string &logger_name, const filename_t &filename, int hour = 0, int minute = 0, |
285 | bool truncate = false, uint16_t max_files = 0, const file_event_handlers &event_handlers = {}) |
286 | { |
287 | return Factory::template create<sinks::daily_file_sink_st>(logger_name, filename, hour, minute, truncate, max_files, event_handlers); |
288 | } |
289 | |
290 | template<typename Factory = spdlog::synchronous_factory> |
291 | inline std::shared_ptr<logger> daily_logger_format_st(const std::string &logger_name, const filename_t &filename, int hour = 0, |
292 | int minute = 0, bool truncate = false, uint16_t max_files = 0, const file_event_handlers &event_handlers = {}) |
293 | { |
294 | return Factory::template create<sinks::daily_file_format_sink_st>( |
295 | logger_name, filename, hour, minute, truncate, max_files, event_handlers); |
296 | } |
297 | } // namespace spdlog |
298 | |