mirror of
https://github.com/hyprwm/hyprutils.git
synced 2025-12-20 05:50:11 +01:00
parent
16a7fe760d
commit
b311dc90dc
7 changed files with 464 additions and 1 deletions
113
include/hyprutils/cli/Logger.hpp
Normal file
113
include/hyprutils/cli/Logger.hpp
Normal 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 = "";
|
||||||
|
};
|
||||||
|
};
|
||||||
9
include/hyprutils/os/File.hpp
Normal file
9
include/hyprutils/os/File.hpp
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -99,7 +99,8 @@ std::vector<SArgumentKey>::iterator CArgumentParserImpl::getValue(const std::str
|
||||||
return it;
|
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())
|
if (getValue(name) != m_values.end() || getValue(abbrev) != m_values.end())
|
||||||
return std::unexpected("Value already exists");
|
return std::unexpected("Value already exists");
|
||||||
|
|
||||||
|
|
|
||||||
181
src/cli/Logger.cpp
Normal file
181
src/cli/Logger.cpp
Normal 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
35
src/cli/Logger.hpp
Normal 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
20
src/os/File.cpp
Normal 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
104
tests/cli/Logger.cpp
Normal 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.");
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue