cli: add logger

This commit is contained in:
Vaxry 2025-11-23 01:10:32 +00:00
parent 16a7fe760d
commit eb27ac56e3
Signed by: vaxry
GPG key ID: 665806380871D640
6 changed files with 304 additions and 0 deletions

View file

@ -0,0 +1,66 @@
#pragma once
#include <format>
#include <expected>
#include <string_view>
#include "../memory/UniquePtr.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 setTrace(bool enabled);
void setTime(bool enabled);
void setEnableStdout(bool enabled);
void setEnableColor(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 == LOG_TRACE && !m_trace)
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
bool m_trace = false;
// this has to be here as part of important optimization of disabled logging
bool m_shouldLogAtAll = false;
friend class CLoggerImpl;
};
};

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);
}

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

@ -0,0 +1,137 @@
#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::setTrace(bool enabled) {
m_trace = enabled;
}
void CLogger::setTime(bool enabled) {
m_impl->m_timeEnabled = enabled;
}
void CLogger::setEnableStdout(bool enabled) {
m_impl->m_stdoutEnabled;
m_impl->updateParentShouldLog();
}
void CLogger::setEnableColor(bool enabled) {
m_impl->m_colorEnabled = 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 == LOG_TRACE && !m_trace)
return;
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_impl->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);
}
logMsg += "]: ";
logMsg += msg;
if (m_impl->m_stdoutEnabled)
std::println("{}{}", m_impl->m_colorEnabled ? logPrefixColor : logPrefix, logMsg);
if (m_impl->m_fileEnabled)
m_impl->m_logOfs << logPrefix << logMsg << "\n";
m_impl->appendToRolling(logPrefix + logMsg);
}
const std::string& CLogger::rollingLog() {
return m_impl->m_rollingLog;
}
CLoggerImpl::CLoggerImpl(CLogger* parent) : m_parent(parent) {
updateParentShouldLog();
}
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);
}

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

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

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

@ -0,0 +1,42 @@
#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.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.setTrace(true);
logger.log(Hyprutils::CLI::LOG_TRACE, "Hello, {}!", "Trace");
EXPECT_EQ(logger.rollingLog(), "DEBUG ]: Hello!\nTRACE ]: Hello, Trace!");
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);
}