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