完全指南:分层日志类别、宏用法与配置语法)
folly 日志库folly::logging完全指南分层日志类别、宏用法与配置语法【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly导读folly/logging 是 Facebook/Meta 开源 C 基础库 Folly 中面向调试日志debug logging设计的灵活日志组件其核心设计目标有二禁用状态下的日志语句开销极低通常只需一次条件判断以及通过分层日志类别hierarchical log categories轻松控制代码任意区域的日志级别。本文以仓库中 folly/logging/README.md 为主干结合 docs 系列文档与源码完整讲解日志类别与级别模型、XLOG/FB_LOG 系列宏、内置处理器stream/file、基础与 JSON 两种配置语法以及异步 I/O、字符转义等实用特性。读完本文你将能在自己的 C 项目中正确接入并精细调优 folly logging。一、设计动机为什么需要一个新的 C 日志库folly logging 面向的核心场景是在性能敏感的关键路径上也敢于留下大量调试日志。它有两个主要设计目标禁用时日志语句足够廉价——理想情况下一条被禁用的日志语句应当退化为一次条件判断日志参数在消息未启用时绝不求值。由于 C 缺乏内建的惰性求值手段这通常意味着必须借助预处理器宏来实现。易于针对代码库的特定区域调节日志级别——这本质上要求提供分层日志类别模型让一个服务可以为其核心功能开启较高日志级别同时为所依赖的库保持较低级别。对比常见的 glogglog 满足了第一个目标却不满足第二个而多数其他 C 日志库恰好相反。folly logging 希望同时满足两者。补充阅读仓库中的 docs/Comparisons.md 专门讨论了与其他日志库的对比细节。二、日志类别Log Category模型2.1 类别名称是分层的所有日志消息都会记录到某个特定日志类别。类别名称以.作为层级分隔符folly.io与folly.futures都是folly的子类别folly.io.async是folly.io的子类别根类别root category名称为空字符串也可写作.参见 docs/LogCategories.md。每个日志类别除根类别外都有一个父类别并可能拥有零个或多个子类别。类别层级完全由名称推导例如spacesim是spacesim.ships的父类别。一个按源码目录组织类别的示例. --- spacesim --- ships --- corvette -- cpp \ \ \- h | \- cruiser -- cpp | \- h | \- actors --- player -- cpp \ \- h \- ai --- enemy -- cpp \- h在源码层面LogCategory.h 中通过parent_、firstChild_、nextSibling_三个指针把类别组织成一棵树并禁止拷贝与移动因为对象内部保存了父子/兄弟指针。Logger只是一个轻量包装行为类似指向LogCategory的指针从而允许多个Logger对象共享同一个类别实例。2.2 准入检查admittance check与有效级别当一条消息记录到某类别时会执行一次准入检查比较消息的日志级别与该类别的有效级别effective level。默认情况下一个类别的有效级别是其自身级别与其所有父类别级别的最小值。这意味着当你提高某个类别的日志详细程度即降低其最低启用级别时其整棵子类别树都会自动跟着提高。例如把folly类别级别设为WARN那么folly下的所有子类别只会放行WARN及以上消息如果再把folly.io级别设为DEBUG则folly.io下的所有类别会放行DEBUG及以上消息而folly的其他子类别仍然只放行WARN及以上。从源码看LogCategory.h有效级别存放在std::atomicLogLevel effectiveLevel_中logCheck()使用std::memory_order_relaxed加载以追求最低开销代价是级别调整不会立即对所有线程生效需要等其他操作触发内存屏障后才被观察到——这是有意的取舍。getEffectiveLevel()则使用memory_order_acquire语义更保守适合一般调用方。2.3 关闭继承inherit你也可以为特定类别关闭父级级别的继承从而在保持某个大类别的较高日志详细度的同时让个别子类别保持较低详细度。例如folly级别设为DEBUG但禁用folly.futures的级别继承则folly.futures的有效级别只取决于它自己本地设置的级别而不会采用父级folly的DEBUG。源码中继承标志存放在level_原子变量的最高位FLAG_INHERIT 0x80000000见 LogCategory.h与日志级别值本身打包在同一个std::atomicuint32_t里。setLevel(LogLevel level, bool inherit true)即对应此机制LogCategory.h。调试模式下folly::kIsDebug的DFATAL语义同样可见于 LogLevel.h。2.4 消息向上传播propagation一旦消息被准入它会依次交给该类别自身及所有父类别一直到根类别处理。因此把LogHandler安装在根类别上它会收到所有类别的全部已准入消息安装在子类别上则只处理该子树的消息处理器拿到的是完整的LogMessage对象可以自行按级别或其他属性做进一步过滤。另外LogCategory::setPropagateLevelMessagesToParent()允许设置一个级别阈值只有达到该级别的消息才向父类别传播LogLevel::MAX_LEVEL可用来阻止本类别及其子树的日志重复出现在父类别的处理器中典型场景是子类别已将日志导向不同目的地。默认值为LogLevel::MIN_LEVEL即所有消息都向上传播。三、日志级别Log Levels可用级别定义于 folly/logging/LogLevel.h按数值升序排列数值越大越重要级别说明FATAL必然中止程序不可被禁用若未配置任何处理器消息会打印到 stderr避免程序静默退出DFATAL类似FATAL但仅在 debug 构建未定义NDEBUG时中止程序CRITICAL重要的错误消息介于ERR与FATAL之间ERR错误消息命名为ERR而非ERROR是因为常见 Windows 头文件会把ERROR定义为预处理器宏WARN/WARNING警告消息WARNING是WARN的别名INFO信息性消息DBG0~DBG910 个编号调试级别数值越大越啰嗦DBG0比DBG9更重要。把类别级别设为DBG5会启用DBG0~DBG5以及所有更高级别禁用DBG6~DBG9DEBUG/DBG位于DBG9之下把类别级别设为DEBUG会自动启用所有编号DBG级别源码细节LogLevel.hINFO是默认日志级别kDefaultLogLevel LogLevel::INFOFATAL数值为0x7fffffff与MAX_LEVEL相同DFATAL为0x7ffffffe最高位被LogCategory用作标志位因此任何合法级别值都必须小于MAX_LEVEL有static_assert保证枚举值之间支持operator/operator-做整数调整结果会封顶在MAX_LEVELstringToLogLevel()/logLevelToString()提供字符串与级别的双向转换配置解析依赖它们。四、日志宏从 XLOG 到 FB_LOGF宏存在的根本原因在于惰性求值如果消息被禁用日志表达式包括参数构造根本不会执行。所有宏定义于 xlog.h 与 Logger.h。4.1XLOG()自动选择类别大多数场景直接使用XLOG()XLOG(INFO) hello world!;它根据当前源文件路径自动计算类别名把路径中的目录分隔符替换为.。例如源文件src/tiefighter/thruster.cpp的默认类别就是src.tiefighter.thruster.cpp。注意类别名基于编译器的__FILE__宏因此编译时传入的路径参数会直接影响类别名。如果只传bar.cpp而不带目录前缀给编译器类别名就只是bar。文档建议始终从项目仓库根目录发起编译以保证XLOG()类别名稳定。可用XLOG_SET_CATEGORY_NAME()在.cpp文件的顶层作用域覆盖本文件的默认类别名不要把它用在头文件里否则会影响所有包含该头文件的.cpp文件。4.2FB_LOG()显式指定类别如果想把消息记入显式命名的类别使用FB_LOG()其第一个参数是一个folly::Logger对象folly::Logger eventLogger(eden.events); FB_LOG(eventLogger, INFO) something happened;FB_LOG()定义于 Logger.h。4.3 参数拼接函数式与流式XLOG()/FB_LOG()的额外参数会用folly::tostd::string()转成字符串并拼接XLOG(INFO, the number is , 2 2); // 输出 the number is 4两种风格甚至可以混用XLOG(INFO, the number is ) 2 2; XLOG(DBG2) streaming syntax is also supported: 1234;4.4XLOGF()/FB_LOGF()fmt 格式化XLOGF()/FB_LOGF()使用fmt::format()提供 Python 风格的格式化语法XLOGF(DBG1, cannot engage {} thruster: {}, thruster.name(), err.what());4.5 条件与编译期裁剪XLOG_IF(level, cond, ...)/XLOGF_IF(...)仅当cond为真时记录且条件只在级别检查通过后才被求值见 xlog.hFOLLY_XLOG_MIN_LEVEL可在编译期把所有低于该级别的XLOG语句整体裁剪掉其值不能高于或等于FATAL有static_assert强制见 xlog.hXLOG系列底层由 LogStreamProcessor 实现它利用operator的运算符优先级在宏展开中正确触发实际记录。4.6 运行示例仓库提供了可直接编译运行的示例 folly/logging/example/main.cpp#include folly/init/Init.h #include folly/logging/Init.h #include folly/logging/example/lib.h #include folly/logging/xlog.h using namespace example; // 在 main() 之前调用 XLOG() 也是安全的 // 此时会使用 folly::initializeLoggerDB() 的默认日志设置。 static ExampleObject staticInitialized(static); // 配置 folly 启用 INFO其余所有类别启用 WARNING // 默认处理器异步记录WARNING 及以上同步记录。 FOLLY_INIT_LOGGING_CONFIG( .WARNING,follyINFO; default:asynctrue,sync_levelWARNING); int main(int argc, char* argv[]) { // 调用 folly::initLogging() 之前使用日志宏会采用默认设置 // INFO 消息打印到 stderr。 XLOG(DBG) log messages less than INFO will be ignored before initLogging; XLOG(ERR) error messages before initLogging() will be logged to stderr; // folly::Init 会根据 FOLLY_INIT_LOGGING_CONFIG 声明和 // --logging 命令行 flag 自动初始化日志设置。 folly::Init init(argc, argv); // 本文件所有 XLOG() 语句记录到类别 folly.logging.example.main XLOG(INFO, now the normal log settings have been applied); XLOG(DBG1, log arguments are concatenated: , 12345, , , 92.0); XLOGF(DBG1, XLOGF supports {}-style formatting: {:.3f}, python, 1.0 / 3); XLOG(DBG2) streaming syntax is also supported: 1234; XLOG(DBG2, if you really want, , you can even) mix function-style and streaming syntax: 42; XLOGF(DBG3, and {} can mix {} style, you, format) and streaming; ExampleObject(foo); XLOG(INFO) main returning; return 0; }配套的 lib.cpp 展示了析构函数中的XLOGF(DBG1, ...)用法其类别名为folly.logging.example.lib。示例同时说明folly::Init会依据FOLLY_INIT_LOGGING_CONFIG声明以及--logging命令行 flag 自动完成日志初始化initLogging()的封装见 folly/logging/Init.h。五、Log Handler 处理器5.1 职责与默认行为LogHandler对象可附加到任意日志类别接收发往该类别及其子类别的所有消息。它可以执行任意动作写本地文件、打印到stderr/stdout、或发送到远程日志服务。处理器可以自行做额外的级别检查但默认处理收到的全部消息接口定义见 LogHandler.h。5.2 内置类型stream与file配置中可用stream类型写stdout/stderrmyhandlerstream:streamstderrfile类型把消息追加到磁盘文件用path指定路径myhandlerfile:path/var/log/my.log安全提示file处理器默认不会被folly::initLogging()注册它允许按配置向任意文件追加内容。只有当配置字符串来源可信时才应启用若程序以高权限运行而低权限用户可写配置文件该处理器存在安全隐患。需要显式启用时可在initLogging()之前调用folly::LoggerDB::get()-registerHandlerFactory( std::make_uniquefolly::FileHandlerFactory());对应的工厂实现见 FileHandlerFactory.h 与 StreamHandlerFactory.h。5.3 处理器选项内置处理器支持以下通用选项asynctrue时在独立线程异步写日志false时在产生消息的线程内同步写。asynctrue程序永远不会因写日志而阻塞但消息积压超过缓冲区时会丢弃消息追上进度后会打印一条说明丢弃了多少条消息。asyncfalse不丢消息代价是可能阻塞正常处理。额外好处是程序崩溃时所有日志都已落盘而asynctrue下崩溃可能丢失少量最新消息例如线程 A 刚记完日志就解引用空指针I/O 线程可能尚未刷盘。max_buffer_size仅在asynctrue下生效指定内存中可缓冲的未刷盘日志数据上限字节数。新消息会导致超限时会被整体丢弃——消息要么完整保留要么完整丢弃绝不保留半条。异步写入实现见 AsyncLogWriter.h 与 AsyncFileWriter.h。sync_level配合asynctrue使用指定一个级别阈值该级别及以上的消息仍以同步方式写入确保高优先级日志在潜在崩溃前一定落盘同时让低级别日志保持非阻塞。formatter控制消息格式化。当前唯一内置 formatter 是glog格式类似 glog见 GlogStyleFormatter.h未来可扩展也可自行实现LogFormatter。5.4 默认处理器默认情况下initLogging()创建一个名为default的处理器安装在根类别上把所有消息以 glog 风格格式写到stderr且async默认关闭即默认不启动独立日志 I/O 线程。高性能程序若想避免日志导致性能抖动可开启异步WARN; default:asynctrue六、日志配置基础语法与 JSON 语法folly::parseLogConfig()把配置字符串解析为LogConfigLogConfigParser.h再通过LoggerDB::get().updateConfig()增量应用或LoggerDB::get().resetConfig()整体替换folly::initLogging()则提供从命令行 flag 或配置文件一次性初始化的便捷入口。配置语法完整记录于 docs/Config.md。6.1 基础语法Basic Configuration Syntax最简单形态是用逗号分隔的CATEGORYLEVEL对follyINFO,folly.io.asyncDBG2单独写一个级别名则作用于根类别WARN完整语法要点配置串用分号分段第一个分号之前是类别配置逗号分隔其后每段定义一个处理器配置基础语法不支持任何转义类别名含逗号/分号等特殊字符时必须改用 JSON 格式类别配置形式为NAMELEVEL:HANDLER1:HANDLER2省略NAME时作用于根类别根类别也可显式写空字符串或.NAME:LEVEL表示关闭该类别对父级别的继承省略:及处理器列表时更新配置不改变该类别现有处理器显式写一个空处理器列表末尾孤立的:则会清空该类别处理器处理器配置形式为NAMETYPE:OPTION1VALUE1,OPTION2VALUE2选项列表传给对应类型的LogHandlerFactory省略TYPE写作NAME:OPTIONS表示更新已存在处理器的指定选项未提及的选项保持不变——因此这种形式只能用于updateConfig()不能用于resetConfig()。官方给出的示例配置串原文完整保留配置串效果ERROR根类别级别设为ERR配置串中ERROR是ERR的别名follyINFO,folly.ioDBG2folly设为INFOfolly.io设为DBG2follyDBG2,folly.io:INFOfolly设为DBG2folly.io设为INFO且禁止继承即使父类别启用了DBG2folly.io的DBG2消息也会被丢弃folly:WARNfolly设为WARN并禁止继承默认级别通常为INFO适合压制某个话痨组件ERROR:stderr, follyINFO; stderrstream:streamstderr根类别级别ERROR且使用stderr处理器folly级别INFO定义一个写 stderr 的处理器ERROR:x,follyINFO:y;xstream:streamstderr;yfile:path/tmp/y.log定义xstderr与y文件 /tmp/y.log两个处理器分别挂到根类别与follyERROR:default:x; defaultstream:streamstderr; xfile:path/tmp/x.log根类别同时使用default与x两个处理器ERROR:根类别级别ERR且清空其处理器列表与不写:的区别后者保留现有处理器;defaultstream:streamstdout不改变任何类别设置仅定义一个写 stdout 的default处理器ERROR; stderr:asynctrue根类别ERR并把已存在的stderr处理器async置为 true只能用于updateConfig()INFO; default:asynctrue,sync_levelWARN根类别INFOdefault处理器开启异步且WARN及以上消息同步写入以保证崩溃前落盘6.2 JSON 配置语法若配置串以{开头允许前导空白parseLogConfig()会按 JSON 对象解析也可用parseLogConfigJson()强制按 JSON 解析。JSON 解析采用宽松模式允许 C/C 风格注释和尾随逗号。顶层对象支持两个可选成员categories与handlers其他成员被忽略。categories类别名到配置的对象映射。每个类别配置为对象字段包括level必填字符串或正整数inherit可选默认true是否继承父级别propagate可选默认最小级别向父类别传播的消息的最低级别类别值也可以是纯字符串或整数等价于只设置level且inherit开启。handlers处理器名到配置的对象映射。每个处理器配置为对象字段包括type处理器类型名必须对应已注册到LoggerDB的LogHandlerFactory缺省时表示更新已存在处理器options会合并进现有配置options可选传给工厂的字符串到字符串映射。JSON 示例来自文档含注释与尾逗号的宽松语法支持{ categories: { foo: { level: INFO, handlers: [stderr] }, foo.only_fatal: { level: FATAL, inherit: false } } handlers: { stderr: { type: stream, options: { stream: stderr, async: true, sync_level: WARN, max_buffer_size: 4096000 } } } }注意JSON 示例中的handlers字段目前尚未在类别配置对象中使用类别通过handlers数组引用已定义的处理器文档中的类别字段以level、inherit、propagate为准。6.3 程序化配置内部LogConfig类承载全部配置信息你可以直接构造LogConfig对象再经updateConfig()/resetConfig()应用也可以直接操作LogCategory对象上的级别等设置。虽然可以手工创建LogHandler并挂到类别上但文档明确建议通过updateConfig()/resetConfig()来做——手工创建的处理器没有名称与类型信息LoggerDB::getConfig()将无法完整描述它。七、自定义 Log Handler 与扩展点自定义LogHandler类完全可行LogHandlerFactoryAPI 允许从配置串解析出的设置创建自定义处理器类型并通过LoggerDB::get()-registerHandlerFactory()注册。其中 StandardLogHandler 是一个把消息处理拆成两步的通用实现先用LogFormatter格式化再用LogWriter写出。因此只想定制“格式化”或“写出”任一步骤时只需提供对应的LogFormatter或LogWriter无需实现完整处理器StandardLogHandlerFactory 可用来基于自定义 formatter/writer 快速实现自己的处理器工厂。八、其他优势安全性细节8.1 默认转义不可打印字符folly logging 默认对日志消息中的不可打印字符进行转义使直接记录任意输入数据更安全避免其中可能携带的恶意终端转义序列。这有助于防范 CVE-2013-1862 与 CVE-2009-4496 这类终端注入类漏洞。8.2 多行日志消息处理LogMessage类会标记消息是否包含内部换行方便处理器给每一行都加上日志头避免后续行缺少正确的日志头前缀。九、小结一条日志的完整旅程把上述机制串起来一条日志的生命周期是XLOG(INFO)等宏先做一次廉价级别检查logCheck()relaxed 内存序读有效级别未通过则整条语句直接跳过参数不求值通过后构造LogMessage交给所在类别的admitMessage()消息依次经本类别及所有父类别的处理器处理可被propagate阈值截断处理器按formatter格式化由LogWriter同步或异步、按max_buffer_size/sync_level策略写出FATAL/DFATAL级别在相应构建配置下最终触发程序中止。十、进一步阅读folly/logging/README.md —— 本文主体来源库的概览与设计目标docs/Overview.md —— 特性总览docs/Usage.md —— 宏用法详解docs/LogCategories.md —— 类别层级与传播语义docs/LogLevels.md —— 级别语义docs/Config.md —— 配置语法完整参考docs/LogHandlers.md —— 处理器与选项源码类别与级别见 LogCategory.h、LogLevel.h宏见 xlog.h、Logger.h可运行示例见 folly/logging/example/main.cpp【免费下载链接】follyAn open-source C library developed and used at Facebook.项目地址: https://gitcode.com/GitHub_Trending/fol/folly创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考