diff --git a/scwx-qt/scwx-qt.cmake b/scwx-qt/scwx-qt.cmake index 156b622e..e79791b7 100644 --- a/scwx-qt/scwx-qt.cmake +++ b/scwx-qt/scwx-qt.cmake @@ -91,6 +91,7 @@ set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp source/scwx/qt/manager/download_manager.hpp source/scwx/qt/manager/font_manager.hpp source/scwx/qt/manager/hotkey_manager.hpp + source/scwx/qt/manager/log_manager.hpp source/scwx/qt/manager/media_manager.hpp source/scwx/qt/manager/placefile_manager.hpp source/scwx/qt/manager/position_manager.hpp @@ -106,6 +107,7 @@ set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp source/scwx/qt/manager/download_manager.cpp source/scwx/qt/manager/font_manager.cpp source/scwx/qt/manager/hotkey_manager.cpp + source/scwx/qt/manager/log_manager.cpp source/scwx/qt/manager/media_manager.cpp source/scwx/qt/manager/placefile_manager.cpp source/scwx/qt/manager/position_manager.cpp diff --git a/scwx-qt/source/scwx/qt/main/main.cpp b/scwx-qt/source/scwx/qt/main/main.cpp index 0b4a4f07..9d694a5f 100644 --- a/scwx-qt/source/scwx/qt/main/main.cpp +++ b/scwx-qt/source/scwx/qt/main/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -22,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -31,9 +31,6 @@ static const std::string logPrefix_ = "scwx::main"; static const auto logger_ = scwx::util::Logger::Create(logPrefix_); static void OverrideDefaultStyle(const std::vector& args); -static void QtLogMessageHandler(QtMsgType messageType, - const QMessageLogContext& context, - const QString& message); int main(int argc, char* argv[]) { @@ -45,11 +42,8 @@ int main(int argc, char* argv[]) } // Initialize logger - scwx::util::Logger::Initialize(); - spdlog::set_level(spdlog::level::debug); - - // Install Qt Message Handler - qInstallMessageHandler(&QtLogMessageHandler); + auto& logManager = scwx::qt::manager::LogManager::Instance(); + logManager.Initialize(); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts, true); @@ -101,6 +95,7 @@ int main(int argc, char* argv[]) scwx::qt::config::RadarSite::Initialize(); scwx::qt::config::CountyDatabase::Initialize(); scwx::qt::manager::SettingsManager::Instance().Initialize(); + logManager.InitializeLogFile(); scwx::qt::manager::ResourceManager::Initialize(); // Theme @@ -176,40 +171,3 @@ OverrideDefaultStyle([[maybe_unused]] const std::vector& args) } #endif } - -void QtLogMessageHandler(QtMsgType messageType, - const QMessageLogContext& context, - const QString& message) -{ - static const auto qtLogger_ = scwx::util::Logger::Create("qt"); - - static const std::unordered_map - levelMap_ {{QtMsgType::QtDebugMsg, spdlog::level::level_enum::debug}, - {QtMsgType::QtInfoMsg, spdlog::level::level_enum::info}, - {QtMsgType::QtWarningMsg, spdlog::level::level_enum::warn}, - {QtMsgType::QtCriticalMsg, spdlog::level::level_enum::err}, - {QtMsgType::QtFatalMsg, spdlog::level::level_enum::critical}}; - - spdlog::level::level_enum level = spdlog::level::level_enum::info; - auto it = levelMap_.find(messageType); - if (it != levelMap_.cend()) - { - level = it->second; - } - - spdlog::source_loc location {}; - if (context.file != nullptr && context.function != nullptr) - { - location = {context.file, context.line, context.function}; - } - - if (context.category != nullptr) - { - qtLogger_->log( - location, level, "[{}] {}", context.category, message.toStdString()); - } - else - { - qtLogger_->log(location, level, message.toStdString()); - } -} diff --git a/scwx-qt/source/scwx/qt/manager/log_manager.cpp b/scwx-qt/source/scwx/qt/manager/log_manager.cpp new file mode 100644 index 00000000..457c8d28 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/log_manager.cpp @@ -0,0 +1,178 @@ +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +static const std::string logPrefix_ = "scwx::qt::manager::log_manager"; +static const auto logger_ = scwx::util::Logger::Create(logPrefix_); + +static void QtLogMessageHandler(QtMsgType messageType, + const QMessageLogContext& context, + const QString& message); + +class LogManager::Impl +{ +public: + explicit Impl() {} + ~Impl() {} + + void PruneLogFiles(); + + std::string logPath_ {}; + std::string logFile_ {}; + int pid_ {}; +}; + +LogManager::LogManager() : p(std::make_unique()) {} +LogManager::~LogManager() = default; + +void LogManager::Initialize() +{ + // Initialize logger + scwx::util::Logger::Initialize(); + spdlog::set_level(spdlog::level::debug); + + // Install Qt Message Handler + qInstallMessageHandler(&QtLogMessageHandler); +} + +void LogManager::InitializeLogFile() +{ + p->logPath_ = + QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + .toStdString(); + p->pid_ = boost::this_process::get_id(); + p->logFile_ = fmt::format("{}/supercell-wx.{}.log", p->logPath_, p->pid_); + + // Create log directory if it doesn't exist + if (!std::filesystem::exists(p->logPath_)) + { + if (!std::filesystem::create_directories(p->logPath_)) + { + logger_->error("Unable to create log directory: \"{}\"", p->logPath_); + return; + } + } + + scwx::util::Logger::AddFileSink(p->logFile_); + + p->PruneLogFiles(); +} + +void LogManager::Impl::PruneLogFiles() +{ + using namespace std::chrono_literals; + + static constexpr std::size_t kMaxLogFiles_ = 5; + static constexpr auto kMinModificationAge_ = 30min; + + std::multimap + logFiles {}; + + // Find existing log files in log directory + for (auto& file : std::filesystem::directory_iterator(logPath_)) + { + const std::string filename = file.path().filename().string(); + + if (file.is_regular_file() && filename.starts_with("supercell-wx.") && + filename.ends_with(".log")) + { + logger_->trace("Found log file: {}", filename); + + try + { + auto lastWriteTime = std::filesystem::last_write_time(file); + logFiles.insert({lastWriteTime, file}); + } + catch (const std::exception&) + { + logger_->error("Error getting last write time of file: {}", + file.path().string()); + } + } + } + + // Clean up old log files + auto now = std::filesystem::file_time_type::clock::now(); + auto modificationThreshold = now - kMinModificationAge_; + + for (auto& logFile : + logFiles | std::views::reverse | std::views::drop(kMaxLogFiles_)) + { + if (logFile.first < modificationThreshold) + { + const std::string filename = logFile.second.filename().string(); + + logger_->info("Removing old log file: {}", filename); + + std::error_code ec; + if (!std::filesystem::remove(logFile.second, ec)) + { + logger_->warn( + "Could not remove file: {}, {}", filename, ec.message()); + } + } + } +} + +LogManager& LogManager::Instance() +{ + static LogManager LogManager_ {}; + return LogManager_; +} + +void QtLogMessageHandler(QtMsgType messageType, + const QMessageLogContext& context, + const QString& message) +{ + static const auto qtLogger_ = scwx::util::Logger::Create("qt"); + + static const std::unordered_map + levelMap_ {{QtMsgType::QtDebugMsg, spdlog::level::level_enum::debug}, + {QtMsgType::QtInfoMsg, spdlog::level::level_enum::info}, + {QtMsgType::QtWarningMsg, spdlog::level::level_enum::warn}, + {QtMsgType::QtCriticalMsg, spdlog::level::level_enum::err}, + {QtMsgType::QtFatalMsg, spdlog::level::level_enum::critical}}; + + spdlog::level::level_enum level = spdlog::level::level_enum::info; + auto it = levelMap_.find(messageType); + if (it != levelMap_.cend()) + { + level = it->second; + } + + spdlog::source_loc location {}; + if (context.file != nullptr && context.function != nullptr) + { + location = {context.file, context.line, context.function}; + } + + if (context.category != nullptr) + { + qtLogger_->log( + location, level, "[{}] {}", context.category, message.toStdString()); + } + else + { + qtLogger_->log(location, level, message.toStdString()); + } +} + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/scwx-qt/source/scwx/qt/manager/log_manager.hpp b/scwx-qt/source/scwx/qt/manager/log_manager.hpp new file mode 100644 index 00000000..29c87943 --- /dev/null +++ b/scwx-qt/source/scwx/qt/manager/log_manager.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include +#include + +namespace scwx +{ +namespace qt +{ +namespace manager +{ + +class LogManager : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(LogManager) + +public: + explicit LogManager(); + ~LogManager(); + + void Initialize(); + void InitializeLogFile(); + + static LogManager& Instance(); + +private: + class Impl; + std::unique_ptr p; +}; + +} // namespace manager +} // namespace qt +} // namespace scwx diff --git a/wxdata/include/scwx/util/logger.hpp b/wxdata/include/scwx/util/logger.hpp index cab97a42..c85076e3 100644 --- a/wxdata/include/scwx/util/logger.hpp +++ b/wxdata/include/scwx/util/logger.hpp @@ -21,6 +21,7 @@ namespace Logger { void Initialize(); +void AddFileSink(const std::string& baseFilename); std::shared_ptr Create(const std::string& name); } // namespace Logger diff --git a/wxdata/source/scwx/util/logger.cpp b/wxdata/source/scwx/util/logger.cpp index 0642dfe0..8a97d80e 100644 --- a/wxdata/source/scwx/util/logger.cpp +++ b/wxdata/source/scwx/util/logger.cpp @@ -3,6 +3,7 @@ #include #include +#include #include namespace scwx @@ -12,9 +13,34 @@ namespace util namespace Logger { +static const std::string logPattern_ = "[%Y-%m-%d %T.%e] [%t] [%^%l%$] [%n] %v"; + +static std::vector> extraSinks_ {}; + void Initialize() { - spdlog::set_pattern("[%Y-%m-%d %T.%e] [%t] [%^%l%$] [%n] %v"); + spdlog::set_pattern(logPattern_); +} + +void AddFileSink(const std::string& baseFilename) +{ + constexpr std::size_t maxSize = 20u * 1024u * 1024u; // 20 MB + constexpr std::size_t maxFiles = 5u; + constexpr bool rotateOnOpen = true; + + auto fileSink = std::make_shared( + baseFilename, maxSize, maxFiles, rotateOnOpen); + + fileSink->set_pattern(logPattern_); + + spdlog::apply_all( + [&](std::shared_ptr logger) + { + auto& sinks = logger->sinks(); + sinks.push_back(fileSink); + }); + + extraSinks_.push_back(fileSink); } std::shared_ptr Create(const std::string& name) @@ -26,6 +52,13 @@ std::shared_ptr Create(const std::string& name) std::shared_ptr logger = std::make_shared(name, sink); + // Add additional registered sinks + for (auto& extraSink : extraSinks_) + { + auto& sinks = logger->sinks(); + sinks.push_back(extraSink); + } + // Register the logger, so it can be retrieved later using spdlog::get() spdlog::register_logger(logger);