1//! @file log.cpp
2//! @author ryftchen
3//! @brief The definitions (log) in the application module.
4//! @version 0.1.0
5//! @copyright Copyright (c) 2022-2026 ryftchen. All rights reserved.
6
7#include "log.hpp"
8
9#ifndef _PRECOMPILED_HEADER
10#include <cassert>
11#include <filesystem>
12#include <fstream>
13#include <ranges>
14#include <regex>
15#else
16#include "application/pch/precompiled_header.hpp"
17#endif
18
19#include "utility/include/benchmark.hpp"
20#include "utility/include/time.hpp"
21
22namespace application
23{
24namespace log
25{
26//! @brief Alias for the access controller.
27using AccessController = configure::Controller<Log>;
28
29//! @brief The SGR (select graphic rendition) of ANSI control sequences.
30namespace ansi_sgr
31{
32//! @brief ANSI escape codes for red foreground color.
33constexpr std::string_view red = "\033[0;31m";
34//! @brief ANSI escape codes for green foreground color.
35constexpr std::string_view green = "\033[0;32m";
36//! @brief ANSI escape codes for yellow foreground color.
37constexpr std::string_view yellow = "\033[0;33m";
38//! @brief ANSI escape codes for blue foreground color.
39constexpr std::string_view blue = "\033[0;34m";
40//! @brief ANSI escape codes for bold foreground color.
41constexpr std::string_view bold = "\033[1m";
42//! @brief ANSI escape codes for dim foreground color.
43constexpr std::string_view dim = "\033[2m";
44//! @brief ANSI escape codes for italic foreground color.
45constexpr std::string_view italic = "\033[3m";
46//! @brief ANSI escape codes for underline foreground color.
47constexpr std::string_view underline = "\033[4m";
48//! @brief ANSI escape codes for reverse.
49constexpr std::string_view reverse = "\033[7m";
50//! @brief ANSI escape codes for default foreground color.
51constexpr std::string_view defaultFg = "\033[39m";
52//! @brief ANSI escape codes for default background color.
53constexpr std::string_view defaultBg = "\033[49m";
54//! @brief ANSI escape codes for reset.
55constexpr std::string_view reset = "\033[0m";
56} // namespace ansi_sgr
57
58//! @brief Anonymous namespace.
59inline namespace
60{
61//! @brief Prefix of debug level in log.
62constexpr std::string_view debugLevelPrefix = "[DBG]";
63//! @brief Prefix of info level in log.
64constexpr std::string_view infoLevelPrefix = "[INF]";
65//! @brief Prefix of warning level in log.
66constexpr std::string_view warningLevelPrefix = "[WRN]";
67//! @brief Prefix of error level in log.
68constexpr std::string_view errorLevelPrefix = "[ERR]";
69//! @brief Prefix of trace level in log.
70constexpr std::string_view traceLevelPrefix = "[TRC]";
71//! @brief Regular expression of debug level in log.
72constexpr std::string_view debugLevelPrefixRegex = R"(\[DBG\])";
73//! @brief Regular expression of info level in log.
74constexpr std::string_view infoLevelPrefixRegex = R"(\[INF\])";
75//! @brief Regular expression of warning level in log.
76constexpr std::string_view warningLevelPrefixRegex = R"(\[WRN\])";
77//! @brief Regular expression of error level in log.
78constexpr std::string_view errorLevelPrefixRegex = R"(\[ERR\])";
79//! @brief Regular expression of trace level in log.
80constexpr std::string_view traceLevelPrefixRegex = R"(\[TRC\])";
81//! @brief Regular expression of date time in log.
82constexpr std::string_view dateTimeRegex = R"(\[(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{9})Z\])";
83//! @brief Regular expression of code file in log.
84constexpr std::string_view codeFileRegex = R"(\[[^ ]+\.(cpp|hpp)#\d+\])";
85
86//! @brief Debug level prefix with color. Include ANSI escape codes.
87constexpr auto debugLevelPrefixWithColor = utility::common::
88 concatString<ansi_sgr::blue, ansi_sgr::bold, ansi_sgr::defaultBg, debugLevelPrefix, ansi_sgr::reset>;
89//! @brief Info level prefix with color. Include ANSI escape codes.
90constexpr auto infoLevelPrefixWithColor = utility::common::
91 concatString<ansi_sgr::green, ansi_sgr::bold, ansi_sgr::defaultBg, infoLevelPrefix, ansi_sgr::reset>;
92//! @brief Warning level prefix with color. Include ANSI escape codes.
93constexpr auto warningLevelPrefixWithColor = utility::common::
94 concatString<ansi_sgr::yellow, ansi_sgr::bold, ansi_sgr::defaultBg, warningLevelPrefix, ansi_sgr::reset>;
95//! @brief Error level prefix with color. Include ANSI escape codes.
96constexpr auto errorLevelPrefixWithColor = utility::common::
97 concatString<ansi_sgr::red, ansi_sgr::bold, ansi_sgr::defaultBg, errorLevelPrefix, ansi_sgr::reset>;
98//! @brief Trace level prefix with color. Include ANSI escape codes.
99constexpr auto traceLevelPrefixWithColor = utility::common::
100 concatString<ansi_sgr::reverse, ansi_sgr::bold, ansi_sgr::defaultBg, traceLevelPrefix, ansi_sgr::reset>;
101//! @brief Base color of the date time. Include ANSI escape codes.
102constexpr auto dateTimeBaseColor =
103 utility::common::concatString<ansi_sgr::defaultFg, ansi_sgr::bold, ansi_sgr::dim, ansi_sgr::defaultBg>;
104//! @brief Base color of the code file. Include ANSI escape codes.
105constexpr auto codeFileBaseColor =
106 utility::common::concatString<ansi_sgr::defaultFg, ansi_sgr::bold, ansi_sgr::underline, ansi_sgr::defaultBg>;
107//! @brief Base color of the history cache. Include ANSI escape codes.
108constexpr auto historyCacheBaseColor =
109 utility::common::concatString<ansi_sgr::reverse, ansi_sgr::italic, ansi_sgr::defaultBg>;
110
111//! @brief Regular expressions for log highlighting.
112struct HlRegex
113{
114 //! @brief Alternatives to predefined level prefix highlighting.
115 const std::vector<std::pair<std::regex, std::string>> predefinedLevelPrefixes{
116 {std::regex{std::string{debugLevelPrefixRegex}}, std::string{debugLevelPrefixWithColor}},
117 {std::regex{std::string{infoLevelPrefixRegex}}, std::string{infoLevelPrefixWithColor}},
118 {std::regex{std::string{warningLevelPrefixRegex}}, std::string{warningLevelPrefixWithColor}},
119 {std::regex{std::string{errorLevelPrefixRegex}}, std::string{errorLevelPrefixWithColor}},
120 {std::regex{std::string{traceLevelPrefixRegex}}, std::string{traceLevelPrefixWithColor}}};
121 //! @brief Identity segments for basic highlighting (date time, code file, etc.).
122 const std::vector<std::pair<std::regex, std::string>> identitySegments{
123 {std::regex{std::string{dateTimeRegex}}, std::string{dateTimeBaseColor}},
124 {std::regex{std::string{codeFileRegex}}, std::string{codeFileBaseColor}}};
125};
126} // namespace
127
128//! @brief Log style.
129static const HlRegex& logStyle()
130{
131 static const HlRegex highlight{};
132 return highlight;
133}
134
135Log::Log() : FSM(State::initial)
136{
137 if (!configure::detail::activateHelper()) [[unlikely]]
138 {
139 throw std::logic_error{"The " + name + " is disabled."};
140 }
141}
142
143std::shared_ptr<Log> Log::getInstance()
144{
145 static const std::shared_ptr<Log> logger(::new Log{});
146 return logger;
147}
148
149// NOLINTBEGIN(cppcoreguidelines-avoid-goto)
150void Log::service()
151{
152retry:
153 try
154 {
155 logStyle();
156 processEvent(event: Relaunch{});
157
158 assert(currentState() == State::initial);
159 processEvent(event: OpenFile{});
160
161 assert(currentState() == State::active);
162 awaitNotification2Proceed();
163 processEvent(event: GoLogging{});
164
165 assert(currentState() == State::established);
166 notificationLoop();
167 if (inResetting.load())
168 {
169 goto retry;
170 }
171 processEvent(event: CloseFile{});
172
173 assert(currentState() == State::active);
174 processEvent(event: NoLogging{});
175
176 assert(currentState() == State::inactive);
177 }
178 catch (const std::exception& err)
179 {
180 LOG_ERR << "Suspend the " << name << " during " << static_cast<State>(currentState()) << " state. "
181 << err.what();
182
183 processEvent(event: Standby{});
184 if (awaitNotification2Retry())
185 {
186 goto retry;
187 }
188 }
189}
190// NOLINTEND(cppcoreguidelines-avoid-goto)
191
192void Log::flush(const OutputLevel severity, const std::string_view labelTpl, const std::string_view formatted)
193{
194 if (severity < priorityLevel)
195 {
196 return;
197 }
198
199 std::unique_lock<std::mutex> daemonLock(daemonMtx, std::defer_lock);
200 try
201 {
202 if (auto rows = reformatContents(
203 label: utility::common::formatString(
204 fmt: labelTpl,
205 args: isInServingState(state: State::established) ? (daemonLock.lock(), getPrefix(level: severity)) : traceLevelPrefix),
206 formatted);
207 daemonLock.owns_lock())
208 {
209 std::ranges::for_each(rows, [this](auto& output) { logQueue.push(std::move(output)); });
210 daemonLock.unlock();
211 daemonCond.notify_one();
212 }
213 else
214 {
215 cacheSwitch.lock();
216 std::ranges::for_each(
217 rows,
218 [this](auto& output)
219 {
220 unprocessedCache.emplace_front(output);
221 std::osyncstream(std::cerr) << changeToLogStyle(output) << std::endl;
222 });
223 cacheSwitch.unlock();
224 }
225 }
226 catch (...)
227 {
228 if (daemonLock.owns_lock())
229 {
230 daemonLock.unlock();
231 }
232 throw;
233 }
234}
235
236std::string Log::createLabelTemplate(const std::string_view srcFile, const std::uint32_t srcLine)
237{
238 return std::format(
239 fmt: "[{}] {{}} [{}#{}] ",
240 args: utility::time::currentStandardTime(),
241 args: (srcFile.rfind(str: sourceDirectory) != std::string_view::npos)
242 ? srcFile.substr(pos: srcFile.rfind(str: sourceDirectory) + sourceDirectory.length(), n: srcFile.length())
243 : srcFile,
244 args: srcLine);
245}
246
247std::string_view Log::getPrefix(const OutputLevel level)
248{
249 switch (level)
250 {
251 case OutputLevel::debug:
252 return debugLevelPrefix;
253 case OutputLevel::info:
254 return infoLevelPrefix;
255 case OutputLevel::warning:
256 return warningLevelPrefix;
257 case OutputLevel::error:
258 return errorLevelPrefix;
259 default:
260 break;
261 }
262 return traceLevelPrefix;
263}
264
265std::vector<std::string> Log::reformatContents(const std::string_view label, const std::string_view formatted)
266{
267 std::vector<std::string> rows{};
268 if (formatted.find(c: '\n') == std::string_view::npos)
269 {
270 rows.emplace_back(args: formatted);
271 }
272 else
273 {
274 std::size_t pos = 0;
275 std::size_t prev = 0;
276 while ((pos = formatted.find(c: '\n', pos: prev)) != std::string_view::npos)
277 {
278 rows.emplace_back(args: formatted.substr(pos: prev, n: pos - prev + 1));
279 prev += pos - prev + 1;
280 }
281 if (prev < formatted.length())
282 {
283 rows.emplace_back(args: formatted.substr(pos: prev));
284 }
285 }
286
287 auto reformatted =
288 rows
289 | std::views::transform(
290 [](auto& line)
291 {
292 line.erase(
293 std::ranges::remove_if(line, [](const auto c) { return (c == '\n') || (c == '\r'); }).begin(),
294 line.cend());
295 return line;
296 })
297 | std::views::filter([](const auto& line) { return !line.empty(); })
298 | std::views::transform([&label](const auto& line) { return label.data() + line; });
299 return std::vector<std::string>{std::ranges::begin(reformatted), std::ranges::end(reformatted)};
300}
301
302void Log::onPreviewing(const std::function<void(const std::string&)>& peeking) const
303{
304 const LockGuard guard(fileLock, LockMode::read);
305 if (peeking)
306 {
307 peeking(filePath);
308 }
309}
310
311void Log::syncWaitOr(const State state, const std::function<void()>& handling) const
312{
313 do
314 {
315 if (isInServingState(state: State::idle) && handling)
316 {
317 handling();
318 }
319 std::this_thread::yield();
320 }
321 while (!isInServingState(state));
322}
323
324void Log::syncNotifyVia(const std::function<void()>& action)
325{
326 std::unique_lock<std::mutex> daemonLock(daemonMtx);
327 if (action)
328 {
329 action();
330 }
331 daemonLock.unlock();
332 daemonCond.notify_one();
333}
334
335void Log::syncCountdownIf(const std::function<bool()>& condition, const std::function<void()>& handling) const
336{
337 if (!condition)
338 {
339 return;
340 }
341 for (const utility::time::Stopwatch timing{}; timing.elapsedTime() <= timeoutPeriod;)
342 {
343 if (!condition())
344 {
345 return;
346 }
347 std::this_thread::yield();
348 }
349 if (handling)
350 {
351 handling();
352 }
353}
354
355bool Log::isInServingState(const State state) const
356{
357 return (currentState() == state) && !inResetting.load();
358}
359
360std::string Log::getFullLogPath(const std::string_view filename)
361{
362 const char* const processHome = std::getenv(name: "FOO_HOME"); // NOLINT(concurrency-mt-unsafe)
363 if (!processHome)
364 {
365 throw std::runtime_error{"The environment variable FOO_HOME is not set."};
366 }
367 return std::string{processHome} + '/' + filename.data();
368}
369
370void Log::tryCreateLogFolder() const
371{
372 const LockGuard guard(fileLock, LockMode::write);
373 if (const auto logFolderPath = std::filesystem::absolute(p: filePath).parent_path();
374 !std::filesystem::exists(p: logFolderPath))
375 {
376 std::filesystem::create_directories(p: logFolderPath);
377 std::filesystem::permissions(
378 p: logFolderPath, prms: std::filesystem::perms::owner_all, opts: std::filesystem::perm_options::add);
379 }
380}
381
382void Log::backUpLogFileIfNeeded() const
383{
384 const LockGuard guard(fileLock, LockMode::write);
385 if (constexpr std::uint32_t maxFileSize = 512 * 1024;
386 !std::filesystem::is_regular_file(p: filePath) || (std::filesystem::file_size(p: filePath) < maxFileSize))
387 {
388 return;
389 }
390
391 const std::regex pattern(
392 std::regex_replace(
393 s: std::filesystem::path{filePath}.filename().string(), e: std::regex{R"([-[\]{}()*+?.,\^$|#\s])"}, fmt: R"(\$&)")
394 + R"(\.(\d+))");
395 auto transformed = std::filesystem::directory_iterator(std::filesystem::absolute(p: filePath).parent_path())
396 | std::views::transform(
397 [&pattern, match = std::smatch{}](const auto& entry) mutable
398 {
399 const auto filename = entry.path().filename().string();
400 return std::regex_match(filename, match, pattern) ? std::stoi(str: match[1].str()) : 0;
401 });
402 const int index = std::ranges::max(transformed);
403 std::filesystem::rename(from: filePath, to: filePath + '.' + std::to_string(val: index + 1));
404}
405
406void Log::tryClearLogFile() const
407{
408 const LockGuard guard(fileLock, LockMode::write);
409 std::ofstream tempOfs{};
410 tempOfs.open(s: filePath, mode: std::ios_base::out | std::ios_base::trunc);
411 tempOfs.close();
412}
413
414void Log::openLogFile()
415{
416 tryCreateLogFolder();
417 backUpLogFileIfNeeded();
418
419 const LockGuard guard(fileLock, LockMode::write);
420 switch (writeMode)
421 {
422 case OutputMode::append:
423 logWriter.open();
424 break;
425 case OutputMode::overwrite:
426 logWriter.open(overwrite: true);
427 break;
428 default:
429 break;
430 }
431}
432
433void Log::closeLogFile()
434{
435 const LockGuard guard(fileLock, LockMode::write);
436 logWriter.unlock();
437 logWriter.close();
438}
439
440void Log::startLogging()
441{
442 logWriter.lock();
443
444 cacheSwitch.lock();
445 while (!unprocessedCache.empty())
446 {
447 std::osyncstream(std::cout) << historyCacheBaseColor.data() + unprocessedCache.front() + ansi_sgr::reset.data()
448 << std::endl;
449 unprocessedCache.pop_front();
450 }
451 cacheSwitch.unlock();
452}
453
454void Log::stopLogging()
455{
456 const std::lock_guard<std::mutex> daemonLock(daemonMtx);
457 isOngoing.store(i: false);
458 inResetting.store(i: false);
459
460 while (!logQueue.empty())
461 {
462 logQueue.pop();
463 }
464}
465
466void Log::doToggle()
467{
468 utility::benchmark::escape(p: this);
469 utility::benchmark::clobber();
470}
471
472void Log::doRollback()
473{
474 const std::lock_guard<std::mutex> daemonLock(daemonMtx);
475 isOngoing.store(i: false);
476
477 while (!logQueue.empty())
478 {
479 logQueue.pop();
480 }
481 if (logWriter.isOpened())
482 {
483 const bool needClear = logWriter.isLocked();
484 closeLogFile();
485 if (needClear)
486 {
487 tryClearLogFile();
488 }
489 }
490
491 inResetting.store(i: false);
492}
493
494bool Log::isLogFileOpened(const GoLogging& /*event*/) const
495{
496 return logWriter.isOpened();
497}
498
499bool Log::isLogFileClosed(const NoLogging& /*event*/) const
500{
501 return !logWriter.isOpened();
502}
503
504void Log::notificationLoop()
505{
506 while (isOngoing.load())
507 {
508 std::unique_lock<std::mutex> daemonLock(daemonMtx);
509 daemonCond.wait(lock&: daemonLock, p: [this]() { return !isOngoing.load() || !logQueue.empty() || inResetting.load(); });
510 if (inResetting.load())
511 {
512 break;
513 }
514
515 const LockGuard guard(fileLock, LockMode::write);
516 while (!logQueue.empty())
517 {
518 switch (targetType)
519 {
520 case OutputType::file:
521 logWriter.stream() << logQueue.front() << std::endl;
522 break;
523 case OutputType::terminal:
524 std::osyncstream(std::cout) << changeToLogStyle(line&: logQueue.front()) << std::endl;
525 break;
526 case OutputType::all:
527 logWriter.stream() << logQueue.front() << std::endl;
528 std::osyncstream(std::cout) << changeToLogStyle(line&: logQueue.front()) << std::endl;
529 break;
530 default:
531 break;
532 }
533 logQueue.pop();
534 }
535 }
536}
537
538void Log::awaitNotification2Proceed()
539{
540 std::unique_lock<std::mutex> daemonLock(daemonMtx);
541 daemonCond.wait(lock&: daemonLock, p: [this]() { return isOngoing.load(); });
542}
543
544bool Log::awaitNotification2Retry()
545{
546 std::unique_lock<std::mutex> daemonLock(daemonMtx);
547 daemonCond.wait(lock&: daemonLock);
548 return inResetting.load();
549}
550
551template <Log::OutputLevel Lv>
552Holder<Lv>::~Holder()
553{
554 printfStyle(severity: Lv, srcFile: location.file_name(), srcLine: location.line(), format: buffer.str());
555}
556
557template class Holder<Log::OutputLevel::debug>;
558template class Holder<Log::OutputLevel::info>;
559template class Holder<Log::OutputLevel::warning>;
560template class Holder<Log::OutputLevel::error>;
561
562//! @brief The operator (<<) overloading of the State enum.
563//! @param os - output stream object
564//! @param state - current state
565//! @return reference of the output stream object
566std::ostream& operator<<(std::ostream& os, const Log::State state)
567{
568 using enum Log::State;
569 switch (state)
570 {
571 case initial:
572 os << "INITIAL";
573 break;
574 case active:
575 os << "ACTIVE";
576 break;
577 case established:
578 os << "ESTABLISHED";
579 break;
580 case inactive:
581 os << "INACTIVE";
582 break;
583 case idle:
584 os << "IDLE";
585 break;
586 default:
587 os << "UNKNOWN (" << static_cast<std::underlying_type_t<Log::State>>(state) << ')';
588 break;
589 }
590 return os;
591}
592
593//! @brief Change line string to log style.
594//! @param line - target line to be changed
595//! @return changed line
596std::string& changeToLogStyle(std::string& line)
597{
598 const auto& style = logStyle();
599 if (const auto segIter = std::ranges::find_if(
600 style.predefinedLevelPrefixes,
601 [&line](const auto& predefined) { return std::regex_search(line, std::get<std::regex>(predefined)); });
602 segIter != style.predefinedLevelPrefixes.cend())
603 {
604 line = std::regex_replace(s: line, e: std::get<std::regex>(p: *segIter), fmt: std::get<std::string>(p: *segIter));
605 }
606
607 for (std::smatch match{}; [[maybe_unused]] const auto& [segment, scheme] : style.identitySegments)
608 {
609 if (std::regex_search(s: line, m&: match, e: segment))
610 {
611 line = match.prefix().str() + scheme + match.str() + ansi_sgr::reset.data() + match.suffix().str();
612 }
613 }
614 return line;
615}
616
617namespace intf
618{
619//! @brief Preview in the log context.
620//! @param peeking - further handling for peeking
621void previewInContext(const std::function<void(const std::string&)>& peeking)
622{
623 Log::getInstance()->onPreviewing(peeking);
624}
625} // namespace intf
626} // namespace log
627
628//! @brief Wait for the logger to start.
629template <>
630void log::AccessController::startup() const
631try
632{
633 using Log = log::Log;
634 using State = Log::State;
635 srv->syncWaitOr(
636 state: State::active, handling: []() { throw std::runtime_error{"The " + Log::name + " did not set up successfully ..."}; });
637 srv->syncNotifyVia(action: [this]() { srv->isOngoing.store(i: true); });
638 srv->syncWaitOr(
639 state: State::established, handling: []() { throw std::runtime_error{"The " + Log::name + " did not start successfully ..."}; });
640}
641catch (const std::exception& err)
642{
643 LOG_ERR << err.what();
644}
645
646//! @brief Wait for the logger to stop.
647template <>
648void log::AccessController::shutdown() const
649try
650{
651 using Log = log::Log;
652 using State = Log::State;
653 srv->syncNotifyVia(action: [this]() { srv->isOngoing.store(i: false); });
654 srv->syncWaitOr(
655 state: State::inactive, handling: []() { throw std::runtime_error{"The " + Log::name + " did not stop successfully ..."}; });
656}
657catch (const std::exception& err)
658{
659 LOG_ERR << err.what();
660}
661
662//! @brief Request to reset the logger.
663template <>
664void log::AccessController::reload() const
665try
666{
667 using Log = log::Log;
668 srv->syncNotifyVia(action: [this]() { srv->inResetting.store(i: true); });
669 srv->syncCountdownIf(
670 condition: [this]() { return srv->inResetting.load(); },
671 handling: [this]()
672 {
673 throw std::runtime_error{
674 "The " + Log::name + " did not reset properly in " + std::to_string(val: srv->timeoutPeriod) + " ms ..."};
675 });
676}
677catch (const std::exception& err)
678{
679 LOG_ERR << err.what();
680}
681} // namespace application
682