mirror of
https://github.com/hyprwm/hyprutils.git
synced 2025-12-20 04:40:08 +01:00
cli: add logger
This commit is contained in:
parent
16a7fe760d
commit
eb27ac56e3
6 changed files with 304 additions and 0 deletions
66
include/hyprutils/cli/Logger.hpp
Normal file
66
include/hyprutils/cli/Logger.hpp
Normal 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;
|
||||
};
|
||||
};
|
||||
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);
|
||||
}
|
||||
137
src/cli/Logger.cpp
Normal file
137
src/cli/Logger.cpp
Normal 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
30
src/cli/Logger.hpp
Normal 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
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>()));
|
||||
}
|
||||
42
tests/cli/Logger.cpp
Normal file
42
tests/cli/Logger.cpp
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue