Kea 3.3.1
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
10#include <hooks/hooks_parser.h>
11#include <legal_log_log.h>
12#include <rotating_file.h>
14
15#include <boost/date_time/posix_time/posix_time.hpp>
16
17#include <errno.h>
18#include <iostream>
19#include <list>
20#include <set>
21#include <sstream>
22#include <time.h>
23#include <dirent.h>
24
25using namespace isc::asiolink;
26using namespace isc::util;
27using namespace isc::dhcp;
28using namespace isc::data;
29using namespace isc::db;
30using namespace isc::hooks;
31using namespace std;
32
33namespace isc {
34namespace legal_log {
35
37 : LegalLogMgr(parameters), time_unit_(TimeUnit::Day), count_(1),
38 timestamp_(0), mark_continuation_lines_(true) {
39 apply(parameters);
40}
41
42void
44 string path(LegalLogMgr::getLogPath());
45 string base("kea-legal");
47 int64_t count(1);
48 string count_str;
49 string prerotate;
50 string postrotate;
51
52 // Prioritize parameters.
53 if (parameters.find("path") != parameters.end()) {
54 path = parameters.at("path");
55 }
56 if (parameters.find("base-name") != parameters.end()) {
57 base = parameters.at("base-name");
58 }
59 if (parameters.find("time-unit") != parameters.end()) {
60 string time_unit(parameters.at("time-unit"));
61
62 if (time_unit == "second") {
64 } else if (time_unit == "day") {
66 } else if (time_unit == "month") {
68 } else if (time_unit == "year") {
70 } else {
71 isc_throw(BadValue, "unknown time unit type: " << time_unit
72 << ", expected one of: second, day, month, year");
73 }
74 }
75 if (parameters.find("count") != parameters.end()) {
76 try {
77 count = boost::lexical_cast<int64_t>(parameters.at("count"));
78 } catch (...) {
79 isc_throw(BadValue, "bad value: " << parameters.at("count") << " for count parameter");
80 }
81 if ((count < 0) ||
82 (count > numeric_limits<uint32_t>::max())) {
83 isc_throw(OutOfRange, "count value: " << count
84 << " is out of range, expected value: 0.."
85 << numeric_limits<uint32_t>::max());
86 }
87 }
88 if (parameters.find("prerotate") != parameters.end()) {
89 prerotate = parameters.at("prerotate");
90 }
91 if (parameters.find("postrotate") != parameters.end()) {
92 postrotate = parameters.at("postrotate");
93 }
94 if (parameters.find("mark-continuation-lines") != parameters.end()) {
95 string mcl(parameters.at("mark-continuation-lines"));
96 // The parser sets "true" or "false" so do not check...
97 mark_continuation_lines_ = (mcl != "false");
98 }
99 path_ = path;
100 base_name_ = base;
101 time_unit_ = unit;
102 count_ = static_cast<uint32_t>(count);
103 prerotate_ = prerotate;
104 postrotate_ = postrotate;
105
106 if (path_.empty()) {
107 isc_throw(LegalLogMgrError, "path cannot be blank");
108 }
109
110 if (base_name_.empty()) {
111 isc_throw(LegalLogMgrError, "file name cannot be blank");
112 }
113
114 if (!prerotate_.empty()) {
115 try {
118 } catch (const isc::Exception& ex) {
119 isc_throw(LegalLogMgrError, "Invalid 'prerotate' parameter: " << ex.what());
120 }
121 }
122
123 if (!postrotate_.empty()) {
124 try {
127 } catch (const isc::Exception& ex) {
128 isc_throw(LegalLogMgrError, "Invalid 'postrotate' parameter: " << ex.what());
129 }
130 }
131}
132
136
137string
138RotatingFile::getYearMonthDay(const struct tm& time_info) {
139 char buffer[128];
140 strftime(buffer, sizeof(buffer), "%Y%m%d", &time_info);
141 return (string(buffer));
142}
143
144void
145RotatingFile::updateFileNameAndTimestamp(struct tm& time_info, bool use_existing) {
146 ostringstream stream;
147 string name = base_name_ + ".";
148
149 stream << path_ << "/";
150
151 if (time_unit_ == TimeUnit::Second) {
152 time_t timestamp = mktime(&time_info);
153 ostringstream name_stream;
154 name_stream << right << setfill('0') << setw(20)
155 << static_cast<uint64_t>(timestamp);
156 name += "T";
157 name += name_stream.str();
158 } else {
159 name += getYearMonthDay(time_info);
160 }
161
162 stream << name << ".txt";
163
164 file_name_ = stream.str();
165
166 if (use_existing) {
167 useExistingFiles(time_info);
168 }
169}
170
171void
172RotatingFile::useExistingFiles(struct tm& time_info) {
173 DIR* dir = opendir(path_.c_str());
174 if (!dir) {
175 return;
176 }
177
178 unique_ptr<DIR, void(*)(DIR*)> defer(dir, [](DIR* d) { closedir(d); });
179
180 // Set of sorted files by name.
181 set<string> files;
182
183 // Add only files of interest that could be used to append logging data.
184 for (struct dirent* dent = readdir(dir); dent; dent = readdir(dir)) {
185 string name(dent->d_name);
186 // Supported file formats are: 'base-name.YYYYMMDD.txt' and
187 // 'base-name.TXXXXXXXXXXXXXXXXXXXX.txt'.
188 if ((name.size() != (base_name_.size() + sizeof(".YYYYMMDD.txt") - 1)) &&
189 (name.size() != (base_name_.size() + sizeof(".TXXXXXXXXXXXXXXXXXXXX.txt") - 1))) {
190 continue;
191 }
192
193 // Skip non .txt files.
194 if (name.substr(name.size() - 4) != ".txt") {
195 continue;
196 }
197
198 string file = name.substr(0, name.size() - 4);
199
200 // Skip files that are not beginning with base name.
201 if (base_name_ != file.substr(0, base_name_.size())) {
202 continue;
203 }
204
205 file = file.substr(base_name_.size() + 1);
206 uint32_t tag_size = sizeof("YYYYMMDD") - 1;
207 uint32_t index = 0;
208 if (time_unit_ == TimeUnit::Second) {
209 if (file.at(0) != 'T') {
210 continue;
211 }
212 tag_size = sizeof("TXXXXXXXXXXXXXXXXXXXX") - 1;
213 index = 1;
214 }
215 if (file.size() != tag_size) {
216 continue;
217 }
218 for (; index < tag_size; ++index) {
219 if (!isdigit(file.at(index))) {
220 break;
221 }
222 }
223 if (index != tag_size) {
224 continue;
225 }
226 files.insert(file);
227 }
228
229 if (!files.size()) {
230 return;
231 }
232
233 string file = *files.rbegin();
234
235 if (time_unit_ == TimeUnit::Second) {
236 time_t file_timestamp;
237 try {
238 file_timestamp = static_cast<time_t>(boost::lexical_cast<uint64_t>(file.substr(1)));
239 } catch (...) {
240 return;
241 }
242 time_t current_timestamp = mktime(&time_info);
243 if (current_timestamp < (file_timestamp + static_cast<time_t>(count_))) {
244 localtime_r(&file_timestamp, &time_info);
245 } else {
246 file.clear();
247 }
248 } else {
249 boost::gregorian::date file_date;
250 try {
251 file_date = boost::gregorian::from_undelimited_string(file);
252 } catch (...) {
253 return;
254 }
255 boost::gregorian::date current_date = boost::gregorian::date_from_tm(time_info);
256 if (time_unit_ == TimeUnit::Day) {
257 boost::gregorian::date_duration dd(count_);
258 if (current_date < (file_date + dd)) {
259 time_info = boost::gregorian::to_tm(file_date);
260 } else {
261 file.clear();
262 }
263 } else if (time_unit_ == TimeUnit::Month) {
264 boost::gregorian::months mm(count_);
265 if (current_date < (file_date + mm)) {
266 time_info = boost::gregorian::to_tm(file_date);
267 } else {
268 file.clear();
269 }
270 } else if (time_unit_ == TimeUnit::Year) {
271 boost::gregorian::years yy(count_);
272 if (current_date < (file_date + yy)) {
273 time_info = boost::gregorian::to_tm(file_date);
274 } else {
275 file.clear();
276 }
277 }
278 }
279 if (!file.empty()) {
280 file_name_ = path_ + "/" + base_name_ + "." + file + ".txt";
281 }
282}
283
284void
286 if (isOpen() || MultiThreadingMgr::instance().isTestMode()) {
287 return;
288 }
289
290 struct tm current_time_info = currentTimeInfo();
291 openInternal(current_time_info, true);
292}
293
294void
295RotatingFile::openInternal(struct tm& time_info, bool use_existing) {
296 updateFileNameAndTimestamp(time_info, use_existing);
297 // Open the file
298 file_.open(file_name_.c_str(), ofstream::app);
299 int sav_error = errno;
300 if (!file_.is_open()) {
301 isc_throw(LegalLogMgrError, "cannot open file:" << file_name_
302 << " reason: " << strerror(sav_error));
303 }
304
305 // Store the timestamp for the new open file
306 timestamp_ = mktime(&time_info);
307
309 .arg(file_name_);
310}
311
312void
314 if (isOpen() && !count_) {
315 return;
316 }
317
318 bool rotate_file = false;
319
320 // Time info used for old timestamp
321 struct tm time_info;
322 localtime_r(&timestamp_, &time_info);
323
324 // Time info used for new timestamp
325 struct tm current_time_info = currentTimeInfo();
326
327 // New timestamp
328 time_t timestamp = mktime(&current_time_info);
329
330 // Date used for old timestamp
331 boost::gregorian::date old_date = boost::gregorian::date_from_tm(time_info);
332
333 // Date used for new timestamp
334 boost::gregorian::date new_date = boost::gregorian::date_from_tm(current_time_info);
335
336 if (!isOpen()) {
337 rotate_file = true;
338 } else if (time_unit_ == TimeUnit::Second) {
339 if (static_cast<time_t>(count_) <= (timestamp - timestamp_)) {
340 rotate_file = true;
341 }
342 } else if (time_unit_ == TimeUnit::Day) {
343 boost::gregorian::date_duration dd(count_);
344 if ((old_date + dd) <= new_date) {
345 rotate_file = true;
346 }
347 } else if (time_unit_ == TimeUnit::Month) {
348 boost::gregorian::months mm(count_);
349 if ((old_date + mm) <= new_date) {
350 rotate_file = true;
351 }
352 } else if (time_unit_ == TimeUnit::Year) {
353 boost::gregorian::years yy(count_);
354 if ((old_date + yy) <= new_date) {
355 rotate_file = true;
356 }
357 }
358
359 if (rotate_file) {
360 close();
361
362 if (!prerotate_.empty()) {
363 ProcessArgs args;
364 args.push_back(getFileName());
365 ProcessSpawn process(ProcessSpawn::ASYNC, prerotate_, args);
366 process.spawn(true);
367 }
368
369 openInternal(current_time_info, false);
370
371 if (!postrotate_.empty()) {
372 ProcessArgs args;
373 args.push_back(getFileName());
374 ProcessSpawn process(ProcessSpawn::ASYNC, postrotate_, args);
375 process.spawn(true);
376 }
377 }
378}
379
380void
381RotatingFile::writeln(const string& text, const string&) {
382 if (util::MultiThreadingMgr::instance().getMode()) {
383 lock_guard<mutex> lock(mutex_);
384 writelnInternal(text);
385 } else {
386 writelnInternal(text);
387 }
388}
389
390void
391RotatingFile::writelnInternal(const string& text) {
392 if (text.empty()) {
393 return;
394 }
395
396 // Call rotate in case we've crossed days since we last wrote.
397 rotate();
398
399 string timestamp = getNowString();
400 stringstream ss(text);
401 // Collect lines.
402 list<string> lines;
403 for (string line; getline(ss, line, '\n');) {
404 lines.push_back(line);
405 }
406 while (!lines.empty()) {
407 string line = lines.front();
408 lines.pop_front();
409 file_ << timestamp;
410 if (mark_continuation_lines_ && !lines.empty()) {
411 file_ << "-";
412 } else {
413 file_ << " ";
414 }
415 file_ << line << endl;
416 }
417 int sav_error = errno;
418 if (!file_.good()) {
419 isc_throw(LegalLogMgrError, "error writing to file:" << file_name_
420 << " reason: " << strerror(sav_error));
421 }
422}
423
424bool
426 return (file_.is_open());
427}
428
429void
431 try {
432 if (file_.is_open()) {
434 .arg(file_name_);
435 file_.flush();
436 file_.close();
437 }
438 } catch (const exception& ex) {
439 // Highly unlikely to occur but let's at least spit out an error.
440 // Beyond that we swallow it for tidiness.
442 .arg(file_name_).arg(ex.what());
443 }
444}
445
452
453} // namespace legal_log
454} // 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...