cli: add logger (#90)

Adds a Logger to the CLI namespace
This commit is contained in:
Vaxry 2025-11-23 15:45:14 +00:00 committed by GitHub
parent 16a7fe760d
commit b311dc90dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 464 additions and 1 deletions

View file

@ -0,0 +1,113 @@
#pragma once
#include <format>
#include <expected>
#include <string_view>
#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<void, std::string> setOutputFile(const std::string_view& file);
const std::string& rollingLog();
void log(eLogLevel level, const std::string_view& msg);
template <typename... Args>
// NOLINTNEXTLINE
void log(eLogLevel level, std::format_string<Args...> 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<CLoggerImpl> 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 <typename... Args>
// NOLINTNEXTLINE
void log(eLogLevel level, std::format_string<Args...> 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<CLoggerImpl> m_impl;
CLogger* m_logger = nullptr;
eLogLevel m_logLevel = LOG_DEBUG;
std::string m_name = "";
};
};

View file

@ -0,0 +1,9 @@
#pragma once
#include <string>
#include <expected>
#include <string_view>
namespace Hyprutils::File {
std::expected<std::string, std::string> readFileAsString(const std::string_view& path);
}

View file

@ -99,7 +99,8 @@ std::vector<SArgumentKey>::iterator CArgumentParserImpl::getValue(const std::str
return it;
}
std::expected<void, std::string> CArgumentParserImpl::registerOption(const std::string_view& name, const std::string_view& abbrev, const std::string_view& description, eArgumentType type) {
std::expected<void, std::string> 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");

181
src/cli/Logger.cpp Normal file
View file

@ -0,0 +1,181 @@
#include "Logger.hpp"
#include <chrono>
#include <print>
using namespace Hyprutils;
using namespace Hyprutils::CLI;
CLogger::CLogger() {
m_impl = Memory::makeUnique<CLoggerImpl>(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<void, std::string> 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<std::mutex> 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<std::chrono::days>(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::days>(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);
}

35
src/cli/Logger.hpp Normal file
View file

@ -0,0 +1,35 @@
#include <hyprutils/cli/Logger.hpp>
#include <fstream>
#include <filesystem>
#include <mutex>
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;
};
}

20
src/os/File.cpp Normal file
View file

@ -0,0 +1,20 @@
#include <hyprutils/os/File.hpp>
#include <filesystem>
#include <fstream>
using namespace Hyprutils;
using namespace Hyprutils::File;
std::expected<std::string, std::string> 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<char>(file)), (std::istreambuf_iterator<char>()));
}

104
tests/cli/Logger.cpp Normal file
View file

@ -0,0 +1,104 @@
#include <cli/Logger.hpp>
#include <hyprutils/os/File.hpp>
#include <gtest/gtest.h>
#include <filesystem>
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.");
}