feat: add a new awesome feature
This commit is contained in:
446
src/SxLog.cpp
Normal file
446
src/SxLog.cpp
Normal file
@@ -0,0 +1,446 @@
|
||||
#include "SxLog.h"
|
||||
#include <cstdlib>
|
||||
#include <clocale>
|
||||
|
||||
/********************************************************************************
|
||||
* @文件: SxLog.cpp
|
||||
* @摘要: StellarX 日志系统实现(过滤/格式化/输出/文件滚动/RAII提交/作用域计时)
|
||||
* @描述:
|
||||
* 该实现文件主要包含 4 个关键点:
|
||||
* 1) FileSink: 文件打开、写入、flush 与按阈值滚动
|
||||
* 2) SxLogger: shouldLog 过滤、formatPrefix 前缀拼接、logLine 统一输出出口
|
||||
* 3) SxLogLine: 析构提交(RAII)确保“一条语句输出一整行”
|
||||
* 4) SxLogScope: 按需启用计时,析构输出耗时
|
||||
*
|
||||
* @实现难点提示:
|
||||
* - shouldLog 必须“零副作用”,否则宏短路会带来不可预测行为
|
||||
* - logLine 是统一出口,必须保证行级一致性,且避免在持锁状态下递归打日志
|
||||
* - 文件滚动要处理文件名安全性与跨平台 rename 行为差异
|
||||
* - 时间戳生成需要兼容 Windows 与 POSIX(localtime_s/localtime_r)
|
||||
********************************************************************************/
|
||||
|
||||
namespace StellarX
|
||||
{
|
||||
// -------- FileSink --------
|
||||
|
||||
// 打开文件输出
|
||||
// 难点:
|
||||
// - 需要支持追加与清空两种模式
|
||||
// - open 前先 close,避免重复打开导致句柄泄漏
|
||||
bool FileSink::open(const std::string& path, bool append)
|
||||
{
|
||||
close();
|
||||
filePath = path;
|
||||
appendMode = append;
|
||||
|
||||
std::ios::openmode mode = std::ios::out;
|
||||
mode |= (append ? std::ios::app : std::ios::trunc);
|
||||
|
||||
ofs.open(path.c_str(), mode);
|
||||
return ofs.is_open();
|
||||
}
|
||||
|
||||
// 关闭文件输出(可重复调用)
|
||||
void FileSink::close()
|
||||
{
|
||||
if (ofs.is_open()) ofs.close();
|
||||
}
|
||||
|
||||
// 查询是否已打开
|
||||
bool FileSink::isOpen() const
|
||||
{
|
||||
return ofs.is_open();
|
||||
}
|
||||
|
||||
// 写入一整行
|
||||
// 难点:
|
||||
// - 写入后若启用 rotateBytes,需要及时检测文件大小是否到阈值
|
||||
void FileSink::writeLine(const std::string& line)
|
||||
{
|
||||
if (!ofs.is_open()) return;
|
||||
ofs << line;
|
||||
if (rotateBytes > 0) rotateIfNeeded();
|
||||
}
|
||||
|
||||
// flush 文件缓冲
|
||||
void FileSink::flush()
|
||||
{
|
||||
if (ofs.is_open()) ofs.flush();
|
||||
}
|
||||
|
||||
// 滚动文件
|
||||
// 难点:
|
||||
// 1) tellp() 返回的是当前写指针位置,通常可近似视为文件大小
|
||||
// 2) 时间戳用于文件名时需要做字符清洗,避免出现不友好字符
|
||||
// 3) rename 行为与权限/占用有关,失败时需要保证不崩溃(此处选择“尽力而为”)
|
||||
bool FileSink::rotateIfNeeded()
|
||||
{
|
||||
if (!ofs.is_open() || rotateBytes == 0) return false;
|
||||
|
||||
const std::streampos pos = ofs.tellp();
|
||||
if (pos < 0) return false;
|
||||
|
||||
const std::size_t size = static_cast<std::size_t>(pos);
|
||||
if (size < rotateBytes) return false;
|
||||
|
||||
ofs.flush();
|
||||
ofs.close();
|
||||
|
||||
// xxx.log -> xxx.log.YYYYmmdd_HHMMSS
|
||||
// 说明:
|
||||
// - makeTimestampLocal 形如 "2026-01-09 12:34:56"
|
||||
// - 文件名中把 '-' ' ' ':' 替换为 '_',只保留数字与 '_',降低环境差异
|
||||
const std::string ts = SxLogger::makeTimestampLocal();
|
||||
std::string safeTs;
|
||||
safeTs.reserve(ts.size());
|
||||
for (char ch : ts)
|
||||
{
|
||||
if (ch >= '0' && ch <= '9') safeTs.push_back(ch);
|
||||
else if (ch == '-' || ch == ' ' || ch == ':') safeTs.push_back('_');
|
||||
}
|
||||
if (safeTs.empty()) safeTs = "rotated";
|
||||
|
||||
const std::string rotated = filePath + "." + safeTs;
|
||||
std::rename(filePath.c_str(), rotated.c_str());
|
||||
|
||||
// 重新打开新文件
|
||||
// 注意: 这里用 append=false,确保新文件从空开始
|
||||
return open(filePath, false);
|
||||
}
|
||||
|
||||
// -------- SxLogger --------
|
||||
|
||||
|
||||
|
||||
// 设置 Windows 控制台 codepage(只执行一次)
|
||||
// 难点:
|
||||
// - 只影响终端解释输出字节的方式,不影响源码文件编码
|
||||
// - 使用 once_flag 避免重复 system 调用造成噪声与性能浪费
|
||||
//
|
||||
// 注意:
|
||||
// - 下面原注释写“切到 UTF-8”,但实际命令是 chcp 936(GBK)
|
||||
// - 为避免改动你原注释,这里补充说明事实,保持行为不变
|
||||
void SxLogger::setGBK()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
static std::once_flag once;
|
||||
std::call_once(once, []() {
|
||||
// 切到 UTF-8,避免中文日志在 CP936 控制台下乱码
|
||||
// 说明:这不是 WinAPI;是执行系统命令
|
||||
std::system("chcp 936 >nul");
|
||||
|
||||
// 补充说明:
|
||||
// - chcp 936 实际是设置为 CP936(GBK)
|
||||
// - 如果你的终端本身是 UTF-8 环境,调用它可能反而改变显示行为
|
||||
// - 该函数建议只在“明确需要 GBK 控制台输出”的场景调用
|
||||
|
||||
// 尝试让 C/C++ 运行库按 UTF-8 工作(对部分流输出有帮助)
|
||||
// std::setlocale(LC_ALL, ".UTF8");
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
// 获取单例
|
||||
// 难点:
|
||||
// - 作为全局入口,初始化必须线程安全
|
||||
// - C++11 起函数内静态对象初始化由标准保证线程安全
|
||||
SxLogger& SxLogger::Get()
|
||||
{
|
||||
static SxLogger inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
// 构造:设置默认语言
|
||||
SxLogger::SxLogger()
|
||||
: lang(SxLogLanguage::ZhCN)
|
||||
{
|
||||
}
|
||||
|
||||
// 设置最低输出级别
|
||||
void SxLogger::setMinLevel(SxLogLevel level)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
cfg.minLevel = level;
|
||||
}
|
||||
|
||||
// 获取最低输出级别
|
||||
SxLogLevel SxLogger::getMinLevel() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
return cfg.minLevel;
|
||||
}
|
||||
|
||||
// 设置语言
|
||||
// 难点:
|
||||
// - 语言只影响 SX_T 的字符串选择
|
||||
// - 这里用 atomic relaxed,避免频繁加锁
|
||||
void SxLogger::setLanguage(SxLogLanguage l)
|
||||
{
|
||||
lang.store(l, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// 获取语言
|
||||
SxLogLanguage SxLogger::getLanguage() const
|
||||
{
|
||||
return lang.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// 设置 Tag 过滤
|
||||
// 难点:
|
||||
// - 当前实现是 vector<string> 线性匹配,适合 tag 数量不大
|
||||
// - 若未来 tag 很多,可考虑 unordered_set 优化(但会增加依赖与复杂度)
|
||||
void SxLogger::setTagFilter(SxTagFilterMode mode, const std::vector<std::string>& tags)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
cfg.tagFilterMode = mode;
|
||||
cfg.tagList = tags;
|
||||
}
|
||||
|
||||
// 清空 Tag 过滤
|
||||
void SxLogger::clearTagFilter()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
cfg.tagFilterMode = SxTagFilterMode::None;
|
||||
cfg.tagList.clear();
|
||||
}
|
||||
|
||||
// 开关控制台输出
|
||||
// 难点:
|
||||
// - ConsoleSink 持有 ostream 引用,不管理其生命周期
|
||||
void SxLogger::enableConsole(bool enable)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
if (enable)
|
||||
{
|
||||
if (!consoleSink) consoleSink.reset(new ConsoleSink(std::cout));
|
||||
}
|
||||
else
|
||||
{
|
||||
consoleSink.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// 开启文件输出
|
||||
// 难点:
|
||||
// - enableFile 成功与否决定 cfg.fileEnabled
|
||||
// - 需要把 rotateBytes 同步到 FileSink
|
||||
bool SxLogger::enableFile(const std::string& path, bool append, std::size_t rotateBytes_)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
|
||||
if (!fileSink) fileSink.reset(new FileSink());
|
||||
fileSink->setRotateBytes(rotateBytes_);
|
||||
|
||||
const bool ok = fileSink->open(path, append);
|
||||
cfg.fileEnabled = ok;
|
||||
cfg.filePath = path;
|
||||
cfg.fileAppend = append;
|
||||
cfg.rotateBytes = rotateBytes_;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// 关闭文件输出
|
||||
void SxLogger::disableFile()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
if (fileSink) fileSink->close();
|
||||
cfg.fileEnabled = false;
|
||||
}
|
||||
|
||||
// 获取配置副本
|
||||
// 难点:
|
||||
// - 返回副本避免外部拿到内部引用后绕过锁修改
|
||||
SxLogConfig SxLogger::getConfigCopy() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// 设置配置(整体替换)
|
||||
void SxLogger::setConfig(const SxLogConfig& c)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
cfg = c;
|
||||
}
|
||||
|
||||
// 级别转字符串
|
||||
const char* SxLogger::levelToString(SxLogLevel level)
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case SxLogLevel::Trace: return "TRACE";
|
||||
case SxLogLevel::Debug: return "DEBUG";
|
||||
case SxLogLevel::Info: return "INFO ";
|
||||
case SxLogLevel::Warn: return "WARN ";
|
||||
case SxLogLevel::Error: return "ERROR";
|
||||
case SxLogLevel::Fatal: return "FATAL";
|
||||
default: return "OFF ";
|
||||
}
|
||||
}
|
||||
|
||||
// 判断 tag 是否允许输出
|
||||
// 难点:
|
||||
// - 精确匹配 tag 字符串
|
||||
// - tag==nullptr 时默认允许,避免“无 tag 日志被误杀”
|
||||
bool SxLogger::tagAllowed(const SxLogConfig& c, const char* tag)
|
||||
{
|
||||
if (c.tagFilterMode == SxTagFilterMode::None) return true;
|
||||
if (!tag) return true;
|
||||
|
||||
bool found = false;
|
||||
for (const auto& t : c.tagList)
|
||||
{
|
||||
if (t == tag) { found = true; break; }
|
||||
}
|
||||
|
||||
if (c.tagFilterMode == SxTagFilterMode::Whitelist) return found;
|
||||
if (c.tagFilterMode == SxTagFilterMode::Blacklist) return !found;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 快速判定是否需要输出(宏短路依赖)
|
||||
// 难点:
|
||||
// 1) 必须无副作用:返回 false 时调用端不会构造对象也不会拼接
|
||||
// 2) 过滤维度要完整:级别、tag、sink 是否启用
|
||||
// 3) 当前实现加锁保证 cfg 与 sink 状态一致;代价是高频路径会有锁开销
|
||||
bool SxLogger::shouldLog(SxLogLevel level, const char* tag) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
if (cfg.minLevel == SxLogLevel::Off) return false;
|
||||
if (level < cfg.minLevel) return false;
|
||||
if (!tagAllowed(cfg, tag)) return false;
|
||||
if (!consoleSink && !cfg.fileEnabled) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 生成本地时间戳字符串
|
||||
// 难点:
|
||||
// - Windows 与 POSIX 的线程安全 localtime API 不同
|
||||
std::string SxLogger::makeTimestampLocal()
|
||||
{
|
||||
using namespace std::chrono;
|
||||
const auto now = system_clock::now();
|
||||
const std::time_t t = system_clock::to_time_t(now);
|
||||
|
||||
std::tm tmv{};
|
||||
#if defined(_WIN32)
|
||||
localtime_s(&tmv, &t);
|
||||
#else
|
||||
localtime_r(&t, &tmv);
|
||||
#endif
|
||||
std::ostringstream oss;
|
||||
oss << std::put_time(&tmv, "%Y-%m-%d %H:%M:%S");
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// 拼接日志前缀(调用方已持锁)
|
||||
// 难点:
|
||||
// - 前缀拼接必须与配置项严格对应,且尽量避免多余开销
|
||||
// - showSource 会输出 (file:line func),对定位时序问题很有价值
|
||||
std::string SxLogger::formatPrefixUnlocked(
|
||||
const SxLogConfig& c,
|
||||
SxLogLevel level,
|
||||
const char* tag,
|
||||
const char* file,
|
||||
int line,
|
||||
const char* func) const
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
if (c.showTimestamp) oss << "[" << makeTimestampLocal() << "] ";
|
||||
if (c.showLevel) oss << "[" << levelToString(level) << "] ";
|
||||
if (c.showTag && tag) oss << "[" << tag << "] ";
|
||||
|
||||
if (c.showThreadId)
|
||||
{
|
||||
oss << "[T:" << std::this_thread::get_id() << "] ";
|
||||
}
|
||||
|
||||
if (c.showSource && file && func)
|
||||
{
|
||||
oss << "(" << file << ":" << line << " " << func << ") ";
|
||||
}
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// 统一输出出口
|
||||
// 难点:
|
||||
// 1) 行级一致性:必须把 prefix + msg + "\n" 当作整体写入
|
||||
// 2) 线程安全:持锁写入可避免不同线程日志互相穿插
|
||||
// 3) 避免重入:在持锁期间不要再调用 SX_LOG...(会导致死锁)
|
||||
void SxLogger::logLine(
|
||||
SxLogLevel level,
|
||||
const char* tag,
|
||||
const char* file,
|
||||
int line,
|
||||
const char* func,
|
||||
const std::string& msg)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mtx);
|
||||
|
||||
if (cfg.minLevel == SxLogLevel::Off) return;
|
||||
if (level < cfg.minLevel) return;
|
||||
if (!tagAllowed(cfg, tag)) return;
|
||||
|
||||
const std::string prefix = formatPrefixUnlocked(cfg, level, tag, file, line, func);
|
||||
const std::string lineText = prefix + msg + "\n";
|
||||
|
||||
if (consoleSink) consoleSink->writeLine(lineText);
|
||||
|
||||
if (cfg.fileEnabled && fileSink && fileSink->isOpen())
|
||||
{
|
||||
fileSink->writeLine(lineText);
|
||||
}
|
||||
|
||||
if (cfg.autoFlush)
|
||||
{
|
||||
if (consoleSink) consoleSink->flush();
|
||||
if (cfg.fileEnabled && fileSink) fileSink->flush();
|
||||
}
|
||||
}
|
||||
|
||||
// -------- SxLogLine --------
|
||||
|
||||
// 构造:只记录元信息
|
||||
SxLogLine::SxLogLine(SxLogLevel level, const char* tag, const char* file, int line, const char* func)
|
||||
: lvl(level), tg(tag), srcFile(file), srcLine(line), srcFunc(func)
|
||||
{
|
||||
}
|
||||
|
||||
// 析构:提交输出
|
||||
// 难点:
|
||||
// - 这是 RAII 设计的核心:保证语句结束时日志自动落地
|
||||
// - 也要求调用端不要把临时对象跨语句保存(宏用法本身也不支持那样做)
|
||||
SxLogLine::~SxLogLine()
|
||||
{
|
||||
SxLogger::Get().logLine(lvl, tg, srcFile, srcLine, srcFunc, ss.str());
|
||||
}
|
||||
|
||||
// -------- SxLogScope --------
|
||||
|
||||
// 构造:按需启用计时
|
||||
// 难点:
|
||||
// - 只有 shouldLog 为 true 才记录起点,避免在未输出场景做无意义计时
|
||||
SxLogScope::SxLogScope(SxLogLevel level, const char* tag, const char* file, int line, const char* func, const char* name)
|
||||
: lvl(level), tg(tag), srcFile(file), srcLine(line), srcFunc(func), scopeName(name)
|
||||
{
|
||||
enabled = SxLogger::Get().shouldLog(lvl, tg);
|
||||
if (enabled) t0 = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
// 析构:输出耗时
|
||||
// 难点:
|
||||
// - steady_clock 用于衡量耗时,避免系统时间调整造成跳变
|
||||
SxLogScope::~SxLogScope()
|
||||
{
|
||||
if (!enabled) return;
|
||||
const auto t1 = std::chrono::steady_clock::now();
|
||||
const auto us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SCOPE " << (scopeName ? scopeName : "") << " cost=" << us << "us";
|
||||
SxLogger::Get().logLine(lvl, tg, srcFile, srcLine, srcFunc, oss.str());
|
||||
}
|
||||
|
||||
} // namespace StellarX
|
||||
Reference in New Issue
Block a user