diff --git a/include/hyprutils/cli/Logger.hpp b/include/hyprutils/cli/Logger.hpp new file mode 100644 index 0000000..3d2d9bd --- /dev/null +++ b/include/hyprutils/cli/Logger.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include + +#include "../memory/UniquePtr.hpp" +#include "../memory/WeakPtr.hpp" + +namespace Hyprutils::CLI { + class CLoggerImpl; + + enum eLogLevel : uint8_t { + LOG_TRACE = 0, + LOG_DEBUG, + LOG_WARN, + LOG_ERR, + LOG_CRIT, + }; + + // CLogger is a thread-safe, general purpose logger. + // the logger's stdout is enabled by default. + // color is enabled by default, it's only for stdout. + // everything else is disabled. + class CLogger { + public: + CLogger(); + ~CLogger(); + + CLogger(const CLogger&) = delete; + CLogger(CLogger&) = delete; + CLogger(CLogger&&) = delete; + + void setLogLevel(eLogLevel level); + void setTime(bool enabled); + void setEnableStdout(bool enabled); + void setEnableColor(bool enabled); + void setEnableRolling(bool enabled); + std::expected setOutputFile(const std::string_view& file); + const std::string& rollingLog(); + + void log(eLogLevel level, const std::string_view& msg); + + template + // NOLINTNEXTLINE + void log(eLogLevel level, std::format_string fmt, Args&&... args) { + if (!m_shouldLogAtAll) + return; + + if (level < m_logLevel) + return; + + std::string logMsg = std::vformat(fmt.get(), std::make_format_args(args...)); + log(level, logMsg); + } + + private: + Memory::CUniquePointer m_impl; + + // this has to be here as part of important optimization of trace logs + eLogLevel m_logLevel = LOG_DEBUG; + + // this has to be here as part of important optimization of disabled logging + bool m_shouldLogAtAll = false; + + friend class CLoggerImpl; + friend class CLoggerConnection; + }; + + // CLoggerConnection is a "handle" to a logger, that can be created from a logger and + // allows to send messages to a logger via a proxy + // this does not allow for any changes to the logger itself, only sending logs. + // Logger connections keep their own logLevel. They inherit it at creation, but can be changed + class CLoggerConnection { + public: + CLoggerConnection(CLogger& logger); + ~CLoggerConnection(); + + CLoggerConnection(const CLoggerConnection&) = delete; + CLoggerConnection(CLoggerConnection&) = delete; + + // Allow move + CLoggerConnection(CLoggerConnection&&) = default; + + void setName(const std::string_view& name); + void setLogLevel(eLogLevel level); + + void log(eLogLevel level, const std::string_view& msg); + + template + // NOLINTNEXTLINE + void log(eLogLevel level, std::format_string fmt, Args&&... args) { + if (!m_impl || !m_logger) + return; + + if (!m_logger->m_shouldLogAtAll) + return; + + if (level < m_logLevel) + return; + + std::string logMsg = std::vformat(fmt.get(), std::make_format_args(args...)); + log(level, logMsg); + } + + private: + Memory::CWeakPointer m_impl; + CLogger* m_logger = nullptr; + eLogLevel m_logLevel = LOG_DEBUG; + + std::string m_name = ""; + }; +}; \ No newline at end of file diff --git a/include/hyprutils/os/File.hpp b/include/hyprutils/os/File.hpp new file mode 100644 index 0000000..2cefdf5 --- /dev/null +++ b/include/hyprutils/os/File.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include + +namespace Hyprutils::File { + std::expected readFileAsString(const std::string_view& path); +} \ No newline at end of file diff --git a/src/cli/ArgumentParser.cpp b/src/cli/ArgumentParser.cpp index e379fd8..d501921 100644 --- a/src/cli/ArgumentParser.cpp +++ b/src/cli/ArgumentParser.cpp @@ -99,7 +99,8 @@ std::vector::iterator CArgumentParserImpl::getValue(const std::str return it; } -std::expected CArgumentParserImpl::registerOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description, eArgumentType type) { +std::expected CArgumentParserImpl::registerOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description, + eArgumentType type) { if (getValue(name) != m_values.end() || getValue(abbrev) != m_values.end()) return std::unexpected("Value already exists"); diff --git a/src/cli/Logger.cpp b/src/cli/Logger.cpp new file mode 100644 index 0000000..32d37a4 --- /dev/null +++ b/src/cli/Logger.cpp @@ -0,0 +1,181 @@ +#include "Logger.hpp" + +#include +#include + +using namespace Hyprutils; +using namespace Hyprutils::CLI; + +CLogger::CLogger() { + m_impl = Memory::makeUnique(this); +} + +CLogger::~CLogger() = default; + +void CLogger::setLogLevel(eLogLevel level) { + m_logLevel = level; +} + +void CLogger::setTime(bool enabled) { + m_impl->m_timeEnabled = enabled; +} + +void CLogger::setEnableStdout(bool enabled) { + m_impl->m_stdoutEnabled = enabled; + m_impl->updateParentShouldLog(); +} + +void CLogger::setEnableColor(bool enabled) { + m_impl->m_colorEnabled = enabled; +} + +void CLogger::setEnableRolling(bool enabled) { + m_impl->m_rollingEnabled = enabled; +} + +std::expected CLogger::setOutputFile(const std::string_view& file) { + if (file.empty()) { + m_impl->m_fileEnabled = false; + m_impl->m_logOfs = {}; + return {}; + } + + std::filesystem::path filePath{file}; + std::error_code ec; + + if (!filePath.has_parent_path()) + return std::unexpected("Path has no parent"); + + auto dir = filePath.parent_path(); + + if (!std::filesystem::exists(dir, ec) || ec) + return std::unexpected("Parent path is inaccessible, or doesn't exist"); + + m_impl->m_logOfs = std::ofstream{filePath, std::ios::trunc}; + m_impl->m_logFilePath = filePath; + + if (!m_impl->m_logOfs.good()) + return std::unexpected("Failed to open a write stream"); + + m_impl->m_fileEnabled = true; + m_impl->updateParentShouldLog(); + + return {}; +} + +void CLogger::log(eLogLevel level, const std::string_view& msg) { + if (!m_shouldLogAtAll) + return; + + if (level < m_logLevel) + return; + + m_impl->log(level, msg); +} + +const std::string& CLogger::rollingLog() { + return m_impl->m_rollingLog; +} + +CLoggerImpl::CLoggerImpl(CLogger* parent) : m_parent(parent) { + updateParentShouldLog(); +} + +void CLoggerImpl::log(eLogLevel level, const std::string_view& msg, const std::string_view& from) { + std::lock_guard lg(m_logMtx); + + std::string logPrefix = "", logPrefixColor = ""; + std::string logMsg = ""; + + switch (level) { + case LOG_TRACE: + logPrefix += "TRACE "; + logPrefixColor += "\033[1;34mTRACE \033[0m"; + break; + case LOG_DEBUG: + logPrefix += "DEBUG "; + logPrefixColor += "\033[1;32mDEBUG \033[0m"; + break; + case LOG_WARN: + logPrefix += "WARN "; + logPrefixColor += "\033[1;33mWARN \033[0m"; + break; + case LOG_ERR: + logPrefix += "ERR "; + logPrefixColor += "\033[1;31mERR \033[0m"; + break; + case LOG_CRIT: + logPrefix += "CRIT "; + logPrefixColor += "\033[1;35mCRIT \033[0m"; + break; + } + + if (m_timeEnabled) { +#ifndef _LIBCPP_VERSION + static auto current_zone = std::chrono::current_zone(); + const auto zt = std::chrono::zoned_time{current_zone, std::chrono::system_clock::now()}; + const auto hms = std::chrono::hh_mm_ss{zt.get_local_time() - std::chrono::floor(zt.get_local_time())}; +#else + // TODO: current clang 17 does not support `zoned_time`, remove this once clang 19 is ready + const auto hms = std::chrono::hh_mm_ss{std::chrono::system_clock::now() - std::chrono::floor(std::chrono::system_clock::now())}; +#endif + logMsg += std::format("@ {} ", hms); + } + + if (!from.empty()) { + logMsg += "from "; + logMsg += from; + logMsg += " "; + } + + logMsg += "]: "; + logMsg += msg; + + if (m_stdoutEnabled) + std::println("{}{}", m_colorEnabled ? logPrefixColor : logPrefix, logMsg); + if (m_fileEnabled) + m_logOfs << logPrefix << logMsg << "\n"; + + if (m_rollingEnabled) + appendToRolling(logPrefix + logMsg); +} + +void CLoggerImpl::updateParentShouldLog() { + m_parent->m_shouldLogAtAll = m_fileEnabled || m_stdoutEnabled; +} + +void CLoggerImpl::appendToRolling(const std::string& s) { + constexpr const size_t ROLLING_LOG_SIZE = 4096; + if (!m_rollingLog.empty()) + m_rollingLog += "\n"; + m_rollingLog += s; + if (m_rollingLog.size() > ROLLING_LOG_SIZE) + m_rollingLog = m_rollingLog.substr(m_rollingLog.find('\n', m_rollingLog.size() - ROLLING_LOG_SIZE) + 1); +} + +CLoggerConnection::CLoggerConnection(CLogger& logger) : m_impl(logger.m_impl), m_logger(&logger), m_logLevel(logger.m_logLevel) { + ; +} + +CLoggerConnection::~CLoggerConnection() = default; + +void CLoggerConnection::setName(const std::string_view& name) { + m_name = name; +} + +void CLoggerConnection::setLogLevel(eLogLevel level) { + m_logLevel = level; +} + +void CLoggerConnection::log(eLogLevel level, const std::string_view& msg) { + if (!m_impl || !m_logger) + return; + + if (!m_logger->m_shouldLogAtAll) + return; + + if (level < m_logLevel) + return; + + m_impl->log(level, msg, m_name); +} diff --git a/src/cli/Logger.hpp b/src/cli/Logger.hpp new file mode 100644 index 0000000..3dab6e0 --- /dev/null +++ b/src/cli/Logger.hpp @@ -0,0 +1,35 @@ +#include +#include +#include +#include + +namespace Hyprutils::CLI { + class CLoggerImpl { + public: + CLoggerImpl(CLogger*); + ~CLoggerImpl() = default; + + CLoggerImpl(const CLoggerImpl&) = delete; + CLoggerImpl(CLoggerImpl&) = delete; + CLoggerImpl(CLoggerImpl&&) = delete; + + void updateParentShouldLog(); + void appendToRolling(const std::string& s); + void log(eLogLevel level, const std::string_view& msg, const std::string_view& from = ""); + + std::string m_rollingLog; + std::ofstream m_logOfs; + std::filesystem::path m_logFilePath; + + bool m_timeEnabled = false; + bool m_stdoutEnabled = true; + bool m_fileEnabled = false; + bool m_colorEnabled = true; + bool m_rollingEnabled = false; + + std::mutex m_logMtx; + + // this is fine because CLogger is NOMOVE and NOCOPY + CLogger* m_parent = nullptr; + }; +} \ No newline at end of file diff --git a/src/os/File.cpp b/src/os/File.cpp new file mode 100644 index 0000000..8b08844 --- /dev/null +++ b/src/os/File.cpp @@ -0,0 +1,20 @@ +#include + +#include +#include + +using namespace Hyprutils; +using namespace Hyprutils::File; + +std::expected File::readFileAsString(const std::string_view& path) { + std::error_code ec; + + if (!std::filesystem::exists(path, ec) || ec) + return std::unexpected("File not found"); + + std::ifstream file(std::string{path}); + if (!file.good()) + return std::unexpected("Failed to open file"); + + return std::string((std::istreambuf_iterator(file)), (std::istreambuf_iterator())); +} diff --git a/tests/cli/Logger.cpp b/tests/cli/Logger.cpp new file mode 100644 index 0000000..2149779 --- /dev/null +++ b/tests/cli/Logger.cpp @@ -0,0 +1,104 @@ +#include +#include + +#include + +#include + +using namespace Hyprutils::CLI; +using namespace Hyprutils; + +TEST(CLI, Logger) { + CLogger logger; + + logger.setEnableRolling(true); + + logger.log(Hyprutils::CLI::LOG_DEBUG, "Hello!"); + + EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!"); + + logger.log(Hyprutils::CLI::LOG_TRACE, "Hello!"); + + EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!"); + + logger.setLogLevel(LOG_TRACE); + + logger.log(Hyprutils::CLI::LOG_TRACE, "Hello, {}!", "Trace"); + + EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!"); + + CLoggerConnection connection(logger); + connection.setName("conn"); + + connection.log(Hyprutils::CLI::LOG_TRACE, "Hello from connection!"); + + EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!\nTRACE from conn ]: Hello from connection!"); + + connection.setLogLevel(Hyprutils::CLI::LOG_WARN); + + connection.log(Hyprutils::CLI::LOG_DEBUG, "Hello from connection!"); + + EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!\nTRACE from conn ]: Hello from connection!"); + + logger.setEnableRolling(false); + + connection.log(Hyprutils::CLI::LOG_ERR, "Err!"); + + EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!\nTRACE from conn ]: Hello from connection!"); + + logger.setEnableStdout(false); + + logger.log(Hyprutils::CLI::LOG_ERR, "Error"); + + EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!\nTRACE from conn ]: Hello from connection!"); + + auto res = logger.setOutputFile("./loggerFile.log"); + EXPECT_TRUE(res); + + logger.log(LOG_DEBUG, "Hi file!"); + + res = logger.setOutputFile(""); // clear + EXPECT_TRUE(res); + + auto fileRead = File::readFileAsString("./loggerFile.log"); + EXPECT_TRUE(fileRead); + + EXPECT_EQ(fileRead.value_or(""), "DEBUG ]: Hi file!\n"); + + std::error_code ec; + std::filesystem::remove("./loggerFile.log", ec); + + // TODO: maybe find a way to test the times and color? + logger.setEnableStdout(true); + logger.setTime(true); + + logger.log(Hyprutils::CLI::LOG_WARN, "Timed warning!"); + + logger.setEnableColor(false); + + logger.log(Hyprutils::CLI::LOG_CRIT, "rip"); + + logger.setEnableRolling(true); + + // spam some logs to check rolling + for (size_t i = 0; i < 200; ++i) { + logger.log(LOG_ERR, "Oh noes!!!"); + } + + EXPECT_TRUE(logger.rollingLog().size() < 4096); + EXPECT_TRUE(logger.rollingLog().starts_with("ERR")); // test the breaking is done correctly + + // test scoping + CLogger* pLogger = new CLogger(); + CLoggerConnection* pConnection = new CLoggerConnection(*pLogger); + + pLogger->setEnableStdout(false); + + pConnection->log(LOG_DEBUG, "This shouldn't log anything."); + + EXPECT_TRUE(pLogger->rollingLog().empty()); + + delete pLogger; + + pConnection->log(LOG_DEBUG, "This shouldn't do anything, or crash."); +} \ No newline at end of file