本文基于本仓库 vendored 的 EasyLogger v2.2.0 源码(
easylogger/EasyLogger-2.2.0/)以及官方文档撰写,所有 API 与示例均可在该版本上直接编译运行。官方链接见文末「官方资源汇总」。
做 开发的朋友,几乎都踩过同一个坑:程序跑得越久越慢,最后发现是日志在拖后腿。
原因是很多人图省事直接用 syslog(),而 syslog 走的是 Unix domain socket 同步 IPC,每次调用都要陷入内核、等守护进程处理。在低端 Cortex-A7 上,单条 syslog() 调用轻松 50~100µs,高频打印直接把业务线程堵死。
本文要讲的 EasyLogger,是 RT-Thread 社区大牛 armink 的作品。它的设计哲学和 syslog 完全相反:只负责把日志格式组装好,然后用一个环形缓冲区 + 后台线程(或自己的 sink)把"业务线程"和"真正的落盘"解耦。结果就是:业务线程打日志几乎零等待,慢速的文件/串口/Flash 写入交给后台线程慢慢做。
为什么在 Arm Linux 上值得用它?
.c 加进工程编就行,交叉编译也只需要一个 -lpthread。⚠️ 适用边界:EasyLogger 是单例、全局配置的库(配置和输出后端全进程只有一套)。它不适合"同一进程里不同模块要输出到完全不同地方并各自独立配置"的复杂场景——那种需求看 zlog。它最适合"进程级统一日志 + 轻量高性能"的嵌入式 Linux 业务。
0 [A] 断言 Assert1 [E] 错误 Error2 [W] 警告 Warn3 [I] 信息 Info4 [D] 调试 Debug5 [V] 详细 Verbose两级过滤机制(理解这个,调日志才不懵):
elog_cfg.h 里的 ELOG_OUTPUT_LVL。低于该级别的代码在编译时直接被替换成空。elog_set_filter_lvl() 等 API。低于该级别的日志在运行时被丢弃。优先级:ELOG_OUTPUT_ENABLE(总开关) > ELOG_OUTPUT_LVL(静态) > elog_set_filter_*(动态)。
EasyLogger 提供了两套宏,本质一样,只是一个要每次写 tag,一个靠文件顶部预先定义的 LOG_TAG:
/* 方式一:elog_x(tag, ...) —— 每次显式传标签 */elog_a("net", "assert: %s", expr); /* 注意第一个参数是 tag */elog_e("net", "connect failed, errno=%d", errno);elog_w("net", "retry later");elog_i("net", "connected");elog_d("net", "send seq=%u", seq);elog_v("net", "heartbeat");/* 方式二:log_x(...) —— 文件顶部先 #define LOG_TAG "net", 之后无需再写 tag,LOG_TAG 自动填入 */#define LOG_TAG "net"#include <elog.h>log_e("connect failed, errno=%d", errno); /* 等价于 elog_e("net", ...) */log_i("connected");实战经验:每个
.c文件顶部按模块定义不同的LOG_TAG(如"wifi.proto"、"can.disp"),再统一用log_x(),日志天然按模块分组,后期过滤/排查极方便。还可以配合LOG_LVL做文件级静态级别控制(详见官方 API 文档「技巧二」)。
下面这些都在 easylogger/inc/elog.h 声明,是最常用的几个:
ElogErrCode elog_init(void) | |
void elog_start(void) | |
void elog_set_fmt(uint8_t level, size_t set) | |
void elog_set_filter_lvl(uint8_t level) | |
void elog_set_filter_tag(const char *tag) | |
void elog_set_filter_kw(const char *kw) | |
void elog_set_filter_tag_lvl(const char *tag, uint8_t lvl) | 按模块(标签)单独设级别 |
void elog_set_output_enabled(bool en) | |
void elog_set_text_color_enabled(bool en) | ELOG_COLOR_ENABLE) |
void elog_raw(const char *fmt, ...) | |
void elog_hexdump(const char *name, uint8_t width, uint8_t *buf, uint16_t size) | |
void elog_buf_enabled(bool en)void elog_flush(void) | |
void elog_async_enabled(bool en) |
格式位宏(ELOG_FMT_*):ELOG_FMT_LVL、ELOG_FMT_TAG、ELOG_FMT_TIME、ELOG_FMT_P_INFO(进程)、ELOG_FMT_T_INFO(线程)、ELOG_FMT_DIR、ELOG_FMT_FUNC、ELOG_FMT_LINE,以及组合 ELOG_FMT_ALL。
典型初始化:
setbuf(stdout, NULL); /* 关掉 stdout 缓冲,避免日志看不到 */elog_init();/* 不同级别给不同格式:错误/警告带时间,调试给全量 */elog_set_fmt(ELOG_LVL_ERROR, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);elog_set_fmt(ELOG_LVL_WARN, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME);elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~ELOG_FMT_FUNC);elog_set_fmt(ELOG_LVL_VERBOSE,ELOG_FMT_ALL & ~ELOG_FMT_FUNC);#ifdef ELOG_COLOR_ENABLEelog_set_text_color_enabled(true);#endifelog_start();💡 想临时拉高某个模块的调试级别,不用重新编译:
elog_set_filter_tag_lvl("wifi", ELOG_LVL_DEBUG);打开 wifi 模块全部调试日志;elog_set_filter_tag_lvl("wifi", ELOG_FILTER_LVL_SILENT);直接静默它。
EasyLogger 的边界非常清晰:库本身只做"过滤 + 格式化",真正的输出(往哪打、怎么加锁、时间从哪来)全部由实现的一个 elog_port.c 决定。所以移植 = 改一个配置头 + 写一个 port 文件。
easylogger/├── inc/│ ├── elog.h # 公共 API(不要改)│ └── elog_cfg.h # ★ 唯一要改的配置文件├── src/│ ├── elog.c # 核心│ ├── elog_utils.c # 工具│ ├── elog_async.c # 异步模式(按需加入编译)│ └── elog_buf.c # 缓冲模式(按需加入编译)├── port/│ └── elog_port.c # ★ 移植接口(Linux 下自己实现)└── plugins/ └── file/ # 文件输出插件(可选) ├── elog_file.c ├── elog_file.h ├── elog_file_cfg.h # 文件插件配置 └── elog_file_port.c导入到工程的动作(官方移植文档第 2 章):
easylogger/ 整个目录拷进工程;src/elog.c、src/elog_utils.c、port/elog_port.c 加入编译(elog_async.c/elog_buf.c 视是否开启异步/缓冲决定);easylogger/inc/ 加入头文件搜索路径;plugins/file/elog_file.c、elog_file_port.c 加入编译,并把 plugins/ 加入搜索路径。
elog_cfg.h(关键宏)下面是一份 Arm Linux 用户态推荐配置(在官方 Linux demo 配置基础上加了中文注释):
/* ===== 总开关与级别 ===== */#define ELOG_OUTPUT_ENABLE /* 总开关,关掉则所有日志变空 */#define ELOG_OUTPUT_LVL ELOG_LVL_VERBOSE /* 静态级别:编译期最低可见级 */#define ELOG_ASSERT_ENABLE /* 开启断言检查 *//* ===== 缓冲区与过滤长度 ===== */#define ELOG_LINE_BUF_SIZE 512 /* 单行日志最大字符数 */#define ELOG_LINE_NUM_MAX_LEN 5 /* 行号最大位数 */#define ELOG_FILTER_TAG_MAX_LEN 16 /* 标签最大长度 */#define ELOG_FILTER_KW_MAX_LEN 16 /* 关键词最大长度 */#define ELOG_FILTER_TAG_LVL_MAX_NUM 5 /* 支持多少组"标签+级别"过滤 */#define ELOG_NEWLINE_SIGN "\n"/* 换行符,可改 "\r\n" *//* ===== 彩色 ===== */#define ELOG_COLOR_ENABLE /* 终端彩色(需配合 elog_set_text_color_enabled(true)) *//* ===== 异步输出(强烈推荐开启) ===== */#define ELOG_ASYNC_OUTPUT_ENABLE /* 开启异步模式 */#define ELOG_ASYNC_OUTPUT_LVL ELOG_LVL_DEBUG /* 高于此级的日志走同步,保证高优先级实时性 */#define ELOG_ASYNC_OUTPUT_BUF_SIZE (ELOG_LINE_BUF_SIZE * 100) /* 环形缓冲 51KB */#define ELOG_ASYNC_OUTPUT_USING_PTHREAD /* 用 pthread 实现后台刷出线程 *//* #define ELOG_ASYNC_LINE_OUTPUT *//* 可选:保证每次 port_output 都是完整的一行 *//* ===== 文件插件(可选) ===== *//* #define ELOG_FILE_ENABLE *//* 取消注释即启用文件输出插件 *//* #define ELOG_FILE_FLUSH_CAHCE_ENABLE *//* 注意:上游宏名就是 CAHCE(拼写如此),开启即每次 fflush+fsync */注意
ELOG_FILE_FLUSH_CAHCE_ENABLE的拼写——上游源码里就是CAHCE,照抄即可,别自己"修正"成 CACHE,否则宏不生效。
elog_port.c(Linux 版)这是移植的核心。需要提供的接口(官方移植文档第 3 章):
elog_port_init() | ||
elog_port_output(log, size) | ||
elog_port_output_lock() | ||
elog_port_output_unlock() | ||
elog_port_get_time() | ELOG_TIME 时用) | |
elog_port_get_p_info() | ||
elog_port_get_t_info() |
下面是一份生产可用的基础 port(终端 + 进程/线程信息),也是示例 1、2 用的:
/* elog_port.c —— Arm Linux 用户态基础移植(终端输出) */#include <elog.h>#include <stdio.h>#include <pthread.h>#include <unistd.h>#include <time.h>/* 进程内线程安全锁(保护输出不被多线程交错撕碎) */static pthread_mutex_t s_lock = PTHREAD_MUTEX_INITIALIZER;ElogErrCode elog_port_init(void) {/* 基础移植无需额外资源,预留扩展点 */return ELOG_NO_ERR;}/* 日志最终出口:这里打到终端。要写文件/串口,在此扩展即可 */void elog_port_output(const char *log, size_t size) {printf("%.*s", (int)size, log);}void elog_port_output_lock(void) { pthread_mutex_lock(&s_lock); }void elog_port_output_unlock(void) { pthread_mutex_unlock(&s_lock); }/* 时间戳:启用 ELOG_TIME_ENABLE 时会被调用 */const char *elog_port_get_time(void) {static char buf[24] = {0};time_t t = time(NULL);struct tm *tm = localtime(&t);if (tm == NULL) return "";snprintf(buf, sizeof(buf), "%02d-%02d %02d:%02d:%02d", tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);return buf;}/* 进程信息:多进程场景用它一眼看出日志来自哪个进程 */const char *elog_port_get_p_info(void) {static char buf[16] = {0};snprintf(buf, sizeof(buf), "pid:%04d", (int)getpid());return buf;}/* 线程信息:多线程场景区分日志来源 */const char *elog_port_get_t_info(void) {static char buf[16] = {0};snprintf(buf, sizeof(buf), "tid:%04ld", (long)pthread_self());return buf;}把时间、进程、线程信息都打开后,一条日志会长这样:
\033[32;22mI/var 04-18 11:20:01 pid:1234 tid:5678 main: hello\033[0m级别、时间、pid、tid、tag 一目了然,多进程多线程排障时省太多事。
想让日志落盘,启用 ELOG_FILE_ENABLE 并补全两个文件:
elog_file_cfg.h(插件自己的配置):
#define ELOG_FILE_NAME "/var/log/myapp.log"/* 日志文件路径 */#define ELOG_FILE_MAX_SIZE (1024 * 1024) /* 单文件最大 1MB */#define ELOG_FILE_MAX_ROTATE 5 /* 超过后滚动 5 个备份 */elog_file_port.c(插件移植,加锁即可):
#include "elog_file.h"ElogErrCode elog_file_port_init(void) { return ELOG_NO_ERR; }void elog_file_port_lock(void) { /* 进程内锁,需要可加 pthread_mutex */ }void elog_file_port_unlock(void) { /* 与上面配对 */ }void elog_file_port_deinit(void) { /* 清理 */ }启用后,基础 port 的 elog_port_output 里顺便调一句 elog_file_write(log, size); 即可同时写文件(官方 Linux demo 的 elog_port.c 就是这样做的)。
# 本机编译(示例 1/2 用到的文件)gcc main.c \ easylogger/src/elog.c easylogger/src/elog_utils.c \ easylogger/src/elog_async.c easylogger/src/elog_buf.c \ elog_port.c \ -I./easylogger/inc -lpthread -o app# 启用文件插件时再加:# easylogger/plugins/file/elog_file.c easylogger/plugins/file/elog_file_port.c# -I./easylogger/plugins -I./easylogger/plugins/file# Arm Linux 交叉编译(把 gcc 换成工具链前缀即可)arm-linux-gnueabihf-gcc main.c \ easylogger/src/elog.c easylogger/src/elog_utils.c \ easylogger/src/elog_async.c easylogger/src/elog_buf.c \ elog_port.c \ -I./easylogger/inc -lpthread -o app_arm下面三个示例都基于上面的 elog_port.c。编译时请确保 elog_cfg.h 里 ELOG_ASYNC_OUTPUT_ENABLE 已开(否则异步相关行为不同)。
最基础的用法:初始化 → 打各级别日志 → hexdump 一段报文 → 退出。
/* * 示例一:单线程(终端输出 + hexdump + 动态过滤) * 编译:make demo_single 运行:./demo_single */#define LOG_TAG "main"#include <elog.h>#include <stdio.h>#include <unistd.h>int main(void) {/* 关掉 stdout 缓冲,避免日志“攒批”看不到 */ setbuf(stdout, NULL); elog_init();/* 不同级别给不同格式:信息级带 级别/标签/时间/进程,调试级给全量 */ elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME | ELOG_FMT_P_INFO); elog_set_fmt(ELOG_LVL_DEBUG, ELOG_FMT_ALL & ~ELOG_FMT_FUNC);#ifdef ELOG_COLOR_ENABLE elog_set_text_color_enabled(true);#endif elog_start(); log_a("assert demo"); /* 一般只在真断言时出现 */ log_e("open %s failed, errno=%d", "/dev/ttyS1", 2); log_w("cpu load high: %d%%", 92); log_i("system started, pid=%d", getpid()); log_d("rx buffer free=%u", 1024u); log_v("heartbeat tick");/* 协议调试:把一帧原始数据 dump 出来 */uint8_t frame[16];for (int i = 0; i < 16; i++) { frame[i] = (uint8_t)(i * 16 + i); } elog_hexdump("uart_rx", 16, frame, sizeof(frame));/* 动态调级演示:只保留 WARN 及以上 */ elog_set_filter_lvl(ELOG_LVL_WARN); log_i("这条 INFO 不会被输出"); log_w("这条 WARN 仍可见");/* ★ 异步模式退出注意:EasyLogger 2.2.0 没有“等待异步刷完”的 API。 本程序会马上退出,若不稍等,分离的异步后台线程会被一起杀掉, 环形缓冲里最后的日志就丢了。长时间运行的服务无需担心(线程持续排空)。 演示/工具类程序可用下面两种方式之一: 1) 像这里留一点排空时间; 2) 调 elog_async_enabled(false) 切回同步。 */ usleep(200000);return 0;}
编译运行:
gcc demo_single.c easylogger/src/elog.c easylogger/src/elog_utils.c \ easylogger/src/elog_async.c easylogger/src/elog_buf.c elog_port.c \ -I./easylogger/inc -lpthread -o demo_single./demo_singleEasyLogger 在 Linux 下是线程安全的,前提是 elog_port_output_lock/unlock 用了互斥锁(上面 port 已经做了)。异步模式下,真正的写文件/终端是后台 pthread 在做,业务线程只是往环形缓冲塞数据,几乎无锁竞争。
下面起 3 个线程,每个循环打 50 条日志,验证不会"串味":
/* * 示例二:多线程(N 个线程并发打日志) * 编译:make demo_mt 运行:./demo_mt * * 结论:EasyLogger 在 Linux 下是线程安全的,前提是 elog_port_output_lock/unlock * 用了互斥锁(基础 port 已经做了)。异步模式下真正的写终端由后台 pthread * 完成,业务线程只往环形缓冲塞数据,几乎无锁竞争。 */#define LOG_TAG "mt"#include <elog.h>#include <stdio.h>#include <pthread.h>#include <unistd.h>static void *worker(void *arg) {long id = (long)arg;for (int i = 0; i < 50; i++) { log_i("thread-%ld loop-%d", id, i); usleep(1000); }return NULL;}int main(void) { setbuf(stdout, NULL); elog_init(); elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_T_INFO | ELOG_FMT_P_INFO);#ifdef ELOG_COLOR_ENABLE elog_set_text_color_enabled(true);#endif elog_start();pthread_t t[3];for (long i = 0; i < 3; i++) { pthread_create(&t[i], NULL, worker, (void *)i); }for (int i = 0; i < 3; i++) { pthread_join(t[i], NULL); } log_i("all threads done");/* 同 demo_single:退出前给异步后台线程留排空时间,避免最后几条日志丢失 */ usleep(200000);return 0;}实战经验:
多线程下不要在 elog_port_output里做耗时极长的同步 IO 且不开异步——开了异步后重活都在后台线程,业务线程瞬间返回。环形缓冲写满时,EasyLogger 会丢弃超出部分(不会阻塞业务),所以极端高频场景要酌情调大 ELOG_ASYNC_OUTPUT_BUF_SIZE,或把最关键的日志级别设到ELOG_ASYNC_OUTPUT_LVL以下走同步,确保不丢。进程退出前,若担心最后几条还在缓冲里没刷完,可让主线程在 return前sleep一小会儿,给后台线程排空时间(2.2.0 没有提供"等待异步刷完"的 API,这是最稳妥的做法)。
这是最容易踩坑的地方,先把原理讲清:
elog_init() + elog_start(),否则子进程既没初始化,也没有那个后台刷出线程(fork 只复制调用线程,异步线程不会被复制)。pthread_mutex 只保护"进程内"的并发,挡不住另一个进程的写入交错。正确做法:打开文件时用 O_APPEND(Linux 对 write() 到普通文件保证原子追加),再叠加 flock() 做跨进程互斥。open() 文件,得到独立的"文件描述/锁",否则父子共享同一个 flock 锁对象,反而失去互斥效果。下面给一份多进程安全的 port + 示例(不依赖文件插件,自己用 open+flock 落盘,最直观):
/* elog_port_mp.c —— 多进程安全移植:共享文件 + flock */#include <elog.h>#include <stdio.h>#include <pthread.h>#include <unistd.h>#include <time.h>#include <fcntl.h>#include <sys/file.h>static pthread_mutex_t s_lock = PTHREAD_MUTEX_INITIALIZER; /* 进程内 */static int g_fd = -1; /* 各进程独立打开 */ElogErrCode elog_port_init(void) {/* 每个进程各自 open 一次,拿到独立的文件描述,flock 才能跨进程互斥 */ g_fd = open("/var/log/elog_mp.log", O_WRONLY | O_CREAT | O_APPEND, 0644);return ELOG_NO_ERR;}void elog_port_output(const char *log, size_t size) {/* 终端也打一份,方便现场看 */printf("%.*s", (int)size, log);/* 文件:flock 跨进程加锁 + O_APPEND 原子追加,保证不交错、不丢字节 */if (g_fd >= 0) { flock(g_fd, LOCK_EX); write(g_fd, log, size); flock(g_fd, LOCK_UN); }}void elog_port_output_lock(void) { pthread_mutex_lock(&s_lock); }void elog_port_output_unlock(void) { pthread_mutex_unlock(&s_lock); }const char *elog_port_get_time(void) {static char buf[24] = {0};time_t t = time(NULL); struct tm *tm = localtime(&t);if (tm == NULL) return "";snprintf(buf, sizeof(buf), "%02d-%02d %02d:%02d:%02d", tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);return buf;}const char *elog_port_get_p_info(void) {static char buf[16] = {0};snprintf(buf, sizeof(buf), "pid:%04d", (int)getpid());return buf;}const char *elog_port_get_t_info(void) {static char buf[16] = {0};snprintf(buf, sizeof(buf), "tid:%04ld", (long)pthread_self());return buf;}/* demo_mp.c —— 父进程 fork 出 2 个子进程,三者并发写同一日志文件 *//* * 示例三:多进程(fork 后各自独立实例,并发写同一日志文件) * 编译:make demo_mp 运行:./demo_mp ; 之后 cat /var/log/elog_mp.log * * ★★★ 多进程必读:fork 与异步模式的坑 ★★★ * 1) EasyLogger 是“进程内”库:缓冲区、异步后台线程、配置都在进程私有内存。 * fork 之后子进程必须重新 elog_init() + elog_start(),否则子进程既没有 * 初始化,也没有后台刷出线程(fork 只复制调用线程)。 * 2) 更隐蔽的坑:elog_async.c 里的 init_ok 是 static,父进程初始化后置为 * true,fork 把它复制到子进程;于是子进程 elog_init() 看到 init_ok==true * 会直接返回,不会创建异步线程 → 子进程的日志塞进环形缓冲却永远没人排空, * 最后全丢。所以本例在父、子进程里都显式 elog_async_enabled(false), * 改成同步输出(写文件这种慢操作交给端口里的 flock,本就不靠后台线程)。 * 3) 写同一文件必须跨进程互斥:用 open(O_APPEND) + flock(),且每个进程各自 * open(见 elog_port_mp.c),不能继承父进程的 fd。 */#define LOG_TAG "mp"#include <elog.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>int main(void) { setbuf(stdout, NULL);/* 父进程先初始化(会 open 自己的 fd) */ elog_init(); elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME | ELOG_FMT_P_INFO);#ifdef ELOG_COLOR_ENABLE elog_set_text_color_enabled(true);#endif elog_start();/* 见上方说明:父进程也关闭异步,避免 fork 后子进程异步线程缺失导致丢日志 */ elog_async_enabled(false); log_i("parent ready, going to fork");for (int c = 0; c < 2; c++) {pid_t pid = fork();if (pid == 0) {/* ===== 子进程:必须重新 init/start ===== */ elog_init(); /* 内部再次 open,获得独立 fd/锁 */ elog_set_fmt(ELOG_LVL_INFO, ELOG_FMT_LVL | ELOG_FMT_TAG | ELOG_FMT_TIME | ELOG_FMT_P_INFO);#ifdef ELOG_COLOR_ENABLE elog_set_text_color_enabled(true);#endif elog_start(); elog_async_enabled(false); /* 关键:子进程强制同步输出 */for (int i = 0; i < 30; i++) { log_i("child-%d iter-%d", c, i); usleep(5000); } log_i("child-%d exit", c);exit(0); } }/* 父进程自己也打一些 */for (int i = 0; i < 30; i++) { log_i("parent iter-%d", i); usleep(5000); }/* 收尸,确保子进程日志都刷完 */int status;while (wait(&status) > 0) { /* 等待所有子进程 */ } log_i("all children reaped, parent exit");return 0;}编译运行:
gcc demo_mp.c easylogger/src/elog.c easylogger/src/elog_utils.c \ easylogger/src/elog_async.c easylogger/src/elog_buf.c elog_port_mp.c \ -I./easylogger/inc -lpthread -o demo_mp./demo_mpcat /var/log/elog_mp.log # 父/子进程的日志按行完整、互不交错多进程更简单的替代方案(推荐小项目用):每个进程写自己的文件,文件名带 PID(如
/var/log/myapp_%d.log)。完全不需要跨进程锁,排查时grep也清爽。上面的flock共享文件方案适合"希望所有进程日志汇聚到一个文件"的场景。
printf 直接刷盘)。EasyLogger 开了异步后,业务线程只是内存拷贝,几乎不阻塞。ELOG_ASYNC_OUTPUT_BUF_SIZE;关键日志设到 ELOG_ASYNC_OUTPUT_LVL 以下走同步保不丢。elog_port_output_lock/unlock 用互斥锁(Linux demo 已做)。fork 后子进程一定要重新 elog_init()+elog_start(),否则既没有初始化也没有后台刷出线程。pthread_mutex 管不了别的进程,用 open(O_APPEND)+flock,或干脆每进程一个文件。ELOG_OUTPUT_LVL 是编译期裁剪(更狠、零运行时开销),elog_set_filter_lvl() 是运行期过滤(可随时改)。线上想"临时拉满某模块日志",用 elog_set_filter_tag_lvl("模块名", ELOG_LVL_DEBUG)。elog_cfg.h 关掉 ELOG_COLOR_ENABLE,或仅对终端后端启用。-lpthread、头文件路径包含 easylogger/inc(以及插件目录)。https://github.com/armink/EasyLoggerhttps://git.oschina.net/armink/EasyLoggerhttps://github.com/armink/EasyLogger/blob/master/docs/zh/api/kernel.mdhttps://github.com/armink/EasyLogger/blob/master/docs/zh/port/kernel.mdhttps://github.com/armink/EasyLogger/tree/master/easylogger/plugins/fileeasylogger/EasyLogger-2.2.0/(API 以本地源码为准)elog_cfg.h + 写 elog_port.c,零第三方依赖。elog_init() → elog_set_fmt(...) → elog_start(),少一步都不输出。elog_x(tag,...) 或 log_x(...)(配合文件顶部 LOG_TAG)。init 且写共享文件要 flock。ELOG_OUTPUT_LVL,运行期 elog_set_filter_*,按模块用 elog_set_filter_tag_lvl。