Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
logger.cpp
Go to the documentation of this file.
1#include <pch.hpp>
2
4
5#include <helios/assert.hpp>
8
9#include <spdlog/async.h>
10#include <spdlog/logger.h>
11#include <spdlog/pattern_formatter.h>
12#include <spdlog/sinks/basic_file_sink.h>
13#include <spdlog/sinks/stdout_color_sinks.h>
14#include <spdlog/spdlog.h>
15
16#include <array>
17#include <chrono>
18#include <cstddef>
19#include <ctime>
20#include <expected>
21#include <filesystem>
22#include <format>
23#include <iterator>
24#include <memory>
25#include <mutex>
26#include <string>
27#include <string_view>
28#include <system_error>
29#include <vector>
30
31using Timestamp = helios::container::StaticString<19>; // YYYY-MM-DD_HH-MM-SS
32
33namespace {
34
35[[nodiscard]] auto GenerateTimestamp() noexcept
36 -> std::expected<Timestamp, std::string_view> {
37 try {
38 const auto now = std::chrono::system_clock::now();
39 const auto time_t_now = std::chrono::system_clock::to_time_t(now);
40
41 std::tm local_time{};
42
43 // Needed because of compatibility issues on Windows
44 // For more info: https://en.cppreference.com/w/c/chrono/localtime.html
45#ifdef HELIOS_PLATFORM_WINDOWS
46 const errno_t err = localtime_s(&local_time, &time_t_now);
47 if (err != 0) [[unlikely]] {
48 return std::unexpected("Failed to get local time");
49 }
50#else
51 const std::tm* const local_time_ptr = localtime_r(&time_t_now, &local_time);
52 if (local_time_ptr == nullptr) [[unlikely]] {
53 return std::unexpected("Failed to get local time");
54 }
55#endif
56
57 const int year = local_time.tm_year + 1900;
58 const int month = local_time.tm_mon + 1;
59 const int day = local_time.tm_mday;
60 const int hour = local_time.tm_hour;
61 const int min = local_time.tm_min;
62 const int sec = local_time.tm_sec;
63
64 std::array<char, 20> time_buf = {};
65 const auto result = std::format_to(
66 time_buf.begin(), "{:04d}-{:02d}-{:02d}_{:02d}-{:02d}-{:02d}", year,
67 month, day, hour, min, sec);
68 const auto len =
69 static_cast<size_t>(std::distance(time_buf.begin(), result));
70 return Timestamp(std::string_view(time_buf.data(), len));
71 } catch (...) {
72 return std::unexpected("Exception during timestamp generation");
73 }
74}
75
76[[nodiscard]] std::string FormatLogFileName(std::string_view logger_name,
77 std::string_view pattern) {
78 std::string result(pattern);
79
80 size_t pos = result.find("{name}");
81 if (pos != std::string::npos) {
82 result.replace(pos, 6, logger_name);
83 }
84
85 pos = result.find("{timestamp}");
86 if (pos != std::string::npos) {
87 const Timestamp timestamp = GenerateTimestamp().value_or("unknown_time");
88 result.replace(pos, 11, timestamp.View());
89 }
90
91 return result;
92}
93
94} // namespace
95
96namespace helios::log {
97
98void Logger::FlushAll() noexcept {
99 const std::shared_lock lock(loggers_mutex_);
100 for (const auto& [_, logger] : loggers_) {
101 if (logger) [[likely]] {
102 try {
103 logger->flush();
104 } catch (...) {
105 // Silently ignore flush errors
106 }
107 }
108 }
109}
110
111void Logger::FlushImpl(LoggerTypeIndex logger_index) noexcept {
112 if (const auto logger = GetLogger(logger_index)) [[likely]] {
113 try {
114 logger->flush();
115 } catch (...) {
116 // Silently ignore flush errors
117 }
118 }
119}
120
121void Logger::SetLevel(Level level) noexcept {
122 if (const auto logger = GetDefaultLogger()) [[likely]] {
123 logger->set_level(
124 static_cast<spdlog::level::level_enum>(std::to_underlying(level)));
125 }
126}
127
128bool Logger::ShouldLog(Level level) const noexcept {
129 if (const auto logger = GetDefaultLogger()) [[likely]] {
130 return logger->should_log(
131 static_cast<spdlog::level::level_enum>(std::to_underlying(level)));
132 }
133 return false;
134}
135
136Level Logger::GetLevel() const noexcept {
137 if (const auto logger = GetDefaultLogger()) [[likely]] {
138 return static_cast<Level>(logger->level());
139 }
140 return Level::kTrace;
141}
142
143void Logger::SetLevelImpl(LoggerTypeIndex logger_index, Level level) noexcept {
144 if (const auto logger = GetLogger(logger_index)) [[likely]] {
145 logger->set_level(
146 static_cast<spdlog::level::level_enum>(std::to_underlying(level)));
147
148 const std::scoped_lock lock(loggers_mutex_);
149 logger_levels_[logger_index] = level;
150 }
151}
152
153bool Logger::ShouldLogImpl(LoggerTypeIndex logger_index,
154 Level level) const noexcept {
155 {
156 const std::shared_lock lock(loggers_mutex_);
157 if (const auto it = logger_levels_.find(logger_index);
158 it != logger_levels_.end()) {
159 return level >= it->second;
160 }
161 }
162
163 // Fallback to checking spdlog if not in cache
164 if (const auto logger = GetLogger(logger_index)) [[likely]] {
165 return logger->should_log(
166 static_cast<spdlog::level::level_enum>(std::to_underlying(level)));
167 }
168 return false;
169}
170
171Level Logger::GetLevelImpl(LoggerTypeIndex logger_index) const noexcept {
172 {
173 const std::shared_lock lock(loggers_mutex_);
174 if (const auto it = logger_levels_.find(logger_index);
175 it != logger_levels_.end()) {
176 return it->second;
177 }
178 }
179
180 // Fallback to checking spdlog if not in cache
181 if (const auto logger = GetLogger(logger_index)) [[likely]] {
182 return static_cast<Level>(logger->level());
183 }
184 return Level::kTrace;
185}
186
187auto Logger::GetLogger(LoggerTypeIndex logger_index) const noexcept
188 -> std::shared_ptr<spdlog::logger> {
189 const std::shared_lock lock(loggers_mutex_);
190 const auto it = loggers_.find(logger_index);
191 return it == loggers_.end() ? nullptr : it->second;
192}
193
194void Logger::LogMessageImpl(const std::shared_ptr<spdlog::logger>& logger,
195 Level level, std::string_view message) noexcept {
196 if (!logger) [[unlikely]] {
197 return;
198 }
199
200 try {
201 logger->log(
202 static_cast<spdlog::level::level_enum>(std::to_underlying(level)),
203 message);
204 } catch (...) {
205 // Silently ignore logging errors
206 }
207}
208
209auto Logger::CreateLogger(std::string_view logger_name,
210 const Config& config) noexcept
211 -> std::shared_ptr<spdlog::logger> {
212 try {
213 std::vector<std::shared_ptr<spdlog::sinks::sink>> log_sinks;
214 log_sinks.reserve(2);
215
216 // Create file sink if enabled
217 if (config.enable_file) {
218 // Create log directory if it doesn't exist
219 std::error_code ec;
220 std::filesystem::create_directories(config.log_directory, ec);
221 if (ec) {
222 // Failed to create directory, continue without file logging
223 } else {
224 // Format the log file name
225 const std::string log_file_name =
226 FormatLogFileName(logger_name, config.file_name_pattern);
227 std::filesystem::path log_file_path(config.log_directory);
228 log_file_path /= log_file_name;
229
230 auto file_formatter = std::make_unique<spdlog::pattern_formatter>();
231 file_formatter->set_pattern(std::string(config.file_pattern));
232 auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
233 log_file_path.string(), config.truncate_files);
234 file_sink->set_formatter(std::move(file_formatter));
235 log_sinks.push_back(std::move(file_sink));
236 }
237 }
238
239 // Create console sink if enabled
240 if (config.enable_console) {
241 auto console_formatter = std::make_unique<spdlog::pattern_formatter>();
242 console_formatter->set_pattern(std::string(config.console_pattern));
243 auto console_sink =
244 std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
245 console_sink->set_formatter(std::move(console_formatter));
246 log_sinks.emplace_back(std::move(console_sink));
247 }
248
249 if (log_sinks.empty()) [[unlikely]] {
250 return nullptr;
251 }
252
253 std::shared_ptr<spdlog::logger> logger;
254 if (config.async_logging) {
255 // Initialize spdlog's global thread pool once per process
256 static std::once_flag s_init_flag;
257 std::call_once(s_init_flag, [&] {
258 spdlog::init_thread_pool(config.async_queue_size,
259 config.async_thread_count);
260 });
261
262 logger = std::make_shared<spdlog::async_logger>(
263 std::string(logger_name), std::make_move_iterator(log_sinks.begin()),
264 std::make_move_iterator(log_sinks.end()), spdlog::thread_pool(),
265 spdlog::async_overflow_policy::block);
266 } else {
267 logger = std::make_shared<spdlog::logger>(
268 std::string(logger_name), std::make_move_iterator(log_sinks.begin()),
269 std::make_move_iterator(log_sinks.end()));
270 }
271
272 spdlog::register_logger(logger);
273 logger->set_level(spdlog::level::trace);
274 logger->flush_on(static_cast<spdlog::level::level_enum>(
275 std::to_underlying(config.auto_flush_level)));
276 return logger;
277 } catch (...) {
278 return nullptr;
279 }
280}
281
282void Logger::DropLoggerFromSpdlog(
283 const std::shared_ptr<spdlog::logger>& logger) noexcept {
284 if (!logger) [[unlikely]] {
285 return;
286 }
287
288 try {
289 spdlog::drop(logger->name());
290 } catch (...) {
291 // Silently ignore drop errors
292 }
293}
294
295namespace details {
296
297void LogAssertionViaLogger(std::string_view condition,
298 const std::source_location& loc,
299 std::string_view message) noexcept {
300 const std::string formatted =
301 helios::details::FormatAssertionMessage(condition, loc, message);
303}
304
305} // namespace details
306
307} // namespace helios::log
308
309// These functions override the weak symbols defined in assert.cpp to provide
310// logging integration for assertions when the log plugin is linked.
311
312namespace helios::details {
313
314#ifdef _MSC_VER
315// MSVC: Register the handler at static initialization time
316namespace {
317
318void MsvcLogPluginHandler(std::string_view condition,
319 const std::source_location& loc,
320 std::string_view message) noexcept {
321 helios::log::details::LogAssertionViaLogger(condition, loc, message);
322}
323
324struct LogPluginHandlerRegistrar {
325 LogPluginHandlerRegistrar() noexcept {
326 // Register our handler with the assert system
327 SetLogPluginHandler(&MsvcLogPluginHandler);
328 }
329};
330
331// Static instance to register handler at startup
332[[maybe_unused]] static LogPluginHandlerRegistrar g_log_plugin_registrar{};
333
334} // namespace
335
336#else
337// GCC/Clang: Override the weak symbols from assert.cpp
338
339/// @brief Override weak symbol to indicate log plugin is available
340bool HasLogPluginHandler() noexcept {
341 return true;
342}
343
344/// @brief Override weak symbol to provide log plugin assertion handling.
345void LogPluginAssertionHandler(std::string_view condition,
346 const std::source_location& loc,
347 std::string_view message) noexcept {
348 helios::log::details::LogAssertionViaLogger(condition, loc, message);
349}
350
351#endif // _MSC_VER
352
353} // namespace helios::details
constexpr std::basic_string_view< CharT, Traits > View() const noexcept
Returns a std::basic_string_view of the content.
Level GetLevel() const noexcept
Gets the current log level for the default logger.
Definition logger.cpp:136
static Logger & Instance() noexcept
Gets the singleton instance.
Definition logger.hpp:53
void Log(T logger, Level level, std::string_view message) noexcept
Logs a string message with typed logger.
Definition logger.hpp:318
void FlushAll() noexcept
Flushes all registered loggers.
Definition logger.cpp:98
void SetLevel(T, Level level) noexcept
Sets the minimum log level for a typed logger.
Definition logger.hpp:147
bool ShouldLog(Level level) const noexcept
Checks if the default logger should log messages at the given level.
Definition logger.cpp:128
helios::container::StaticString< 19 > Timestamp
Definition logger.cpp:31
auto GenerateTimestamp() noexcept -> std::expected< Timestamp, std::string_view >
Definition logger.cpp:35
std::string FormatLogFileName(std::string_view logger_name, std::string_view pattern)
Definition logger.cpp:76
BasicStaticString< N, char > StaticString
Fixed-capacity string of char.
bool HasLogPluginHandler() noexcept
Log plugin assertion handler (weak symbol).
Definition assert.cpp:116
std::string FormatAssertionMessage(std::string_view condition, const std::source_location &loc, std::string_view message)
Formats a complete assertion failure message with source location and stack trace.
Definition assert.cpp:37
void LogPluginAssertionHandler(std::string_view condition, const std::source_location &loc, std::string_view message) noexcept
Weak symbol for log plugin assertion handler.
Definition assert.cpp:123
void LogAssertionViaLogger(std::string_view condition, const std::source_location &loc, std::string_view message) noexcept
Log plugin assertion handler implementation.
Definition logger.cpp:297
utils::TypeIndex LoggerTypeIndex
Type alias for logger type indices.
Definition config.hpp:98
Level
Log severity levels.
Definition config.hpp:14
STL namespace.
Configuration for logger behavior and output.
Definition config.hpp:24