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