173 lines
4.8 KiB
C++
173 lines
4.8 KiB
C++
#include <base/assert.hpp>
|
|
#include <base/logger.hpp>
|
|
#include <base/types.hpp>
|
|
#include <condition_variable>
|
|
#include <cstddef>
|
|
#include <deque>
|
|
#include <iostream>
|
|
#include <mutex>
|
|
#include <thread>
|
|
|
|
namespace base {
|
|
|
|
namespace logger_detail {
|
|
|
|
static constexpr std::string_view SeverityToString(MessageSeverity sev) {
|
|
// This must match order of logger_detail::MessageSeverity.
|
|
const char* MessageSeverityStringTable[] = { "Debug", "Info", "Warn", "Error", "Fatal" };
|
|
return MessageSeverityStringTable[static_cast<std::size_t>(sev)];
|
|
}
|
|
|
|
constexpr static auto ChannelToString(ChannelId channelId) -> std::string_view {
|
|
MessageSource src;
|
|
MessageComponentSource comp;
|
|
|
|
ChannelIdToComponents(channelId, src, comp);
|
|
|
|
switch(src) {
|
|
case MessageSource::Main: {
|
|
switch(comp) {
|
|
case MessageComponentSource::Main_Global: return "Main::Global";
|
|
default: BASE_CHECK(false, "Invalid component source");
|
|
}
|
|
};
|
|
|
|
case MessageSource::Base: {
|
|
switch(comp) {
|
|
case MessageComponentSource::Base_Assertions: return "Base::Assert";
|
|
default: BASE_CHECK(false, "Invalid component source");
|
|
}
|
|
};
|
|
|
|
case MessageSource::Http: {
|
|
switch(comp) {
|
|
case MessageComponentSource::Http_Server: return "HTTP::Server";
|
|
case MessageComponentSource::Http_WebSocketClient: return "HTTP::WebSocketClient";
|
|
default: BASE_CHECK(false, "Invalid component source");
|
|
}
|
|
};
|
|
|
|
case MessageSource::Server: {
|
|
switch(comp) {
|
|
case MessageComponentSource::Server_Server: return "LobbyServer2::Server";
|
|
default: BASE_CHECK(false, "Invalid component source");
|
|
}
|
|
};
|
|
|
|
default: BASE_CHECK(false, "Invalid channel source {:04x}", static_cast<u16>(src));
|
|
}
|
|
}
|
|
|
|
struct LoggerThreadData {
|
|
// Logger thread stuff
|
|
std::thread loggerThread;
|
|
std::mutex logQueueMutex;
|
|
std::condition_variable logQueueCv;
|
|
std::deque<MessageData> logQueue;
|
|
|
|
bool logThreadShutdown = false;
|
|
|
|
// N.B: This is stored/cached here just because it really does
|
|
// *NOT* need to be in a hot path.
|
|
const std::chrono::time_zone* timeZone = std::chrono::current_zone();
|
|
|
|
bool ShouldUnblock() {
|
|
// Always unblock if the logger thread needs to be shut down.
|
|
if(logThreadShutdown)
|
|
return true;
|
|
|
|
return !logQueue.empty();
|
|
}
|
|
|
|
void PushMessage(MessageData&& md) {
|
|
{
|
|
std::unique_lock<std::mutex> lk(logQueueMutex);
|
|
logQueue.emplace_back(std::move(md));
|
|
}
|
|
logQueueCv.notify_one();
|
|
}
|
|
};
|
|
|
|
Unique<LoggerThreadData> threadData;
|
|
|
|
void LoggerGlobalState::LoggerThread() {
|
|
auto& self = The();
|
|
|
|
// Fancy thread names.
|
|
pthread_setname_np(pthread_self(), "LoggerThread");
|
|
|
|
while(true) {
|
|
std::unique_lock<std::mutex> lk(threadData->logQueueMutex);
|
|
if(threadData->logQueue.empty()) {
|
|
// Await for messages.
|
|
threadData->logQueueCv.wait(lk, []() { return threadData->ShouldUnblock(); });
|
|
}
|
|
|
|
// Flush the logger queue until there are no more messages.
|
|
while(!threadData->logQueue.empty()) {
|
|
self.OutputMessage(threadData->logQueue.back());
|
|
threadData->logQueue.pop_back();
|
|
}
|
|
|
|
// Shutdown if requested.
|
|
if(threadData->logThreadShutdown)
|
|
break;
|
|
}
|
|
}
|
|
|
|
LoggerGlobalState& LoggerGlobalState::The() {
|
|
static LoggerGlobalState storage;
|
|
return storage;
|
|
}
|
|
|
|
LoggerGlobalState::LoggerGlobalState() {
|
|
// Spawn the logger thread
|
|
threadData = std::make_unique<LoggerThreadData>();
|
|
threadData->loggerThread = std::thread(&LoggerGlobalState::LoggerThread);
|
|
}
|
|
|
|
LoggerGlobalState::~LoggerGlobalState() {
|
|
// Shut down the logger thread
|
|
threadData->logThreadShutdown = true;
|
|
threadData->logQueueCv.notify_all();
|
|
threadData->loggerThread.join();
|
|
}
|
|
|
|
void LoggerGlobalState::AttachSink(Sink& sink) {
|
|
sinks.push_back(&sink);
|
|
}
|
|
|
|
void LoggerGlobalState::OutputMessage(const MessageData& data) {
|
|
// give up early if no sinks are attached
|
|
if(sinks.empty())
|
|
return;
|
|
|
|
if(data.severity < logLevel)
|
|
return;
|
|
|
|
auto formattedLoggerMessage =
|
|
std::format("[{:%F %H:%M:%S}|{}|{}] {}", std::chrono::floor<std::chrono::milliseconds>(threadData->timeZone->to_local(data.time)),
|
|
SeverityToString(data.severity), ChannelToString(data.channelId), data.message);
|
|
|
|
for(auto sink : sinks)
|
|
sink->OutputMessage(formattedLoggerMessage);
|
|
}
|
|
|
|
} // namespace logger_detail
|
|
|
|
Logger& Logger::Global() {
|
|
static Logger globalLogger;
|
|
return globalLogger;
|
|
}
|
|
|
|
void Logger::VOut(MessageSeverity severity, std::string_view format, std::format_args args) {
|
|
logger_detail::MessageData data {
|
|
.time = std::chrono::system_clock::now(), .severity = severity, .channelId = channelId, .message = std::vformat(format, args)
|
|
};
|
|
|
|
// Push data into logger thread.
|
|
logger_detail::threadData->PushMessage(std::move(data));
|
|
}
|
|
|
|
} // namespace base
|