Kea 3.3.3
rotating_file.cc
Go to the documentation of this file.
1// Copyright (C) 2016-2026 Internet Systems Consortium, Inc. ("ISC")
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7#include <config.h>
8
12#include <hooks/hooks_parser.h>
14
15#include <iostream>
16#include <list>
17#include <set>
18#include <sstream>
19
20#include <boost/date_time/posix_time/posix_time.hpp>
21
22#include <dirent.h>
23#include <errno.h>
24#include <time.h>
25
26using namespace isc::asiolink;
27using namespace isc::util;
28using namespace isc::dhcp;
29using namespace isc::data;
30using namespace isc::db;
31using namespace isc::hooks;
32using namespace std;
33
34namespace isc {
35namespace legal_log {
36
38 : LegalLogMgr(parameters), time_unit_(TimeUnit::Day), count_(1),
39 timestamp_(0), mark_continuation_lines_(true) {
40 apply(parameters);
41}
42
43void
45 string path(LegalLogMgr::getLogPath());
46 string base("kea-legal");
48 int64_t count(1);
49 string count_str;
50 string prerotate;
51 string postrotate;
52
53 // Prioritize parameters.
54 if (parameters.find("path") != parameters.end()) {
55 path = parameters.at("path");
56 }
57 if (parameters.find("base-name") != parameters.end()) {
58 base = parameters.at("base-name");
59 }
60 if (parameters.find("time-unit") != parameters.end()) {
61 string time_unit(parameters.at("time-unit"));
62
63 if (time_unit == "second") {
65 } else if (time_unit == "day") {
67 } else if (time_unit == "month") {
69 } else if (time_unit == "year") {
71 } else {
72 isc_throw(BadValue, "unknown time unit type: " << time_unit
73 << ", expected one of: second, day, month, year");
74 }
75 }
76 if (parameters.find("count") != parameters.end()) {
77 try {
78 count = boost::lexical_cast<int64_t>(parameters.at("count"));
79 } catch (...) {
80 isc_throw(BadValue, "bad value: " << parameters.at("count") << " for count parameter");
81 }
82 if ((count < 0) ||
83 (count > numeric_limits<uint32_t>::max())) {
84 isc_throw(OutOfRange, "count value: " << count
85 << " is out of range, expected value: 0.."
86 << numeric_limits<uint32_t>::max());
87 }
88 }
89 if (parameters.find("prerotate") != parameters.end()) {
90 prerotate = parameters.at("prerotate");
91 }
92 if (parameters.find("postrotate") != parameters.end()) {
93 postrotate = parameters.at("postrotate");
94 }
95 if (parameters.find("mark-continuation-lines") != parameters.end()) {
96 string mcl(parameters.at("mark-continuation-lines"));
97 // The parser sets "true" or "false" so do not check...
98 mark_continuation_lines_ = (mcl != "false");
99 }
100 path_ = path;
101 base_name_ = base;
102 time_unit_ = unit;
103 count_ = static_cast<uint32_t>(count);
104 prerotate_ = prerotate;
105 postrotate_ = postrotate;
106
107 if (path_.empty()) {
108 isc_throw(LegalLogMgrError, "path cannot be blank");
109 }
110
111 if (base_name_.empty()) {
112 isc_throw(LegalLogMgrError, "file name cannot be blank");
113 }
114
115 if (!prerotate_.empty()) {
116 try {
119 } catch (const isc::Exception& ex) {
120 isc_throw(LegalLogMgrError, "Invalid 'prerotate' parameter: " << ex.what());
121 }
122 }
123
124 if (!postrotate_.empty()) {
125 try {
128 } catch (const isc::Exception& ex) {
129 isc_throw(LegalLogMgrError, "Invalid 'postrotate' parameter: " << ex.what());
130 }
131 }
132}
133
137
138string
139RotatingFile::getYearMonthDay(const struct tm& time_info) {
140 char buffer[128];
141 strftime(buffer, sizeof(buffer), "%Y%m%d", &time_info);
142 return (string(buffer));
143}
144
145void
146RotatingFile::updateFileNameAndTimestamp(struct tm& time_info, bool use_existing) {
147 ostringstream stream;
148 string name = base_name_ + ".";
149
150 stream << path_ << "/";
151
152 if (time_unit_ == TimeUnit::Second) {
153 time_t timestamp = mktime(&time_info);
154 ostringstream name_stream;
155 name_stream << right << setfill('0') << setw(20)
156 << static_cast<uint64_t>(timestamp);
157 name += "T";
158 name += name_stream.str();
159 } else {
160 name += getYearMonthDay(time_info);
161 }
162
163 stream << name << ".txt";
164
165 file_name_ = stream.str();
166
167 if (use_existing) {
168 useExistingFiles(time_info);
169 }
170}
171
172void
173RotatingFile::useExistingFiles(struct tm& time_info) {
174 DIR* dir = opendir(path_.c_str());
175 if (!dir) {
176 return;
177 }
178
179 unique_ptr<DIR, void(*)(DIR*)> defer(dir, [](DIR* d) { closedir(d); });
180
181 // Set of sorted files by name.
182 set<string> files;
183
184 // Add only files of interest that could be used to append logging data.
185 for (struct dirent* dent = readdir(dir); dent; dent = readdir(dir)) {
186 string name(dent->d_name);
187 // Supported file formats are: 'base-name.YYYYMMDD.txt' and
188 // 'base-name.TXXXXXXXXXXXXXXXXXXXX.txt'.
189 if ((name.size() != (base_name_.size() + sizeof(".YYYYMMDD.txt") - 1)) &&
190 (name.size() != (base_name_.size() + sizeof(".TXXXXXXXXXXXXXXXXXXXX.txt") - 1))) {
191 continue;
192 }
193
194 // Skip non .txt files.
195 if (name.substr(name.size() - 4) != ".txt") {
196 continue;
197 }
198
199 string file = name.substr(0, name.size() - 4);
200
201 // Skip files that are not beginning with base name.
202 if (base_name_ != file.substr(0, base_name_.size())) {
203 continue;
204 }
205
206 file = file.substr(base_name_.size() + 1);
207 uint32_t tag_size = sizeof("YYYYMMDD") - 1;
208 uint32_t index = 0;
209 if (time_unit_ == TimeUnit::Second) {
210 if (file.at(0) != 'T') {
211 continue;
212 }
213 tag_size = sizeof("TXXXXXXXXXXXXXXXXXXXX") - 1;
214 index = 1;
215 }
216 if (file.size() != tag_size) {
217 continue;
218 }
219 for (; index < tag_size; ++index) {
220 if (!isdigit(file.at(index))) {
221 break;
222 }
223 }
224 if (index != tag_size) {
225 continue;
226 }
227 files.insert(file);
228 }
229
230 if (!files.size()) {
231 return;
232 }
233
234 string file = *files.rbegin();
235
236 if (time_unit_ == TimeUnit::Second) {
237 time_t file_timestamp;
238 try {
239 file_timestamp = static_cast<time_t>(boost::lexical_cast<uint64_t>(file.substr(1)));
240 } catch (...) {
241 return;
242 }
243 time_t current_timestamp = mktime(&time_info);
244 if (current_timestamp < (file_timestamp + static_cast<time_t>(count_))) {
245 localtime_r(&file_timestamp, &time_info);
246 } else {
247 file.clear();
248 }
249 } else {
250 boost::gregorian::date file_date;
251 try {
252 file_date = boost::gregorian::from_undelimited_string(file);
253 } catch (...) {
254 return;
255 }
256 boost::gregorian::date current_date = boost::gregorian::date_from_tm(time_info);
257 if (time_unit_ == TimeUnit::Day) {
258 boost::gregorian::date_duration dd(count_);
259 if (current_date < (file_date + dd)) {
260 time_info = boost::gregorian::to_tm(file_date);
261 } else {
262 file.clear();
263 }
264 } else if (time_unit_ == TimeUnit::Month) {
265 boost::gregorian::months mm(count_);
266 if (current_date < (file_date + mm)) {
267 time_info = boost::gregorian::to_tm(file_date);
268 } else {
269 file.clear();
270 }
271 } else if (time_unit_ == TimeUnit::Year) {
272 boost::gregorian::years yy(count_);
273 if (current_date < (file_date + yy)) {
274 time_info = boost::gregorian::to_tm(file_date);
275 } else {
276 file.clear();
277 }
278 }
279 }
280 if (!file.empty()) {
281 file_name_ = path_ + "/" + base_name_ + "." + file + ".txt";
282 }
283}
284
285void
287 if (isOpen() || MultiThreadingMgr::instance().isTestMode()) {
288 return;
289 }
290
291 struct tm current_time_info = currentTimeInfo();
292 openInternal(current_time_info, true);
293}
294
295void
296RotatingFile::openInternal(struct tm& time_info, bool use_existing) {
297 updateFileNameAndTimestamp(time_info, use_existing);
298 // Open the file
299 file_.open(file_name_.c_str(), ofstream::app);
300 int sav_error = errno;
301 if (!file_.is_open()) {
302 isc_throw(LegalLogMgrError, "cannot open file:" << file_name_
303 << " reason: " << strerror(sav_error));
304 }
305
306 // Store the timestamp for the new open file
307 timestamp_ = mktime(&time_info);
308
310 .arg(file_name_);
311}
312
313void
315 if (isOpen() && !count_) {
316 return;
317 }
318
319 bool rotate_file = false;
320
321 // Time info used for old timestamp
322 struct tm time_info;
323 localtime_r(&timestamp_, &time_info);
324
325 // Time info used for new timestamp
326 struct tm current_time_info = currentTimeInfo();
327
328 // New timestamp
329 time_t timestamp = mktime(&current_time_info);
330
331 // Date used for old timestamp
332 boost::gregorian::date old_date = boost::gregorian::date_from_tm(time_info);
333
334 // Date used for new timestamp
335 boost::gregorian::date new_date = boost::gregorian::date_from_tm(current_time_info);
336
337 if (!isOpen()) {
338 rotate_file = true;
339 } else if (time_unit_ == TimeUnit::Second) {
340 if (static_cast<time_t>(count_) <= (timestamp - timestamp_)) {
341 rotate_file = true;
342 }
343 } else if (time_unit_ == TimeUnit::Day) {
344 boost::gregorian::date_duration dd(count_);
345 if ((old_date + dd) <= new_date) {
346 rotate_file = true;
347 }
348 } else if (time_unit_ == TimeUnit::Month) {
349 boost::gregorian::months mm(count_);
350 if ((old_date + mm) <= new_date) {
351 rotate_file = true;
352 }
353 } else if (time_unit_ == TimeUnit::Year) {
354 boost::gregorian::years yy(count_);
355 if ((old_date + yy) <= new_date) {
356 rotate_file = true;
357 }
358 }
359
360 if (rotate_file) {
361 close();
362
363 if (!prerotate_.empty()) {
364 ProcessArgs args;
365 args.push_back(getFileName());
366 ProcessSpawn process(ProcessSpawn::ASYNC, prerotate_, args);
367 process.spawn(true);
368 }
369
370 openInternal(current_time_info, false);
371
372 if (!postrotate_.empty()) {
373 ProcessArgs args;
374 args.push_back(getFileName());
375 ProcessSpawn process(ProcessSpawn::ASYNC, postrotate_, args);
376 process.spawn(true);
377 }
378 }
379}
380
381void
382RotatingFile::writeln(const string& text, const string&) {
383 if (util::MultiThreadingMgr::instance().getMode()) {
384 lock_guard<mutex> lock(mutex_);
385 writelnInternal(text);
386 } else {
387 writelnInternal(text);
388 }
389}
390
391void
392RotatingFile::writelnInternal(const string& text) {
393 if (text.empty()) {
394 return;
395 }
396
397 // Call rotate in case we've crossed days since we last wrote.
398 rotate();
399
400 string timestamp = getNowString();
401 stringstream ss(text);
402 // Collect lines.
403 list<string> lines;
404 for (string line; getline(ss, line, '\n');) {
405 lines.push_back(line);
406 }
407 while (!lines.empty()) {
408 string line = lines.front();
409 lines.pop_front();
410 file_ << timestamp;
411 if (mark_continuation_lines_ && !lines.empty()) {
412 file_ << "-";
413 } else {
414 file_ << " ";
415 }
416 file_ << line << endl;
417 }
418 int sav_error = errno;
419 if (!file_.good()) {
420 isc_throw(LegalLogMgrError, "error writing to file:" << file_name_
421 << " reason: " << strerror(sav_error));
422 }
423}
424
425bool
427 return (file_.is_open());
428}
429
430void
432 try {
433 if (file_.is_open()) {
435 .arg(file_name_);
436 file_.flush();
437 file_.close();
438 }
439 } catch (const exception& ex) {
440 // Highly unlikely to occur but let's at least spit out an error.
441 // Beyond that we swallow it for tidiness.
443 .arg(file_name_).arg(ex.what());
444 }
445}
446
453
454} // namespace legal_log
455} // namespace isc
A generic exception that is thrown if a parameter given to a method is considered invalid in that con...
This is a base class for exceptions thrown from the DNS library module.
virtual const char * what() const
Returns a C-style character string of the cause of the exception.
A generic exception that is thrown if a parameter given to a method would refer to or modify out-of-r...
static std::string redactedAccessString(const ParameterMap &parameters)
Redact database access string.
std::map< std::string, std::string > ParameterMap
Database configuration parameter map.
Thrown if a LegalLogMgr encounters an error.
virtual struct tm currentTimeInfo() const
Returns the current local date and time.
static std::string getLogPath(bool reset=false, const std::string explicit_path="")
Fetches the supported legal log file path.
LegalLogMgr(const isc::db::DatabaseConnection::ParameterMap parameters)
Constructor.
virtual std::string getNowString() const
Returns the current date and time as string.
static std::string validatePath(const std::string libpath)
Validates a script path (script loaded by a hook) against the supported path.
virtual void writeln(const std::string &text, const std::string &addr)
Appends a string to the current file.
bool mark_continuation_lines_
The mark continuation lines flag.
virtual void close()
Closes the underlying file.
std::string getFileName() const
Returns the current file name.
static std::string getYearMonthDay(const struct tm &time_info)
Build the year-month-day string from a date.
virtual ~RotatingFile()
Destructor.
static isc::dhcp::LegalLogMgrPtr factory(const isc::db::DatabaseConnection::ParameterMap &parameters)
Factory class method.
void useExistingFiles(struct tm &time_info)
Update file name with previously created file.
virtual void open()
Opens the current file for writing.
RotatingFile(const isc::db::DatabaseConnection::ParameterMap &parameters)
Constructor.
virtual void rotate()
Rotates the file if necessary.
void apply(const isc::db::DatabaseConnection::ParameterMap &parameters)
Parse file specification and create forensic log backend.
virtual bool isOpen() const
Returns true if the file is open.
TimeUnit
Time unit type used to rotate file.
void updateFileNameAndTimestamp(struct tm &time_info, bool use_existing)
Function which updates the file name and internal timestamp from previously created file name (if it ...
virtual void openInternal(struct tm &time_info, bool use_existing)
Open file using specified timestamp.
static MultiThreadingMgr & instance()
Returns a single instance of Multi Threading Manager.
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
const isc::log::MessageID LEGAL_LOG_STORE_OPEN
const isc::log::MessageID LEGAL_LOG_STORE_CLOSE_ERROR
const isc::log::MessageID LEGAL_LOG_STORE_OPENED
const isc::log::MessageID LEGAL_LOG_STORE_CLOSED
#define LOG_ERROR(LOGGER, MESSAGE)
Macro to conveniently test error output and log it.
Definition macros.h:32
#define LOG_INFO(LOGGER, MESSAGE)
Macro to conveniently test info output and log it.
Definition macros.h:20
boost::shared_ptr< LegalLogMgr > LegalLogMgrPtr
Defines a smart pointer to a LegalLogMgr.
isc::log::Logger legal_log_logger("legal-log-hooks")
Legal Log Logger.
Defines the logger used by the top-level component of kea-lfc.
Defines the class, RotatingFile, which implements an appending text file that rotates to a new file o...