Files
StellarX/src/SxLog.cpp

447 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 与 POSIXlocaltime_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 936GBK
// - 为避免改动你原注释,这里补充说明事实,保持行为不变
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 实际是设置为 CP936GBK
// - 如果你的终端本身是 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