aboutsummaryrefslogtreecommitdiff
path: root/src/zencore/logging
diff options
context:
space:
mode:
authorLiam Mitchell <[email protected]>2026-03-09 19:06:36 -0700
committerLiam Mitchell <[email protected]>2026-03-09 19:06:36 -0700
commitd1abc50ee9d4fb72efc646e17decafea741caa34 (patch)
treee4288e00f2f7ca0391b83d986efcb69d3ba66a83 /src/zencore/logging
parentAllow requests with invalid content-types unless specified in command line or... (diff)
parentupdated chunk–block analyser (#818) (diff)
downloadzen-d1abc50ee9d4fb72efc646e17decafea741caa34.tar.xz
zen-d1abc50ee9d4fb72efc646e17decafea741caa34.zip
Merge branch 'main' into lm/restrict-content-type
Diffstat (limited to 'src/zencore/logging')
-rw-r--r--src/zencore/logging/ansicolorsink.cpp273
-rw-r--r--src/zencore/logging/asyncsink.cpp212
-rw-r--r--src/zencore/logging/logger.cpp142
-rw-r--r--src/zencore/logging/msvcsink.cpp80
-rw-r--r--src/zencore/logging/registry.cpp330
-rw-r--r--src/zencore/logging/tracesink.cpp92
6 files changed, 1129 insertions, 0 deletions
diff --git a/src/zencore/logging/ansicolorsink.cpp b/src/zencore/logging/ansicolorsink.cpp
new file mode 100644
index 000000000..540d22359
--- /dev/null
+++ b/src/zencore/logging/ansicolorsink.cpp
@@ -0,0 +1,273 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/logging/ansicolorsink.h>
+#include <zencore/logging/helpers.h>
+#include <zencore/logging/messageonlyformatter.h>
+
+#include <cstdio>
+#include <cstdlib>
+#include <mutex>
+
+#if defined(_WIN32)
+# include <io.h>
+# define ZEN_ISATTY _isatty
+# define ZEN_FILENO _fileno
+#else
+# include <unistd.h>
+# define ZEN_ISATTY isatty
+# define ZEN_FILENO fileno
+#endif
+
+namespace zen::logging {
+
+// Default formatter replicating spdlog's %+ pattern:
+// [YYYY-MM-DD HH:MM:SS.mmm] [logger_name] [level] message\n
+class DefaultConsoleFormatter : public Formatter
+{
+public:
+ void Format(const LogMessage& Msg, MemoryBuffer& Dest) override
+ {
+ // timestamp
+ auto Secs = std::chrono::duration_cast<std::chrono::seconds>(Msg.GetTime().time_since_epoch());
+ if (Secs != m_LastLogSecs)
+ {
+ m_LastLogSecs = Secs;
+ m_CachedLocalTm = helpers::SafeLocaltime(LogClock::to_time_t(Msg.GetTime()));
+ }
+
+ Dest.push_back('[');
+ helpers::AppendInt(m_CachedLocalTm.tm_year + 1900, Dest);
+ Dest.push_back('-');
+ helpers::Pad2(m_CachedLocalTm.tm_mon + 1, Dest);
+ Dest.push_back('-');
+ helpers::Pad2(m_CachedLocalTm.tm_mday, Dest);
+ Dest.push_back(' ');
+ helpers::Pad2(m_CachedLocalTm.tm_hour, Dest);
+ Dest.push_back(':');
+ helpers::Pad2(m_CachedLocalTm.tm_min, Dest);
+ Dest.push_back(':');
+ helpers::Pad2(m_CachedLocalTm.tm_sec, Dest);
+ Dest.push_back('.');
+ auto Millis = helpers::TimeFraction<std::chrono::milliseconds>(Msg.GetTime());
+ helpers::Pad3(static_cast<uint32_t>(Millis.count()), Dest);
+ Dest.push_back(']');
+ Dest.push_back(' ');
+
+ // logger name
+ if (Msg.GetLoggerName().size() > 0)
+ {
+ Dest.push_back('[');
+ helpers::AppendStringView(Msg.GetLoggerName(), Dest);
+ Dest.push_back(']');
+ Dest.push_back(' ');
+ }
+
+ // level (colored range)
+ Dest.push_back('[');
+ Msg.ColorRangeStart = Dest.size();
+ helpers::AppendStringView(helpers::LevelToShortString(Msg.GetLevel()), Dest);
+ Msg.ColorRangeEnd = Dest.size();
+ Dest.push_back(']');
+ Dest.push_back(' ');
+
+ // message
+ helpers::AppendStringView(Msg.GetPayload(), Dest);
+ Dest.push_back('\n');
+ }
+
+ std::unique_ptr<Formatter> Clone() const override { return std::make_unique<DefaultConsoleFormatter>(); }
+
+private:
+ std::chrono::seconds m_LastLogSecs{0};
+ std::tm m_CachedLocalTm{};
+};
+
+static constexpr std::string_view s_Reset = "\033[m";
+
+static std::string_view
+GetColorForLevel(LogLevel InLevel)
+{
+ using namespace std::string_view_literals;
+ switch (InLevel)
+ {
+ case Trace:
+ return "\033[37m"sv; // white
+ case Debug:
+ return "\033[36m"sv; // cyan
+ case Info:
+ return "\033[32m"sv; // green
+ case Warn:
+ return "\033[33m\033[1m"sv; // bold yellow
+ case Err:
+ return "\033[31m\033[1m"sv; // bold red
+ case Critical:
+ return "\033[1m\033[41m"sv; // bold on red background
+ default:
+ return s_Reset;
+ }
+}
+
+struct AnsiColorStdoutSink::Impl
+{
+ explicit Impl(ColorMode Mode) : m_Formatter(std::make_unique<DefaultConsoleFormatter>()), m_UseColor(ResolveColorMode(Mode)) {}
+
+ static bool IsColorTerminal()
+ {
+ // If stdout is not a TTY, no color
+ if (ZEN_ISATTY(ZEN_FILENO(stdout)) == 0)
+ {
+ return false;
+ }
+
+ // NO_COLOR convention (https://no-color.org/)
+ if (std::getenv("NO_COLOR") != nullptr)
+ {
+ return false;
+ }
+
+ // COLORTERM is set by terminals that support color (e.g. "truecolor", "24bit")
+ if (std::getenv("COLORTERM") != nullptr)
+ {
+ return true;
+ }
+
+ // Check TERM for known color-capable values
+ const char* Term = std::getenv("TERM");
+ if (Term != nullptr)
+ {
+ std::string_view TermView(Term);
+ // "dumb" terminals do not support color
+ if (TermView == "dumb")
+ {
+ return false;
+ }
+ // Match against known color-capable terminal types.
+ // TERM often includes suffixes like "-256color", so we use substring matching.
+ constexpr std::string_view ColorTerms[] = {
+ "alacritty",
+ "ansi",
+ "color",
+ "console",
+ "cygwin",
+ "gnome",
+ "konsole",
+ "kterm",
+ "linux",
+ "msys",
+ "putty",
+ "rxvt",
+ "screen",
+ "tmux",
+ "vt100",
+ "vt102",
+ "xterm",
+ };
+ for (std::string_view Candidate : ColorTerms)
+ {
+ if (TermView.find(Candidate) != std::string_view::npos)
+ {
+ return true;
+ }
+ }
+ }
+
+#if defined(_WIN32)
+ // Windows console supports ANSI color by default in modern versions
+ return true;
+#else
+ // Unknown terminal — be conservative
+ return false;
+#endif
+ }
+
+ static bool ResolveColorMode(ColorMode Mode)
+ {
+ switch (Mode)
+ {
+ case ColorMode::On:
+ return true;
+ case ColorMode::Off:
+ return false;
+ case ColorMode::Auto:
+ default:
+ return IsColorTerminal();
+ }
+ }
+
+ void Log(const LogMessage& Msg)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+
+ MemoryBuffer Formatted;
+ m_Formatter->Format(Msg, Formatted);
+
+ if (m_UseColor && Msg.ColorRangeEnd > Msg.ColorRangeStart)
+ {
+ // Print pre-color range
+ fwrite(Formatted.data(), 1, Msg.ColorRangeStart, m_File);
+
+ // Print color
+ std::string_view Color = GetColorForLevel(Msg.GetLevel());
+ fwrite(Color.data(), 1, Color.size(), m_File);
+
+ // Print colored range
+ fwrite(Formatted.data() + Msg.ColorRangeStart, 1, Msg.ColorRangeEnd - Msg.ColorRangeStart, m_File);
+
+ // Reset color
+ fwrite(s_Reset.data(), 1, s_Reset.size(), m_File);
+
+ // Print remainder
+ fwrite(Formatted.data() + Msg.ColorRangeEnd, 1, Formatted.size() - Msg.ColorRangeEnd, m_File);
+ }
+ else
+ {
+ fwrite(Formatted.data(), 1, Formatted.size(), m_File);
+ }
+
+ fflush(m_File);
+ }
+
+ void Flush()
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ fflush(m_File);
+ }
+
+ void SetFormatter(std::unique_ptr<Formatter> InFormatter)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ m_Formatter = std::move(InFormatter);
+ }
+
+private:
+ std::mutex m_Mutex;
+ std::unique_ptr<Formatter> m_Formatter;
+ FILE* m_File = stdout;
+ bool m_UseColor = true;
+};
+
+AnsiColorStdoutSink::AnsiColorStdoutSink(ColorMode Mode) : m_Impl(std::make_unique<Impl>(Mode))
+{
+}
+
+AnsiColorStdoutSink::~AnsiColorStdoutSink() = default;
+
+void
+AnsiColorStdoutSink::Log(const LogMessage& Msg)
+{
+ m_Impl->Log(Msg);
+}
+
+void
+AnsiColorStdoutSink::Flush()
+{
+ m_Impl->Flush();
+}
+
+void
+AnsiColorStdoutSink::SetFormatter(std::unique_ptr<Formatter> InFormatter)
+{
+ m_Impl->SetFormatter(std::move(InFormatter));
+}
+
+} // namespace zen::logging
diff --git a/src/zencore/logging/asyncsink.cpp b/src/zencore/logging/asyncsink.cpp
new file mode 100644
index 000000000..02bf9f3ba
--- /dev/null
+++ b/src/zencore/logging/asyncsink.cpp
@@ -0,0 +1,212 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/logging/asyncsink.h>
+
+#include <zencore/blockingqueue.h>
+#include <zencore/logging/logmsg.h>
+#include <zencore/thread.h>
+
+#include <future>
+#include <string>
+#include <thread>
+
+namespace zen::logging {
+
+struct AsyncLogMessage
+{
+ enum class Type : uint8_t
+ {
+ Log,
+ Flush,
+ Shutdown
+ };
+
+ Type MsgType = Type::Log;
+
+ // Points to the LogPoint from upstream logging code. LogMessage guarantees
+ // this is always valid (either a static LogPoint from ZEN_LOG macros or one
+ // of the per-level default LogPoints).
+ const LogPoint* Point = nullptr;
+
+ int ThreadId = 0;
+ std::string OwnedPayload;
+ std::string OwnedLoggerName;
+ std::chrono::system_clock::time_point Time;
+
+ std::shared_ptr<std::promise<void>> FlushPromise;
+};
+
+struct AsyncSink::Impl
+{
+ explicit Impl(std::vector<SinkPtr> InSinks) : m_Sinks(std::move(InSinks))
+ {
+ m_WorkerThread = std::thread([this]() {
+ zen::SetCurrentThreadName("AsyncLog");
+ WorkerLoop();
+ });
+ }
+
+ ~Impl()
+ {
+ AsyncLogMessage ShutdownMsg;
+ ShutdownMsg.MsgType = AsyncLogMessage::Type::Shutdown;
+ m_Queue.Enqueue(std::move(ShutdownMsg));
+
+ if (m_WorkerThread.joinable())
+ {
+ m_WorkerThread.join();
+ }
+ }
+
+ void Log(const LogMessage& Msg)
+ {
+ AsyncLogMessage AsyncMsg;
+ AsyncMsg.OwnedPayload = std::string(Msg.GetPayload());
+ AsyncMsg.OwnedLoggerName = std::string(Msg.GetLoggerName());
+ AsyncMsg.ThreadId = Msg.GetThreadId();
+ AsyncMsg.Time = Msg.GetTime();
+ AsyncMsg.Point = &Msg.GetLogPoint();
+ AsyncMsg.MsgType = AsyncLogMessage::Type::Log;
+
+ m_Queue.Enqueue(std::move(AsyncMsg));
+ }
+
+ void Flush()
+ {
+ auto Promise = std::make_shared<std::promise<void>>();
+ auto Future = Promise->get_future();
+
+ AsyncLogMessage FlushMsg;
+ FlushMsg.MsgType = AsyncLogMessage::Type::Flush;
+ FlushMsg.FlushPromise = std::move(Promise);
+
+ m_Queue.Enqueue(std::move(FlushMsg));
+
+ Future.get();
+ }
+
+ void SetFormatter(std::unique_ptr<Formatter> InFormatter)
+ {
+ for (auto& CurrentSink : m_Sinks)
+ {
+ CurrentSink->SetFormatter(InFormatter->Clone());
+ }
+ }
+
+private:
+ void ForwardLogToSinks(const AsyncLogMessage& AsyncMsg)
+ {
+ LogMessage Reconstructed(*AsyncMsg.Point, AsyncMsg.OwnedLoggerName, AsyncMsg.OwnedPayload);
+ Reconstructed.SetTime(AsyncMsg.Time);
+ Reconstructed.SetThreadId(AsyncMsg.ThreadId);
+
+ for (auto& CurrentSink : m_Sinks)
+ {
+ if (CurrentSink->ShouldLog(Reconstructed.GetLevel()))
+ {
+ try
+ {
+ CurrentSink->Log(Reconstructed);
+ }
+ catch (const std::exception&)
+ {
+ }
+ }
+ }
+ }
+
+ void FlushSinks()
+ {
+ for (auto& CurrentSink : m_Sinks)
+ {
+ try
+ {
+ CurrentSink->Flush();
+ }
+ catch (const std::exception&)
+ {
+ }
+ }
+ }
+
+ void WorkerLoop()
+ {
+ AsyncLogMessage Msg;
+ while (m_Queue.WaitAndDequeue(Msg))
+ {
+ switch (Msg.MsgType)
+ {
+ case AsyncLogMessage::Type::Log:
+ {
+ ForwardLogToSinks(Msg);
+ break;
+ }
+
+ case AsyncLogMessage::Type::Flush:
+ {
+ FlushSinks();
+ if (Msg.FlushPromise)
+ {
+ Msg.FlushPromise->set_value();
+ }
+ break;
+ }
+
+ case AsyncLogMessage::Type::Shutdown:
+ {
+ m_Queue.CompleteAdding();
+
+ AsyncLogMessage Remaining;
+ while (m_Queue.WaitAndDequeue(Remaining))
+ {
+ if (Remaining.MsgType == AsyncLogMessage::Type::Log)
+ {
+ ForwardLogToSinks(Remaining);
+ }
+ else if (Remaining.MsgType == AsyncLogMessage::Type::Flush)
+ {
+ FlushSinks();
+ if (Remaining.FlushPromise)
+ {
+ Remaining.FlushPromise->set_value();
+ }
+ }
+ }
+
+ FlushSinks();
+ return;
+ }
+ }
+ }
+ }
+
+ std::vector<SinkPtr> m_Sinks;
+ BlockingQueue<AsyncLogMessage> m_Queue;
+ std::thread m_WorkerThread;
+};
+
+AsyncSink::AsyncSink(std::vector<SinkPtr> InSinks) : m_Impl(std::make_unique<Impl>(std::move(InSinks)))
+{
+}
+
+AsyncSink::~AsyncSink() = default;
+
+void
+AsyncSink::Log(const LogMessage& Msg)
+{
+ m_Impl->Log(Msg);
+}
+
+void
+AsyncSink::Flush()
+{
+ m_Impl->Flush();
+}
+
+void
+AsyncSink::SetFormatter(std::unique_ptr<Formatter> InFormatter)
+{
+ m_Impl->SetFormatter(std::move(InFormatter));
+}
+
+} // namespace zen::logging
diff --git a/src/zencore/logging/logger.cpp b/src/zencore/logging/logger.cpp
new file mode 100644
index 000000000..dd1675bb1
--- /dev/null
+++ b/src/zencore/logging/logger.cpp
@@ -0,0 +1,142 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/logging/logger.h>
+#include <zencore/thread.h>
+
+#include <string>
+#include <vector>
+
+namespace zen::logging {
+
+struct Logger::Impl
+{
+ std::string m_Name;
+ std::vector<SinkPtr> m_Sinks;
+ ErrorHandler* m_ErrorHandler = nullptr;
+};
+
+Logger::Logger(std::string_view InName, SinkPtr InSink) : m_Impl(std::make_unique<Impl>())
+{
+ m_Impl->m_Name = InName;
+ m_Impl->m_Sinks.push_back(std::move(InSink));
+}
+
+Logger::Logger(std::string_view InName, std::span<const SinkPtr> InSinks) : m_Impl(std::make_unique<Impl>())
+{
+ m_Impl->m_Name = InName;
+ m_Impl->m_Sinks.assign(InSinks.begin(), InSinks.end());
+}
+
+Logger::~Logger() = default;
+
+void
+Logger::Log(const LogPoint& Point, fmt::format_args Args)
+{
+ if (!ShouldLog(Point.Level))
+ {
+ return;
+ }
+
+ fmt::basic_memory_buffer<char, 250> Buffer;
+ fmt::vformat_to(fmt::appender(Buffer), Point.FormatString, Args);
+
+ LogMessage LogMsg(Point, m_Impl->m_Name, std::string_view(Buffer.data(), Buffer.size()));
+ LogMsg.SetThreadId(GetCurrentThreadId());
+ SinkIt(LogMsg);
+ FlushIfNeeded(Point.Level);
+}
+
+void
+Logger::SinkIt(const LogMessage& Msg)
+{
+ for (auto& CurrentSink : m_Impl->m_Sinks)
+ {
+ if (CurrentSink->ShouldLog(Msg.GetLevel()))
+ {
+ try
+ {
+ CurrentSink->Log(Msg);
+ }
+ catch (const std::exception& Ex)
+ {
+ if (m_Impl->m_ErrorHandler)
+ {
+ m_Impl->m_ErrorHandler->HandleError(Ex.what());
+ }
+ }
+ }
+ }
+}
+
+void
+Logger::FlushIfNeeded(LogLevel InLevel)
+{
+ if (InLevel >= m_FlushLevel.load(std::memory_order_relaxed))
+ {
+ Flush();
+ }
+}
+
+void
+Logger::Flush()
+{
+ for (auto& CurrentSink : m_Impl->m_Sinks)
+ {
+ try
+ {
+ CurrentSink->Flush();
+ }
+ catch (const std::exception& Ex)
+ {
+ if (m_Impl->m_ErrorHandler)
+ {
+ m_Impl->m_ErrorHandler->HandleError(Ex.what());
+ }
+ }
+ }
+}
+
+void
+Logger::SetSinks(std::vector<SinkPtr> InSinks)
+{
+ m_Impl->m_Sinks = std::move(InSinks);
+}
+
+void
+Logger::AddSink(SinkPtr InSink)
+{
+ m_Impl->m_Sinks.push_back(std::move(InSink));
+}
+
+void
+Logger::SetErrorHandler(ErrorHandler* Handler)
+{
+ m_Impl->m_ErrorHandler = Handler;
+}
+
+void
+Logger::SetFormatter(std::unique_ptr<Formatter> InFormatter)
+{
+ for (auto& CurrentSink : m_Impl->m_Sinks)
+ {
+ CurrentSink->SetFormatter(InFormatter->Clone());
+ }
+}
+
+std::string_view
+Logger::Name() const
+{
+ return m_Impl->m_Name;
+}
+
+Ref<Logger>
+Logger::Clone(std::string_view NewName) const
+{
+ Ref<Logger> Cloned(new Logger(NewName, m_Impl->m_Sinks));
+ Cloned->SetLevel(m_Level.load(std::memory_order_relaxed));
+ Cloned->SetFlushLevel(m_FlushLevel.load(std::memory_order_relaxed));
+ Cloned->SetErrorHandler(m_Impl->m_ErrorHandler);
+ return Cloned;
+}
+
+} // namespace zen::logging
diff --git a/src/zencore/logging/msvcsink.cpp b/src/zencore/logging/msvcsink.cpp
new file mode 100644
index 000000000..457a4d6e1
--- /dev/null
+++ b/src/zencore/logging/msvcsink.cpp
@@ -0,0 +1,80 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/zencore.h>
+
+#if ZEN_PLATFORM_WINDOWS
+
+# include <zencore/logging/helpers.h>
+# include <zencore/logging/messageonlyformatter.h>
+# include <zencore/logging/msvcsink.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+# include <Windows.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen::logging {
+
+// Default formatter for MSVC debug output: [level] message\n
+// For error/critical messages with source info, prepends file(line): so that
+// the message is clickable in the Visual Studio Output window.
+class DefaultMsvcFormatter : public Formatter
+{
+public:
+ void Format(const LogMessage& Msg, MemoryBuffer& Dest) override
+ {
+ const auto& Source = Msg.GetSource();
+ if (Msg.GetLevel() >= LogLevel::Err && Source)
+ {
+ helpers::AppendStringView(Source.Filename, Dest);
+ Dest.push_back('(');
+ helpers::AppendInt(Source.Line, Dest);
+ Dest.push_back(')');
+ Dest.push_back(':');
+ Dest.push_back(' ');
+ }
+
+ Dest.push_back('[');
+ helpers::AppendStringView(helpers::LevelToShortString(Msg.GetLevel()), Dest);
+ Dest.push_back(']');
+ Dest.push_back(' ');
+ helpers::AppendStringView(Msg.GetPayload(), Dest);
+ Dest.push_back('\n');
+ }
+
+ std::unique_ptr<Formatter> Clone() const override { return std::make_unique<DefaultMsvcFormatter>(); }
+};
+
+MsvcSink::MsvcSink() : m_Formatter(std::make_unique<DefaultMsvcFormatter>())
+{
+}
+
+void
+MsvcSink::Log(const LogMessage& Msg)
+{
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+
+ MemoryBuffer Formatted;
+ m_Formatter->Format(Msg, Formatted);
+
+ // Null-terminate for OutputDebugStringA
+ Formatted.push_back('\0');
+
+ OutputDebugStringA(Formatted.data());
+}
+
+void
+MsvcSink::Flush()
+{
+ // Nothing to flush for OutputDebugString
+}
+
+void
+MsvcSink::SetFormatter(std::unique_ptr<Formatter> InFormatter)
+{
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ m_Formatter = std::move(InFormatter);
+}
+
+} // namespace zen::logging
+
+#endif // ZEN_PLATFORM_WINDOWS
diff --git a/src/zencore/logging/registry.cpp b/src/zencore/logging/registry.cpp
new file mode 100644
index 000000000..3ed1fb0df
--- /dev/null
+++ b/src/zencore/logging/registry.cpp
@@ -0,0 +1,330 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/logging/registry.h>
+
+#include <zencore/logging/ansicolorsink.h>
+#include <zencore/logging/messageonlyformatter.h>
+
+#include <atomic>
+#include <condition_variable>
+#include <mutex>
+#include <thread>
+#include <unordered_map>
+
+namespace zen::logging {
+
+struct Registry::Impl
+{
+ Impl()
+ {
+ // Create default logger with a stdout color sink
+ SinkPtr DefaultSink(new AnsiColorStdoutSink());
+ m_DefaultLogger = Ref<Logger>(new Logger("", DefaultSink));
+ m_Loggers[""] = m_DefaultLogger;
+ }
+
+ ~Impl() { StopPeriodicFlush(); }
+
+ void Register(Ref<Logger> InLogger)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ if (m_ErrorHandler)
+ {
+ InLogger->SetErrorHandler(m_ErrorHandler);
+ }
+ m_Loggers[std::string(InLogger->Name())] = std::move(InLogger);
+ }
+
+ void Drop(const std::string& Name)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ m_Loggers.erase(Name);
+ }
+
+ Ref<Logger> Get(const std::string& Name)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ auto It = m_Loggers.find(Name);
+ if (It != m_Loggers.end())
+ {
+ return It->second;
+ }
+ return {};
+ }
+
+ void SetDefaultLogger(Ref<Logger> InLogger)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ if (InLogger)
+ {
+ m_Loggers[std::string(InLogger->Name())] = InLogger;
+ }
+ m_DefaultLogger = std::move(InLogger);
+ }
+
+ Logger* DefaultLoggerRaw() { return m_DefaultLogger.Get(); }
+
+ Ref<Logger> DefaultLogger()
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ return m_DefaultLogger;
+ }
+
+ void SetGlobalLevel(LogLevel Level)
+ {
+ m_GlobalLevel.store(Level, std::memory_order_relaxed);
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ for (auto& [Name, CurLogger] : m_Loggers)
+ {
+ CurLogger->SetLevel(Level);
+ }
+ }
+
+ LogLevel GetGlobalLevel() const { return m_GlobalLevel.load(std::memory_order_relaxed); }
+
+ void SetLevels(Registry::LogLevels Levels, LogLevel* DefaultLevel)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+
+ if (DefaultLevel)
+ {
+ m_GlobalLevel.store(*DefaultLevel, std::memory_order_relaxed);
+ for (auto& [Name, CurLogger] : m_Loggers)
+ {
+ CurLogger->SetLevel(*DefaultLevel);
+ }
+ }
+
+ for (auto& [LoggerName, Level] : Levels)
+ {
+ auto It = m_Loggers.find(LoggerName);
+ if (It != m_Loggers.end())
+ {
+ It->second->SetLevel(Level);
+ }
+ }
+ }
+
+ void FlushAll()
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ for (auto& [Name, CurLogger] : m_Loggers)
+ {
+ try
+ {
+ CurLogger->Flush();
+ }
+ catch (const std::exception&)
+ {
+ }
+ }
+ }
+
+ void FlushOn(LogLevel Level)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ m_FlushLevel = Level;
+ for (auto& [Name, CurLogger] : m_Loggers)
+ {
+ CurLogger->SetFlushLevel(Level);
+ }
+ }
+
+ void FlushEvery(std::chrono::seconds Interval)
+ {
+ StopPeriodicFlush();
+
+ m_PeriodicFlushRunning.store(true, std::memory_order_relaxed);
+
+ m_FlushThread = std::thread([this, Interval] {
+ while (m_PeriodicFlushRunning.load(std::memory_order_relaxed))
+ {
+ {
+ std::unique_lock<std::mutex> Lock(m_PeriodicFlushMutex);
+ m_PeriodicFlushCv.wait_for(Lock, Interval, [this] { return !m_PeriodicFlushRunning.load(std::memory_order_relaxed); });
+ }
+
+ if (m_PeriodicFlushRunning.load(std::memory_order_relaxed))
+ {
+ FlushAll();
+ }
+ }
+ });
+ }
+
+ void SetFormatter(std::unique_ptr<Formatter> InFormatter)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ for (auto& [Name, CurLogger] : m_Loggers)
+ {
+ CurLogger->SetFormatter(InFormatter->Clone());
+ }
+ }
+
+ void ApplyAll(void (*Func)(void*, Ref<Logger>), void* Context)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ for (auto& [Name, CurLogger] : m_Loggers)
+ {
+ Func(Context, CurLogger);
+ }
+ }
+
+ void SetErrorHandler(ErrorHandler* Handler)
+ {
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ m_ErrorHandler = Handler;
+ for (auto& [Name, CurLogger] : m_Loggers)
+ {
+ CurLogger->SetErrorHandler(Handler);
+ }
+ }
+
+ void Shutdown()
+ {
+ StopPeriodicFlush();
+ FlushAll();
+
+ std::lock_guard<std::mutex> Lock(m_Mutex);
+ m_Loggers.clear();
+ m_DefaultLogger = nullptr;
+ }
+
+private:
+ void StopPeriodicFlush()
+ {
+ if (m_FlushThread.joinable())
+ {
+ m_PeriodicFlushRunning.store(false, std::memory_order_relaxed);
+ {
+ std::lock_guard<std::mutex> Lock(m_PeriodicFlushMutex);
+ m_PeriodicFlushCv.notify_one();
+ }
+ m_FlushThread.join();
+ }
+ }
+
+ std::mutex m_Mutex;
+ std::unordered_map<std::string, Ref<Logger>> m_Loggers;
+ Ref<Logger> m_DefaultLogger;
+ std::atomic<LogLevel> m_GlobalLevel{Trace};
+ LogLevel m_FlushLevel{Off};
+ ErrorHandler* m_ErrorHandler = nullptr;
+
+ // Periodic flush
+ std::atomic<bool> m_PeriodicFlushRunning{false};
+ std::mutex m_PeriodicFlushMutex;
+ std::condition_variable m_PeriodicFlushCv;
+ std::thread m_FlushThread;
+};
+
+Registry&
+Registry::Instance()
+{
+ static Registry s_Instance;
+ return s_Instance;
+}
+
+Registry::Registry() : m_Impl(std::make_unique<Impl>())
+{
+}
+
+Registry::~Registry() = default;
+
+void
+Registry::Register(Ref<Logger> InLogger)
+{
+ m_Impl->Register(std::move(InLogger));
+}
+
+void
+Registry::Drop(const std::string& Name)
+{
+ m_Impl->Drop(Name);
+}
+
+Ref<Logger>
+Registry::Get(const std::string& Name)
+{
+ return m_Impl->Get(Name);
+}
+
+void
+Registry::SetDefaultLogger(Ref<Logger> InLogger)
+{
+ m_Impl->SetDefaultLogger(std::move(InLogger));
+}
+
+Logger*
+Registry::DefaultLoggerRaw()
+{
+ return m_Impl->DefaultLoggerRaw();
+}
+
+Ref<Logger>
+Registry::DefaultLogger()
+{
+ return m_Impl->DefaultLogger();
+}
+
+void
+Registry::SetGlobalLevel(LogLevel Level)
+{
+ m_Impl->SetGlobalLevel(Level);
+}
+
+LogLevel
+Registry::GetGlobalLevel() const
+{
+ return m_Impl->GetGlobalLevel();
+}
+
+void
+Registry::SetLevels(LogLevels Levels, LogLevel* DefaultLevel)
+{
+ m_Impl->SetLevels(Levels, DefaultLevel);
+}
+
+void
+Registry::FlushAll()
+{
+ m_Impl->FlushAll();
+}
+
+void
+Registry::FlushOn(LogLevel Level)
+{
+ m_Impl->FlushOn(Level);
+}
+
+void
+Registry::FlushEvery(std::chrono::seconds Interval)
+{
+ m_Impl->FlushEvery(Interval);
+}
+
+void
+Registry::SetFormatter(std::unique_ptr<Formatter> InFormatter)
+{
+ m_Impl->SetFormatter(std::move(InFormatter));
+}
+
+void
+Registry::ApplyAllImpl(void (*Func)(void*, Ref<Logger>), void* Context)
+{
+ m_Impl->ApplyAll(Func, Context);
+}
+
+void
+Registry::SetErrorHandler(ErrorHandler* Handler)
+{
+ m_Impl->SetErrorHandler(Handler);
+}
+
+void
+Registry::Shutdown()
+{
+ m_Impl->Shutdown();
+}
+
+} // namespace zen::logging
diff --git a/src/zencore/logging/tracesink.cpp b/src/zencore/logging/tracesink.cpp
new file mode 100644
index 000000000..8a6f4e40c
--- /dev/null
+++ b/src/zencore/logging/tracesink.cpp
@@ -0,0 +1,92 @@
+
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/logbase.h>
+#include <zencore/logging/tracesink.h>
+#include <zencore/string.h>
+#include <zencore/timer.h>
+#include <zencore/trace.h>
+
+#if ZEN_WITH_TRACE
+
+namespace zen::logging {
+
+UE_TRACE_CHANNEL_DEFINE(LogChannel)
+
+UE_TRACE_EVENT_BEGIN(Logging, LogCategory, NoSync | Important)
+ UE_TRACE_EVENT_FIELD(const void*, CategoryPointer)
+ UE_TRACE_EVENT_FIELD(uint8_t, DefaultVerbosity)
+ UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, Name)
+UE_TRACE_EVENT_END()
+
+UE_TRACE_EVENT_BEGIN(Logging, LogMessageSpec, NoSync | Important)
+ UE_TRACE_EVENT_FIELD(const void*, LogPoint)
+ UE_TRACE_EVENT_FIELD(const void*, CategoryPointer)
+ UE_TRACE_EVENT_FIELD(int32_t, Line)
+ UE_TRACE_EVENT_FIELD(uint8_t, Verbosity)
+ UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, FileName)
+ UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, FormatString)
+UE_TRACE_EVENT_END()
+
+UE_TRACE_EVENT_BEGIN(Logging, LogMessage, NoSync)
+ UE_TRACE_EVENT_FIELD(const void*, LogPoint)
+ UE_TRACE_EVENT_FIELD(uint64_t, Cycle)
+ UE_TRACE_EVENT_FIELD(uint8_t[], FormatArgs)
+UE_TRACE_EVENT_END()
+
+void
+TraceLogCategory(const logging::Logger* Category, const char* Name, logging::LogLevel DefaultVerbosity)
+{
+ uint16_t NameLen = uint16_t(strlen(Name));
+ UE_TRACE_LOG(Logging, LogCategory, LogChannel, NameLen * sizeof(ANSICHAR))
+ << LogCategory.CategoryPointer(Category) << LogCategory.DefaultVerbosity(uint8_t(DefaultVerbosity))
+ << LogCategory.Name(Name, NameLen);
+}
+
+void
+TraceLogMessageSpec(const void* LogPoint,
+ const logging::Logger* Category,
+ logging::LogLevel Verbosity,
+ const std::string_view File,
+ int32_t Line,
+ const std::string_view Format)
+{
+ uint16_t FileNameLen = uint16_t(File.size());
+ uint16_t FormatStringLen = uint16_t(Format.size());
+ uint32_t DataSize = (FileNameLen * sizeof(ANSICHAR)) + (FormatStringLen * sizeof(ANSICHAR));
+ UE_TRACE_LOG(Logging, LogMessageSpec, LogChannel, DataSize)
+ << LogMessageSpec.LogPoint(LogPoint) << LogMessageSpec.CategoryPointer(Category) << LogMessageSpec.Line(Line)
+ << LogMessageSpec.Verbosity(uint8_t(Verbosity)) << LogMessageSpec.FileName(File.data(), FileNameLen)
+ << LogMessageSpec.FormatString(Format.data(), FormatStringLen);
+}
+
+void
+TraceLogMessageInternal(const void* LogPoint, int32_t EncodedFormatArgsSize, const uint8_t* EncodedFormatArgs)
+{
+ UE_TRACE_LOG(Logging, LogMessage, LogChannel) << LogMessage.LogPoint(LogPoint) << LogMessage.Cycle(GetHifreqTimerValue())
+ << LogMessage.FormatArgs(EncodedFormatArgs, EncodedFormatArgsSize);
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+void
+TraceSink::Log(const LogMessage& Msg)
+{
+ ZEN_UNUSED(Msg);
+}
+
+void
+TraceSink::Flush()
+{
+}
+
+void
+TraceSink::SetFormatter(std::unique_ptr<Formatter> /*InFormatter*/)
+{
+ // This sink doesn't use a formatter since it just forwards the raw format
+ // args to the trace system
+}
+
+} // namespace zen::logging
+
+#endif