378 lines
11 KiB
C
378 lines
11 KiB
C
/**
|
||
* @file myLog.c
|
||
* @brief 异步日志实现 — 消息池 + 生产者/消费者 + 批量写入 + 文件轮转
|
||
*
|
||
* 架构概述:
|
||
* 调用 log_prt() → 从消息池取空闲块 → 填充内容 → 入队(生产者)
|
||
* 工作线程 → 批量出队(消费者) → 控制台彩色输出 + 文件写入
|
||
* 首次调用 pthread_once 自动启动工作线程
|
||
*/
|
||
|
||
#include "myLog.h"
|
||
#include "myBase.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <stdarg.h>
|
||
#include <time.h>
|
||
#include <sys/time.h>
|
||
#include <sys/stat.h>
|
||
#include <unistd.h>
|
||
#include <errno.h>
|
||
#include <pthread.h>
|
||
#include <stdlib.h>
|
||
|
||
/* ========== 可配置参数 ========== */
|
||
#define POOL_SIZE 10000 /* 消息池大小 */
|
||
#define BATCH_MAX 100 /* 单次批量处理上限 */
|
||
#define FLUSH_MS 100 /* 空闲等待间隔(毫秒) */
|
||
#define FILE_MAX_SIZE (10*1024*1024) /* 单文件 10MB */
|
||
#define FILE_MAX_KEEP 5 /* 保留历史文件数 */
|
||
#define CONTENT_MAX 4096 /* 单条日志内容上限 */
|
||
|
||
/* ========== 数据结构 ========== */
|
||
|
||
/** 单条日志消息 */
|
||
typedef struct log_msg {
|
||
uint32_t level;
|
||
char time_str[64];
|
||
char src_info[128]; /* "文件名/函数:行号" */
|
||
char content[CONTENT_MAX];
|
||
struct log_msg *next; /* 空闲链表 / 队列链表 */
|
||
} log_msg_t;
|
||
|
||
/** 线程安全队列 */
|
||
typedef struct {
|
||
log_msg_t *head, *tail;
|
||
int count;
|
||
pthread_mutex_t lock;
|
||
pthread_cond_t cond_pop; /* 消费者等待 */
|
||
pthread_cond_t cond_push; /* 生产者等待(队列满) */
|
||
} msg_queue_t;
|
||
|
||
/** 日志系统全局状态 */
|
||
LOCAL struct {
|
||
msg_queue_t q;
|
||
pthread_t worker;
|
||
volatile int running;
|
||
FILE *fp;
|
||
long file_bytes;
|
||
int to_file;
|
||
int to_console;
|
||
char dir[256];
|
||
} g_log;
|
||
|
||
/* ========== 消息池(预分配,无运行时 malloc) ========== */
|
||
LOCAL log_msg_t *g_pool_free; /* 空闲链表头 */
|
||
LOCAL log_msg_t g_pool_buf[POOL_SIZE]; /* 预分配数组 */
|
||
LOCAL pthread_mutex_t g_pool_lock = PTHREAD_MUTEX_INITIALIZER;
|
||
LOCAL pthread_once_t g_init_once = PTHREAD_ONCE_INIT;
|
||
|
||
/** 初始化消息池:把数组元素链成空闲链表 */
|
||
LOCAL void pool_init(void)
|
||
{
|
||
pthread_mutex_lock(&g_pool_lock);
|
||
if (g_pool_free != NULL) { pthread_mutex_unlock(&g_pool_lock); return; }
|
||
for (int i = 0; i < POOL_SIZE - 1; i++)
|
||
g_pool_buf[i].next = &g_pool_buf[i + 1];
|
||
g_pool_buf[POOL_SIZE - 1].next = NULL;
|
||
g_pool_free = &g_pool_buf[0];
|
||
pthread_mutex_unlock(&g_pool_lock);
|
||
}
|
||
|
||
/** 从池中取一个空闲消息块(失败返回 NULL) */
|
||
LOCAL log_msg_t *pool_alloc(void)
|
||
{
|
||
pthread_mutex_lock(&g_pool_lock);
|
||
log_msg_t *m = g_pool_free;
|
||
if (m) g_pool_free = m->next;
|
||
pthread_mutex_unlock(&g_pool_lock);
|
||
return m;
|
||
}
|
||
|
||
/** 归还消息块到池 */
|
||
LOCAL void pool_free(log_msg_t *m)
|
||
{
|
||
if (!m) return;
|
||
pthread_mutex_lock(&g_pool_lock);
|
||
m->next = g_pool_free;
|
||
g_pool_free = m;
|
||
pthread_mutex_unlock(&g_pool_lock);
|
||
}
|
||
|
||
/* ========== 队列操作 ========== */
|
||
|
||
/** 入队(满则阻塞) */
|
||
LOCAL void q_push(log_msg_t *m)
|
||
{
|
||
pthread_mutex_lock(&g_log.q.lock);
|
||
while (g_log.q.count >= POOL_SIZE)
|
||
pthread_cond_wait(&g_log.q.cond_push, &g_log.q.lock);
|
||
m->next = NULL;
|
||
if (!g_log.q.head) g_log.q.head = m;
|
||
else g_log.q.tail->next = m;
|
||
g_log.q.tail = m;
|
||
g_log.q.count++;
|
||
pthread_cond_signal(&g_log.q.cond_pop);
|
||
pthread_mutex_unlock(&g_log.q.lock);
|
||
}
|
||
|
||
/** 出队(空则等待或返回 NULL) */
|
||
LOCAL log_msg_t *q_pop(void)
|
||
{
|
||
pthread_mutex_lock(&g_log.q.lock);
|
||
while (g_log.q.count == 0 && g_log.running)
|
||
pthread_cond_wait(&g_log.q.cond_pop, &g_log.q.lock);
|
||
if (g_log.q.count == 0) { pthread_mutex_unlock(&g_log.q.lock); return NULL; }
|
||
log_msg_t *m = g_log.q.head;
|
||
g_log.q.head = m->next;
|
||
if (!g_log.q.head) g_log.q.tail = NULL;
|
||
g_log.q.count--;
|
||
pthread_cond_signal(&g_log.q.cond_push);
|
||
pthread_mutex_unlock(&g_log.q.lock);
|
||
return m;
|
||
}
|
||
|
||
/* ========== 文件管理 ========== */
|
||
|
||
/** 拼接日志文件路径:dir/app.log 或 dir/app.log.N */
|
||
LOCAL void file_path(char *buf, size_t sz, int idx)
|
||
{
|
||
if (idx == 0)
|
||
snprintf(buf, sz, "%s/app.log", g_log.dir);
|
||
else
|
||
snprintf(buf, sz, "%s/app.log.%d", g_log.dir, idx);
|
||
}
|
||
|
||
/** 创建日志目录 */
|
||
LOCAL int ensure_dir(void)
|
||
{
|
||
if (!g_log.dir[0]) {
|
||
/* 默认放到 /tmp/logs */
|
||
snprintf(g_log.dir, sizeof(g_log.dir), "/tmp/logs");
|
||
}
|
||
struct stat st;
|
||
if (stat(g_log.dir, &st) != 0)
|
||
return mkdir(g_log.dir, 0755);
|
||
return 0;
|
||
}
|
||
|
||
/** 轮转日志文件:app.log→.1→.2→...→.N(max),删除最旧的 */
|
||
LOCAL void rotate_files(void)
|
||
{
|
||
char old[512], neo[512];
|
||
if (g_log.fp) { fclose(g_log.fp); g_log.fp = NULL; }
|
||
|
||
/* 删除最旧 */
|
||
file_path(old, sizeof(old), FILE_MAX_KEEP);
|
||
remove(old);
|
||
|
||
/* 依次后移 */
|
||
for (int i = FILE_MAX_KEEP - 1; i >= 1; i--) {
|
||
file_path(old, sizeof(old), i);
|
||
file_path(neo, sizeof(neo), i + 1);
|
||
rename(old, neo);
|
||
}
|
||
|
||
/* 当前文件 → .1 */
|
||
file_path(old, sizeof(old), 0);
|
||
file_path(neo, sizeof(neo), 1);
|
||
rename(old, neo);
|
||
g_log.file_bytes = 0;
|
||
}
|
||
|
||
/** 打开当前日志文件 */
|
||
LOCAL FILE *open_file(void)
|
||
{
|
||
if (ensure_dir() != 0) return NULL;
|
||
char path[512];
|
||
file_path(path, sizeof(path), 0);
|
||
|
||
/* 检查已有文件大小 */
|
||
struct stat st;
|
||
if (stat(path, &st) == 0) {
|
||
if (st.st_size > FILE_MAX_SIZE)
|
||
rotate_files();
|
||
else
|
||
g_log.file_bytes = st.st_size;
|
||
}
|
||
|
||
FILE *fp = fopen(path, "a");
|
||
if (fp) setlinebuf(fp); /* 行缓冲 */
|
||
return fp;
|
||
}
|
||
|
||
/* ========== 格式化 ========== */
|
||
|
||
/** 当前时间字符串:YYYY-MM-DD HH:MM:SS.mmm */
|
||
LOCAL void fmt_time(char *buf, size_t sz)
|
||
{
|
||
struct timeval tv;
|
||
gettimeofday(&tv, NULL);
|
||
struct tm *t = localtime(&tv.tv_sec);
|
||
if (t) {
|
||
snprintf(buf, sz, "%04d-%02d-%02d %02d:%02d:%02d.%03d",
|
||
t->tm_year+1900, t->tm_mon+1, t->tm_mday,
|
||
t->tm_hour, t->tm_min, t->tm_sec,
|
||
(int)(tv.tv_usec / 1000));
|
||
} else {
|
||
snprintf(buf, sz, "----.--.-- --:--:--.---");
|
||
}
|
||
}
|
||
|
||
/** 提取文件名(去掉路径) 并拼接成 "file/func:line" */
|
||
LOCAL void fmt_src(char *buf, size_t sz,
|
||
const char *file, const char *func, uint32_t line)
|
||
{
|
||
if (!file || !func) { snprintf(buf, sz, "?:?:0"); return; }
|
||
const char *p = strrchr(file, '/');
|
||
if (!p) p = strrchr(file, '\\');
|
||
const char *name = p ? p + 1 : file;
|
||
snprintf(buf, sz, "%s/%s:%u", name, func, line);
|
||
}
|
||
|
||
/* ========== 输出 ========== */
|
||
|
||
/** 批量写控制台 + 文件 */
|
||
LOCAL void output_batch(log_msg_t **batch, int cnt)
|
||
{
|
||
if (cnt == 0) return;
|
||
static const char *titles[] = {"INFO", "ERROR"};
|
||
static const char *colors[] = {COLOR_WHITE, COLOR_RED};
|
||
|
||
/* 控制台(带颜色) */
|
||
if (g_log.to_console) {
|
||
for (int i = 0; i < cnt; i++) {
|
||
log_msg_t *m = batch[i];
|
||
printf("%s[%s] [%s%s%s] %s\n",
|
||
colors[m->level], m->time_str,
|
||
colors[m->level], titles[m->level],
|
||
COLOR_RESET, m->content);
|
||
}
|
||
fflush(stdout);
|
||
}
|
||
|
||
/* 文件 */
|
||
if (g_log.to_file) {
|
||
if (!g_log.fp) g_log.fp = open_file();
|
||
if (g_log.fp) {
|
||
for (int i = 0; i < cnt; i++) {
|
||
log_msg_t *m = batch[i];
|
||
int n = fprintf(g_log.fp, "[%s] [%s] %s\n",
|
||
titles[m->level], m->time_str, m->content);
|
||
if (n > 0) g_log.file_bytes += n;
|
||
}
|
||
fflush(g_log.fp);
|
||
if (g_log.file_bytes > FILE_MAX_SIZE) rotate_files();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ========== 工作线程 ========== */
|
||
|
||
LOCAL void *worker_thread(void *arg)
|
||
{
|
||
(void)arg;
|
||
while (g_log.running) {
|
||
log_msg_t *batch[BATCH_MAX];
|
||
int cnt = 0;
|
||
|
||
/* 批量取消息 */
|
||
for (int i = 0; i < BATCH_MAX; i++) {
|
||
log_msg_t *m = q_pop();
|
||
if (!m) break;
|
||
batch[cnt++] = m;
|
||
}
|
||
|
||
if (cnt > 0) {
|
||
output_batch(batch, cnt);
|
||
for (int i = 0; i < cnt; i++) pool_free(batch[i]);
|
||
} else {
|
||
usleep(FLUSH_MS * 1000);
|
||
}
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
/* ========== 全局初始化(pthread_once) ========== */
|
||
|
||
LOCAL void global_init(void)
|
||
{
|
||
pool_init();
|
||
/* g_log.q 已在声明时用 {0} 初始化 */
|
||
g_log.q.head = g_log.q.tail = NULL;
|
||
g_log.q.count = 0;
|
||
g_log.to_console = 1;
|
||
g_log.to_file = 0;
|
||
g_log.running = 1;
|
||
pthread_create(&g_log.worker, NULL, worker_thread, NULL);
|
||
}
|
||
|
||
/* ========== 公开 API ========== */
|
||
|
||
int log_init(const char *dir)
|
||
{
|
||
if (dir && dir[0])
|
||
snprintf(g_log.dir, sizeof(g_log.dir), "%s", dir);
|
||
pool_init(); /* 确保消息池就绪 */
|
||
g_log.to_file = 1;
|
||
return 0;
|
||
}
|
||
|
||
void log_prt(uint32_t level, const char *file, const char *func,
|
||
uint32_t line, const char *fmt, ...)
|
||
{
|
||
/* 懒初始化 */
|
||
pthread_once(&g_init_once, global_init);
|
||
if (level > LOG_ERROR || !fmt || !g_log.running) return;
|
||
|
||
/* 从消息池取空闲块 */
|
||
log_msg_t *m = pool_alloc();
|
||
if (!m) return; /* 池耗尽,丢弃本条 */
|
||
|
||
m->level = level;
|
||
fmt_time(m->time_str, sizeof(m->time_str));
|
||
fmt_src(m->src_info, sizeof(m->src_info), file, func, line);
|
||
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
int pos = snprintf(m->content, sizeof(m->content), "[%s] ", m->src_info);
|
||
if (pos < (int)sizeof(m->content))
|
||
vsnprintf(m->content + pos, sizeof(m->content) - pos, fmt, ap);
|
||
va_end(ap);
|
||
|
||
q_push(m);
|
||
}
|
||
|
||
void log_file_on(void) { g_log.to_file = 1; }
|
||
void log_file_off(void) { g_log.to_file = 0; }
|
||
void log_console_on(void) { g_log.to_console = 1; }
|
||
void log_console_off(void) { g_log.to_console = 0; }
|
||
|
||
void log_flush(void)
|
||
{
|
||
/* 轮询等待队列归零 */
|
||
for (;;) {
|
||
pthread_mutex_lock(&g_log.q.lock);
|
||
int c = g_log.q.count;
|
||
pthread_mutex_unlock(&g_log.q.lock);
|
||
if (c == 0) break;
|
||
usleep(10000);
|
||
}
|
||
}
|
||
|
||
void log_cleanup(void)
|
||
{
|
||
if (!g_log.running) return;
|
||
g_log.running = 0;
|
||
pthread_cond_signal(&g_log.q.cond_pop);
|
||
pthread_join(g_log.worker, NULL);
|
||
if (g_log.fp) { fclose(g_log.fp); g_log.fp = NULL; }
|
||
/* 释放池中残留消息 */
|
||
pthread_mutex_lock(&g_log.q.lock);
|
||
log_msg_t *cur = g_log.q.head, *tmp;
|
||
while (cur) { tmp = cur; cur = cur->next; pool_free(tmp); }
|
||
g_log.q.head = g_log.q.tail = NULL;
|
||
g_log.q.count = 0;
|
||
pthread_mutex_unlock(&g_log.q.lock);
|
||
}
|