Helios Engine 0.1.0
A modular ECS based data-oriented C++23 game engine
 
Loading...
Searching...
No Matches
logger.cpp
Go to the documentation of this file.
2
4
5#include <spdlog/logger.h>
6#include <spdlog/pattern_formatter.h>
7#include <spdlog/sinks/basic_file_sink.h>
8#include <spdlog/sinks/stdout_color_sinks.h>
9#include <spdlog/spdlog.h>
10
11#ifdef HELIOS_ENABLE_STACKTRACE
12#ifdef HELIOS_USE_STL_STACKTRACE
13#include <stacktrace>
14#else
15#include <boost/stacktrace.hpp>
16#endif
17#endif
18
19#include <algorithm>
20#include <array>
21#include <charconv>
22#include <chrono>
23#include <cstddef>
24#include <cstring>
25#include <ctime>
26#include <expected>
27#include <filesystem>
28#include <format>
29#include <iterator>
30#include <memory>
31#include <source_location>
32#include <string>
33#include <string_view>
34#include <system_error>
35#include <utility>
36#include <vector>
37
38namespace {
39
40constexpr size_t kFormatBufferReserveSize = 256;
41constexpr size_t kMaxStackTraceFrames = 10;
42constexpr size_t kStackTraceReserveSize = 512;
43
44[[nodiscard]] auto GenerateTimestamp() noexcept -> std::expected<std::string, std::string_view> {
45 try {
46 const auto now = std::chrono::system_clock::now();
47 const auto time_t_now = std::chrono::system_clock::to_time_t(now);
48
49 std::tm local_time{};
50
51 // Needed because of compatibility issues on Windows
52 // For more info: https://en.cppreference.com/w/c/chrono/localtime.html
53#ifdef HELIOS_PLATFORM_WINDOWS
54 const errno_t err = localtime_s(&local_time, &time_t_now);
55 if (err != 0) [[unlikely]] {
56 return std::unexpected("Failed to get local time");
57 }
58#else
59 const std::tm* const local_time_ptr = localtime_r(&time_t_now, &local_time);
60 if (local_time_ptr == nullptr) [[unlikely]] {
61 return std::unexpected("Failed to get local time");
62 }
63#endif
64
65 const int year = local_time.tm_year + 1900;
66 const int month = local_time.tm_mon + 1;
67 const int day = local_time.tm_mday;
68 const int hour = local_time.tm_hour;
69 const int min = local_time.tm_min;
70 const int sec = local_time.tm_sec;
71 return std::format("{:04d}-{:02d}-{:02d}_{:02d}-{:02d}-{:02d}", year, month, day, hour, min, sec);
72 } catch (...) {
73 return std::unexpected("Exception during timestamp generation");
74 }
75}
76
77class SourceLocationFormatterFlag final : public spdlog::custom_flag_formatter {
78public:
79 explicit SourceLocationFormatterFlag(spdlog::level::level_enum min_level) noexcept : min_level_(min_level) {}
80
81 void format(const spdlog::details::log_msg& msg, const std::tm& tm, spdlog::memory_buf_t& dest) override;
82
83 [[nodiscard]] auto clone() const -> std::unique_ptr<spdlog::custom_flag_formatter> override {
84 return std::make_unique<SourceLocationFormatterFlag>(min_level_);
85 }
86
87private:
88 spdlog::level::level_enum min_level_;
89};
90
91void SourceLocationFormatterFlag::format(const spdlog::details::log_msg& msg, [[maybe_unused]] const std::tm& tm,
92 spdlog::memory_buf_t& dest) {
93 // Only add source location if level is at or above minimum
94 if (msg.level < min_level_) [[likely]] {
95 return;
96 }
97
98 std::array<char, kFormatBufferReserveSize> buffer = {};
99
100 char* ptr = buffer.data();
101 char* const buffer_end = buffer.data() + buffer.size();
102
103 *ptr++ = ' ';
104 *ptr++ = '[';
105
106 if (msg.source.filename != nullptr) [[likely]] {
107 const std::string_view filename(msg.source.filename);
108 const auto remaining_space = static_cast<size_t>(std::distance(ptr, buffer_end));
109 const size_t copy_len = std::min(filename.size(), remaining_space - 20);
110 ptr = std::copy_n(filename.begin(), copy_len, ptr);
111 }
112
113 *ptr++ = ':';
114
115 // Use to_chars for safe formatting
116 const auto result = std::to_chars(ptr, buffer_end - 1, msg.source.line);
117 if (result.ec == std::errc{}) [[likely]] {
118 ptr = result.ptr;
119 } else {
120 *ptr++ = '?';
121 }
122
123 *ptr++ = ']';
124 dest.append(buffer.data(), ptr);
125}
126
127#ifdef HELIOS_ENABLE_STACKTRACE
128class StackTraceFormatterFlag final : public spdlog::custom_flag_formatter {
129public:
130 explicit StackTraceFormatterFlag(spdlog::level::level_enum min_level) : min_level_(min_level) {}
131
132 void format(const spdlog::details::log_msg& msg, const std::tm& tm, spdlog::memory_buf_t& dest) override;
133
134 [[nodiscard]] auto clone() const -> std::unique_ptr<spdlog::custom_flag_formatter> override {
135 return std::make_unique<StackTraceFormatterFlag>(min_level_);
136 }
137
138private:
139#ifdef HELIOS_USE_STL_STACKTRACE
140 [[nodiscard]] static size_t FindStartingFrame(const std::stacktrace& stack_trace,
141 const spdlog::source_loc& source_loc) noexcept;
142#else
143 [[nodiscard]] static size_t FindStartingFrame(const boost::stacktrace::stacktrace& stack_trace,
144 const spdlog::source_loc& source_loc) noexcept;
145#endif
146
147 [[nodiscard]] static bool IsExactSourceMatch(const std::string& stacktrace_entry,
148 const std::string& target_pattern) noexcept;
149
150 [[nodiscard]] static constexpr bool IsValidPrecedingChar(char ch) noexcept {
151 return ch == ' ' || ch == '\t' || ch == '/' || ch == '\\' || ch == ':';
152 }
153
154 [[nodiscard]] static constexpr bool IsValidFollowingChar(char ch) noexcept {
155 return ch == ' ' || ch == '\t' || ch == ')' || ch == '\n' || ch == '\r';
156 }
157
158 spdlog::level::level_enum min_level_;
159};
160
161void StackTraceFormatterFlag::format(const spdlog::details::log_msg& msg, [[maybe_unused]] const std::tm& tm,
162 spdlog::memory_buf_t& dest) {
163 // Only add stack trace if level is at or above minimum
164 if (msg.level < min_level_) [[likely]] {
165 return;
166 }
167
168 using namespace std::literals::string_view_literals;
169
170 try {
171#ifdef HELIOS_USE_STL_STACKTRACE
172 const std::stacktrace stack_trace = std::stacktrace::current();
173#else
174 const boost::stacktrace::stacktrace stack_trace;
175#endif
176
177 dest.append("\nStack trace:"sv);
178
179 if (stack_trace.size() <= 1) [[unlikely]] {
180 dest.append(" <empty>"sv);
181 return;
182 }
183
184 // Find the starting frame based on source location
185 const size_t start_frame = FindStartingFrame(stack_trace, msg.source);
186
187 const size_t frame_count = std::min(stack_trace.size(), start_frame + kMaxStackTraceFrames);
188
189 constexpr size_t kEntimatedFrameSize = 64;
190 dest.reserve(dest.size() + (frame_count * kEntimatedFrameSize));
191
192 // Print up to kMaxStackTraceFrames frames starting from start_frame
193 for (size_t i = start_frame, out_idx = 1; i < frame_count; ++i, ++out_idx) {
194 const auto& entry = stack_trace[i];
195
196#ifdef HELIOS_USE_STL_STACKTRACE
197 std::string entry_str = std::to_string(entry);
198#else
199 std::string entry_str = boost::stacktrace::to_string(entry);
200#endif
201 std::format_to(std::back_inserter(dest), "\n {}: {}", out_idx, std::move(entry_str));
202 }
203 } catch (...) {
204 dest.append("\nStack trace: <error>"sv);
205 }
206}
207
208#ifdef HELIOS_USE_STL_STACKTRACE
209size_t StackTraceFormatterFlag::FindStartingFrame(const std::stacktrace& stack_trace,
210 const spdlog::source_loc& source_loc) noexcept {
211#else
212size_t StackTraceFormatterFlag::FindStartingFrame(const boost::stacktrace::stacktrace& stack_trace,
213 const spdlog::source_loc& source_loc) noexcept {
214#endif
215 if (source_loc.filename == nullptr || source_loc.line <= 0) [[unlikely]] {
216 return 1; // Default: skip frame 0 (current function)
217 }
218
219 // Extract just the filename from the full path
220 const std::string_view filename = helios::utils::GetFileName(std::string_view(source_loc.filename));
221
222 std::string target_pattern;
223 std::string entry_str;
224
225 try {
226 target_pattern = std::format("{}:{}", filename, source_loc.line);
227 entry_str.reserve(kStackTraceReserveSize);
228 } catch (...) {
229 return 1; // Fallback if formatting fails
230 }
231
232 // Look for exact filename:line matches
233 for (size_t i = 1; i < stack_trace.size(); ++i) {
234 try {
235 entry_str.clear();
236#ifdef HELIOS_USE_STL_STACKTRACE
237 entry_str = std::to_string(stack_trace[i]);
238#else
239 entry_str = boost::stacktrace::to_string(stack_trace[i]);
240#endif
241 if (IsExactSourceMatch(entry_str, target_pattern)) [[unlikely]] {
242 return i;
243 }
244 } catch (...) {
245 // Skip frames that can't be converted to string
246 continue;
247 }
248 }
249
250 return 1; // Fallback: skip frame 0
251}
252
253bool StackTraceFormatterFlag::IsExactSourceMatch(const std::string& stacktrace_entry,
254 const std::string& target_pattern) noexcept {
255 try {
256 size_t pos = stacktrace_entry.find(target_pattern);
257 if (pos == std::string::npos) [[likely]] {
258 return false;
259 }
260
261 // Ensure we have an exact match by checking boundaries
262 while (pos != std::string::npos) {
263 const bool valid_start = (pos == 0) || IsValidPrecedingChar(stacktrace_entry[pos - 1]);
264 const size_t end_pos = pos + target_pattern.length();
265 const bool valid_end = (end_pos == stacktrace_entry.length()) || IsValidFollowingChar(stacktrace_entry[end_pos]);
266
267 if (valid_start && valid_end) [[unlikely]] {
268 return true;
269 }
270
271 // Continue searching for next occurrence
272 pos = stacktrace_entry.find(target_pattern, pos + 1);
273 }
274
275 return false;
276 } catch (...) {
277 return false;
278 }
279}
280
281#endif
282
283[[nodiscard]] std::string FormatLogFileName(std::string_view logger_name, std::string_view pattern) {
284 std::string result(pattern);
285
286 size_t pos = result.find("{name}");
287 if (pos != std::string::npos) {
288 result.replace(pos, 6, logger_name);
289 }
290
291 pos = result.find("{timestamp}");
292 if (pos != std::string::npos) {
293 const std::string timestamp = GenerateTimestamp().value_or("unknown_time");
294 result.replace(pos, 11, timestamp);
295 }
296
297 return result;
298}
299
300} // namespace
301
302namespace helios {
303
304void Logger::FlushAll() noexcept {
305 const std::shared_lock lock(loggers_mutex_);
306 for (const auto& [_, logger] : loggers_) {
307 if (logger) [[likely]] {
308 try {
309 logger->flush();
310 } catch (...) {
311 // Silently ignore flush errors
312 }
313 }
314 }
315}
316
317void Logger::FlushImpl(LoggerId logger_id) noexcept {
318 if (const auto logger = GetLogger(logger_id)) [[likely]] {
319 try {
320 logger->flush();
321 } catch (...) {
322 // Silently ignore flush errors
323 }
324 }
325}
326
327void Logger::SetLevel(LogLevel level) noexcept {
328 if (const auto logger = GetDefaultLogger()) [[likely]] {
329 logger->set_level(static_cast<spdlog::level::level_enum>(std::to_underlying(level)));
330 }
331}
332
333bool Logger::ShouldLog(LogLevel level) const noexcept {
334 if (const auto logger = GetDefaultLogger()) [[likely]] {
335 return logger->should_log(static_cast<spdlog::level::level_enum>(std::to_underlying(level)));
336 }
337 return false;
338}
339
340LogLevel Logger::GetLevel() const noexcept {
341 if (const auto logger = GetDefaultLogger()) [[likely]] {
342 return static_cast<LogLevel>(logger->level());
343 }
344 return LogLevel::kTrace;
345}
346
347void Logger::SetLevelImpl(LoggerId logger_id, LogLevel level) noexcept {
348 if (const auto logger = GetLogger(logger_id)) [[likely]] {
349 logger->set_level(static_cast<spdlog::level::level_enum>(std::to_underlying(level)));
350
351 const std::scoped_lock lock(loggers_mutex_);
352 logger_levels_[logger_id] = level;
353 }
354}
355
356bool Logger::ShouldLogImpl(LoggerId logger_id, LogLevel level) const noexcept {
357 {
358 const std::shared_lock lock(loggers_mutex_);
359 if (const auto it = logger_levels_.find(logger_id); it != logger_levels_.end()) {
360 return level >= it->second;
361 }
362 }
363
364 // Fallback to checking spdlog if not in cache
365 if (const auto logger = GetLogger(logger_id)) [[likely]] {
366 return logger->should_log(static_cast<spdlog::level::level_enum>(std::to_underlying(level)));
367 }
368 return false;
369}
370
371LogLevel Logger::GetLevelImpl(LoggerId logger_id) const noexcept {
372 {
373 const std::shared_lock lock(loggers_mutex_);
374 if (const auto it = logger_levels_.find(logger_id); it != logger_levels_.end()) {
375 return it->second;
376 }
377 }
378
379 // Fallback to checking spdlog if not in cache
380 if (const auto logger = GetLogger(logger_id)) [[likely]] {
381 return static_cast<LogLevel>(logger->level());
382 }
383 return LogLevel::kTrace;
384}
385
386std::shared_ptr<spdlog::logger> Logger::GetLogger(LoggerId logger_id) const noexcept {
387 const std::shared_lock lock(loggers_mutex_);
388 const auto it = loggers_.find(logger_id);
389 return it == loggers_.end() ? nullptr : it->second;
390}
391
392void Logger::LogMessageImpl(const std::shared_ptr<spdlog::logger>& logger, LogLevel level,
393 const std::source_location& loc, std::string_view message) noexcept {
394 if (!logger) [[unlikely]] {
395 return;
396 }
397
398 try {
399 const std::string_view filename = utils::GetFileName(std::string_view(loc.file_name()));
400 logger->log(spdlog::source_loc{filename.data(), static_cast<int>(loc.line()), loc.function_name()},
401 static_cast<spdlog::level::level_enum>(std::to_underlying(level)), message);
402 } catch (...) {
403 // Silently ignore logging errors
404 }
405}
406
407void Logger::LogAssertionFailureImpl(const std::shared_ptr<spdlog::logger>& logger, std::string_view condition,
408 const std::source_location& loc, std::string_view message) noexcept {
409 if (!logger) [[unlikely]] {
410 return;
411 }
412
413 try {
414 const std::string assertion_msg = std::format("Assertion failed: {} | {}", condition, message);
415 LogMessageImpl(logger, LogLevel::kCritical, loc, assertion_msg);
416 } catch (...) {
417 // Silently ignore logging errors
418 }
419}
420
421auto Logger::CreateLogger(std::string_view logger_name, const LoggerConfig& config) noexcept
422 -> std::shared_ptr<spdlog::logger> {
423 try {
424 std::vector<std::shared_ptr<spdlog::sinks::sink>> log_sinks;
425 log_sinks.reserve(2);
426
427 const auto source_loc_level =
428 static_cast<spdlog::level::level_enum>(std::to_underlying(config.source_location_level));
429 [[maybe_unused]] const auto stack_trace_level =
430 static_cast<spdlog::level::level_enum>(std::to_underlying(config.stack_trace_level));
431
432 // Create file sink if enabled
433 if (config.enable_file) {
434 // Create log directory if it doesn't exist
435 std::error_code ec;
436 std::filesystem::create_directories(config.log_directory, ec);
437 if (ec) {
438 // Failed to create directory, continue without file logging
439 } else {
440 // Format the log file name
441 const std::string log_file_name = FormatLogFileName(logger_name, config.file_name_pattern);
442 const std::filesystem::path log_file_path = config.log_directory / log_file_name;
443
444 auto file_formatter = std::make_unique<spdlog::pattern_formatter>();
445#ifdef HELIOS_ENABLE_STACKTRACE
446 file_formatter->add_flag<SourceLocationFormatterFlag>('*', source_loc_level)
447 .add_flag<StackTraceFormatterFlag>('#', stack_trace_level)
448 .set_pattern(config.file_pattern);
449#else
450 file_formatter->add_flag<SourceLocationFormatterFlag>('*', source_loc_level).set_pattern(config.file_pattern);
451#endif
452 auto file_sink =
453 std::make_shared<spdlog::sinks::basic_file_sink_mt>(log_file_path.string(), config.truncate_files);
454 file_sink->set_formatter(std::move(file_formatter));
455 log_sinks.push_back(std::move(file_sink));
456 }
457 }
458
459 // Create console sink if enabled
460 if (config.enable_console) {
461 auto console_formatter = std::make_unique<spdlog::pattern_formatter>();
462#ifdef HELIOS_ENABLE_STACKTRACE
463 console_formatter->add_flag<SourceLocationFormatterFlag>('*', source_loc_level)
464 .add_flag<StackTraceFormatterFlag>('#', stack_trace_level)
465 .set_pattern(config.console_pattern);
466#else
467 console_formatter->add_flag<SourceLocationFormatterFlag>('*', source_loc_level)
468 .set_pattern(config.console_pattern);
469#endif
470 auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
471 console_sink->set_formatter(std::move(console_formatter));
472 log_sinks.emplace_back(std::move(console_sink));
473 }
474
475 if (log_sinks.empty()) [[unlikely]] {
476 return nullptr;
477 }
478
479 auto logger = std::make_shared<spdlog::logger>(std::string(logger_name), std::make_move_iterator(log_sinks.begin()),
480 std::make_move_iterator(log_sinks.end()));
481 spdlog::register_logger(logger);
482 logger->set_level(spdlog::level::trace);
483 logger->flush_on(static_cast<spdlog::level::level_enum>(std::to_underlying(config.auto_flush_level)));
484 return logger;
485 } catch (...) {
486 return nullptr;
487 }
488}
489
490void Logger::DropLoggerFromSpdlog(const std::shared_ptr<spdlog::logger>& logger) noexcept {
491 if (!logger) [[unlikely]] {
492 return;
493 }
494
495 try {
496 spdlog::drop(logger->name());
497 } catch (...) {
498 // Silently ignore drop errors
499 }
500}
501
502} // namespace helios
503
504// MSVC-specific definition for assertion logging integration
505// On GCC/Clang, the inline definition in logger.hpp takes precedence over the weak symbol in assert.cpp
506#if defined(_MSC_VER)
507namespace helios::details {
508
509void LogAssertionFailureViaLogger(std::string_view condition, const std::source_location& loc,
510 std::string_view message) noexcept {
511 Logger::GetInstance().LogAssertionFailure(condition, loc, message);
512}
513
514} // namespace helios::details
515#endif
SourceLocationFormatterFlag(spdlog::level::level_enum min_level) noexcept
Definition logger.cpp:79
auto clone() const -> std::unique_ptr< spdlog::custom_flag_formatter > override
Definition logger.cpp:83
void format(const spdlog::details::log_msg &msg, const std::tm &tm, spdlog::memory_buf_t &dest) override
Definition logger.cpp:91
void SetLevel(T logger, LogLevel level) noexcept
Sets the minimum log level for a typed logger.
Definition logger.hpp:382
static Logger & GetInstance() noexcept
Gets the singleton instance.
Definition logger.hpp:447
void FlushAll() noexcept
Flushes all registered loggers.
Definition logger.cpp:304
void LogAssertionFailure(T logger, std::string_view condition, const std::source_location &loc, std::string_view message) noexcept
Logs assertion failure with typed logger.
Definition logger.hpp:595
bool ShouldLog(LogLevel level) const noexcept
Checks if the default logger should log messages at the given level.
Definition logger.cpp:333
LogLevel GetLevel() const noexcept
Gets the current log level for the default logger.
Definition logger.cpp:340
constexpr size_t kFormatBufferReserveSize
Definition logger.cpp:40
constexpr size_t kMaxStackTraceFrames
Definition logger.cpp:41
auto GenerateTimestamp() noexcept -> std::expected< std::string, std::string_view >
Definition logger.cpp:44
constexpr size_t kStackTraceReserveSize
Definition logger.cpp:42
std::string FormatLogFileName(std::string_view logger_name, std::string_view pattern)
Definition logger.cpp:283
void LogAssertionFailureViaLogger(std::string_view condition, const std::source_location &loc, std::string_view message) noexcept
Bridge to logger-provided assertion logging.
Definition logger.hpp:649
constexpr std::string_view GetFileName(std::string_view path)
Extracts the file name from a given path.
LogLevel
Log severity levels.
Definition logger.hpp:34
size_t LoggerId
Type alias for logger type IDs.
Definition logger.hpp:106
STL namespace.