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