本文使用C语言实现了一套基础的日志库。
目录
一.日志库简介
二.项目思路及函数实现
三.相关头文件及编译方式
一.日志库简介
日志库是大型项目里问题定位,运维监控,安全审计,数据复盘的核心基础和重要依据。本文使用C语言基于单例设计模式编写一个小型日志库。实现了日志安全等级分类,设置等级,打开日志文件,以及写入日志文件以及终端打印。
二.项目思路及函数实现
日志库主要基于单例模式,保证全局只有一个LogSingleton实例,同时对外开放Get_Log()接口用来获取函数,在日志内部要定义一个全局互斥锁,使得不同线程访问同一个日志文件时不会冲突。
首先定义LogSingleton数据类型,主要包括安全等级,日志文件及路径,以及全局互斥锁。
typedef struct LogSingleton{
int log_level;
int enable;//1-开启,0-关闭
pthread_mutex_t mutex;
FILE* log_file;
char log_path[256];
}LogSingleton;
然后是日志安全级别的定义,包括ERROR,INFO,DEBUG三个级别。
#define LOG_LEVEL_ERROR 0
#define LOG_LEVEL_INFO 1
#define LOG_LEVEL_DEBUG 2
#define LOG_LEVEL_MAX 3
然后要初始化全局使用的日志对象,同时提供访问接口Get_Log()。
static LogSingleton Logger = {
.log_level = LOG_LEVEL_INFO,
.enable = 1,
.mutex = PTHREAD_MUTEX_INITIALIZER,//初始化锁
.log_file = NULL,
.log_path = "./app.log"
};
LogSingleton* Get_Log(void){
return &Logger;
}
然后是日志中不可缺少的格式化时间的函数,该函数使用time.h将日志记录时间规范输出。将输出的格式化时间字符串存到time_buf中。
//格式化输出时间,函数将拼接好的时间字符串存到time_buf里
void GetLogTime(char* time_buf,int buf_len){
if(time_buf == NULL || buf_len < 20) return;
time_t now = time(NULL);//返回时间戳
struct tm* t = localtime(&now);
if(t == NULL){
printf("时间获取失败!\\n");
return;
}
snprintf(time_buf, buf_len, "[%04d-%02d-%02d %02d:%02d:%02d]",
t->tm_year + 1900,
t->tm_mon + 1,
t->tm_mday,
t->tm_hour,
t->tm_min,
t->tm_sec);
}
下面是设置日志安全及级别的函数,同时要注意设置级别是要使用互斥锁,避免线程问题。
//设置日志级别
void SetLogLevel(int level){
LogSingleton* logger = Get_Log();
if(logger == NULL) return;
if(level < 0 || level >= LOG_LEVEL_MAX){
printf("日志级别非法!\\n");
return;
}
pthread_mutex_lock(&logger->mutex);
logger->log_level = level;
pthread_mutex_unlock(&logger->mutex);
printf("日志级别已经设为%s\\n",(level == 0)?"ERROR":((level == 1)?"INFO":"DEBUG"));
}
下面是打开日志记录文件的函数,同时注意要将日志文件路径复制到全局单例中,使得单例模式中的这个文件路径是一直有效的。
//打开日志文件
int OpenLogFile(const char* path){
LogSingleton* logger = Get_Log();
if(logger == NULL || path == NULL || strlen(path) == 0){
printf("日志路径非法!\\n");
return -1;
}
pthread_mutex_lock(&logger->mutex);
if(logger->log_file != NULL){
fclose(logger->log_file);
logger->log_file = NULL;
}
//将文件路径复制到全局的log单例中
strncpy(logger->log_path,path,sizeof(logger->log_path)-1);
logger->log_path[sizeof(logger->log_path)-1] = '\\0';//添加\\0使得字符串结束
logger->log_file = fopen(logger->log_path,"a+");
if(logger->log_file == NULL){
printf("日志 %s 打开失败!\\n",logger->log_path);
pthread_mutex_unlock(&logger->mutex);
return -1;
}
pthread_mutex_unlock(&logger->mutex);
printf("日志文件打开成功!\\n");
return 0;
}
下面的函数是日志的核心函数,用于构建日志的核心输出内容。首先将日志内容进行拼接,包括时间和安全级别和输出内容。并同时将内容写入日志文件和终端。
void PrintLog(const char* msg, int level, …){
LogSingleton* logger = Get_Log();
if(logger == NULL || logger->enable == 0) return;
if(logger->log_level < level) return;
char time_buf[32] = {0};
GetLogTime(time_buf,sizeof(time_buf));
const char* level_str = NULL;
switch(level){
case LOG_LEVEL_ERROR:
level_str = "[ERROR]";
break;
case LOG_LEVEL_INFO:
level_str = "[INFO]";
break;
case LOG_LEVEL_DEBUG:
level_str = "[DEBUG]";
break;
default:
level_str = "[UNKNOWN]";
}
//处理可变参数
char log_content[1024] = {0};
va_list args;
va_start(args, level); // 初始化可变参数列表
vsnprintf(log_content, sizeof(log_content), msg, args); // 格式化
va_end(args); // 释放参数列表
char finally_log[1024 + 32 + 8] = {0};
snprintf(finally_log,sizeof(finally_log),"%s %s:%s\\n",time_buf,level_str,log_content);
//输出控制台和文件
pthread_mutex_lock(&logger->mutex);
printf("%s",finally_log);
if(logger->log_file != NULL){
fputs(finally_log,logger->log_file);
fflush(logger->log_file);//立即刷盘
}
pthread_mutex_unlock(&logger->mutex);
}
下面定义了三个不同安全级别的输出宏,可以直接输出ERROR,INFO,DEBUG三个级别的信息,使得用户使用日志更加方便。
#define LOG_ERROR(msg, …) PrintLog(msg, LOG_LEVEL_ERROR, ##__VA_ARGS__)
#define LOG_INFO(msg, …) PrintLog(msg, LOG_LEVEL_INFO, ##__VA_ARGS__)
#define LOG_DEBUG(msg, …) PrintLog(msg, LOG_LEVEL_DEBUG, ##__VA_ARGS__)
最后就是关闭日志文件函数,同时要释放互斥锁的资源。
void CloseLogFile(void){
LogSingleton* logger = Get_Log();
if(logger == NULL) return;
pthread_mutex_lock(&logger->mutex);
if(logger->log_file != NULL){
fclose(logger->log_file);
logger->log_file = NULL;
printf("日志已经关闭!\\n");
}
pthread_mutex_unlock(&logger->mutex);
pthread_mutex_destroy(&logger->mutex);
}
上面就是完整的基于单例模式的日志库实现。
三.相关头文件及编译方式
日志库的头文件如下
#pragma once
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <pthread.h>
#include <stdarg.h>
#define LOG_LEVEL_ERROR 0
#define LOG_LEVEL_INFO 1
#define LOG_LEVEL_DEBUG 2
#define LOG_LEVEL_MAX 3
typedef struct LogSingleton LogSingleton;
LogSingleton* Get_Log(void);
void GetLogTime(char* time_buf,int buf_len);
void SetLogLevel(int level);
int OpenLogFile(const char* path);
void PrintLog(const char* msg, int level, …);
#define LOG_ERROR(msg, …) PrintLog(msg, LOG_LEVEL_ERROR, ##__VA_ARGS__)
#define LOG_INFO(msg, …) PrintLog(msg, LOG_LEVEL_INFO, ##__VA_ARGS__)
#define LOG_DEBUG(msg, …) PrintLog(msg, LOG_LEVEL_DEBUG, ##__VA_ARGS__)
void CloseLogFile(void);
下面是将上述日志库编译为动态链接库的shell脚本,命名为log.sh。
#!/bin/bash
#首先将log.c编译为log.o,必须加-fPIC参数
gcc -c -fPIC log.c -o log.o
#将log.o打包为liblog.so,必须加-shared参数
gcc -shared -o liblog.so log.o -lpthread
#删除中间文件
rm -f log.o
然后别忘记需要给上面的log.sh脚本添加可执行权限.
chmod +x log.sh
这样就完成了一个基于单例模式的日志库了,当然真实的日志库要比这个更加复杂。
本人新手,如有错误,欢迎各位大佬指正。




