基于前几天IO的学习,完成一个小项目。
一、项目概述
MiniLog是一个运行在Linux终端的轻量级日志记录与查询工具,使用C语言和POSIX文件I/O实现。支持写入不同等级日志、查看全部日志、按等级查询、统计数量等功能。
二、项目需求分析
| 需求 | 说明 |
|---|---|
| 写日志 | 支持DEBUG/INFO/WARN/ERROR四个等级,格式为[时间] [等级] 内容 |
| 查看全部 | list命令输出所有日志 |
| 按等级查询 | find命令按等级过滤 |
| 日志统计 | count命令统计各等级数量 |
| 命令行解析 | 支持-f指定路径和默认路径两种模式 |
| 错误处理 | 参数错误、文件不存在等情况给出友好提示 |
命令行格式:
| 命令 | 格式 |
|---|---|
| 写入 | ./minilog write INFO "content" |
| 写入(指定路径) | ./minilog -f /path/to/log write INFO "content" |
| 查看全部 | ./minilog list |
| 按等级查询 | ./minilog find ERROR |
| 统计 | ./minilog count |
| 最后N行 | ./minilog tail 10 |
| 帮助 | ./minilog help |
三、模块划分
text
minilog/
├── main.c # 主入口,命令行解析
├── log.c # 日志写入功能
├── query.c # 查询功能(list/find/count/tail/help)
├── log.h # 头文件(结构体声明、函数声明、宏定义)
├── Makefile
└── README.md
main.c 负责根据用户输入分发到对应的功能函数,log.c 只负责写入,query.c 负责各种查询。
四、核心功能实现
4.1 写日志
步骤:
-
校验日志等级是否合法(DEBUG/INFO/WARN/ERROR)
-
检查
logs/目录是否存在,不存在则创建 -
以追加模式打开日志文件(
O_CREAT | O_APPEND) -
获取当前时间并格式化为
[2024-01-15 10:30:45]格式 -
拼接日志行:
时间 [等级] 内容\n -
write()写入文件
c
int WriteLog(char *level, char *content, char *filename)
{
if(strcmp(level, "INFO") != 0 && strcmp(level, "DEBUG") != 0 && strcmp(level, "WARN") != 0 && strcmp(level, "ERROR") != 0)
{
HelpLog();
return -1;
}
char tmpfile[32] = {0};
strcpy(tmpfile, filename);
char *dirname = strtok(tmpfile, "/"); //logs
char *logname = strtok(NULL, "/"); //app.log
//创建目录
int ret = access(dirname, F_OK);
if(ret == -1)
{
mkdir(dirname, 0755);
}
//创建日志
int fp = 0;
char tmpname[1024] = {0};
sprintf(tmpname, "%s/%s", dirname, logname);
fp = open(tmpname, O_RDWR | O_CREAT | O_APPEND, 0644);
if(fp == -1)
{
perror("fail to open");
return -1;
}
//获取当前时间
time_t now = time(NULL); // 当前时间戳
struct tm *tm_info = localtime(&now); // 转成可读结构体
char time_str[32];
strftime(time_str, sizeof(time_str), "[%Y-%m-%d %H:%M:%S]", tm_info);
//写入日志
char write_str[1024] = {0};
sprintf(write_str, "%s [%s] %s\n", time_str, level, content);
lseek(fp, 0, SEEK_END);
write(fp, write_str, strlen(write_str));
close(fp);
return 0;
}
4.2 查看全部日志
open() 打开文件,循环 read() 读取并 write(1, ...) 输出到屏幕。
c
int ListLog(char *filename)
{
//查询文件是否存在
int ret = access(filename, F_OK);
if(ret == -1)
{
printf("no file!\n");
return -1;
}
//打开文件
int fp = 0;
fp = open(filename, O_RDONLY);
if(fp == -1)
{
perror("fail to open");
return -1;
}
//输出到屏幕
char read_str[1024] = {0};
ssize_t fp_ret = 0;
while(1)
{
fp_ret = read(fp, read_str, sizeof(read_str));
if(fp_ret <= 0)
break;
write(1, read_str, strlen(read_str));
}
close(fp);
return 0;
}
4.3 按等级查询
fopen()/fgets() 逐行读取,用 strtok() 切分出等级字段,与目标等级比较,匹配则输出。
c
int FindLog(char *level, char *filename)
{
if(strcmp(level, "INFO") != 0 && strcmp(level, "DEBUG") != 0 && strcmp(level, "WARN") != 0 && strcmp(level, "ERROR") != 0)
{
HelpLog();
return -1;
}
//打开文件
FILE *fp = 0;
fp = fopen(filename, "r");
if(fp == NULL)
{
perror("fail to open");
return -1;
}
//查找对应等级
int ret = 0;
while(1)
{
char tmpstr[1024] = {0};
char tmplevel[32] = {0};
char tmpwrite[1024] = {0};
char *token = NULL;
token = fgets(tmpstr, sizeof(tmpstr), fp);
if(token == NULL)
break;
strcpy(tmpwrite, tmpstr);
strtok(tmpstr, " ");
strtok(NULL, " ");
token = strtok(NULL, " "); //获取当前日志等级
sprintf(tmplevel, "[%s]", level);
if(strcmp(token, tmplevel) == 0)
{
write(1, tmpwrite, strlen(tmpwrite));
ret++;
}
}
//没有对应日志
if(ret == 0)
{
printf("no targer log!\n");
}
fclose(fp);
return 0;
}
4.4 日志统计
逐行读取,根据等级字段分别累加,最后输出各等级数量和总数。
c
int CountLog(char *filename)
{
int debug = 0;
int info = 0;
int warn = 0;
int error = 0;
int total = 0;
//打开文件
FILE *fp = 0;
fp = fopen(filename, "r");
if(fp == NULL)
{
perror("fail to open");
return -1;
}
//统计对应数量
while(1)
{
char tmpstr[1024] = {0};
char *token = NULL;
token = fgets(tmpstr, sizeof(tmpstr), fp);
if(token == NULL)
break;
strtok(tmpstr, " ");
strtok(NULL, " ");
token = strtok(NULL, " ");
if(strcmp(token, "[DEBUG]") == 0)
debug++;
else if(strcmp(token, "[INFO]") == 0)
info++;
else if(strcmp(token, "[WARN]") == 0)
warn++;
else if(strcmp(token, "[ERROR]") == 0)
error++;
}
total = debug + info + warn + error;
printf("DEBUG:%d\nINFO :%d\nWARN :%d\nERROR:%d\nTOTAL:%d\n", debug, info, warn, error, total);
fclose(fp);
return 0;
}
4.5 最后N行
lseek() 从文件末尾向前移动,统计换行符个数,找到第N个换行符的位置后从那里开始读取并输出。
c
int TailLog(char *num, char *filename)
{
int count = atoi(num);
int i = 0;
int fp = 0;
fp = open(filename, O_RDONLY);
if(fp == -1)
{
perror("fail to open");
return -1;
}
//移动光标至倒数第n行开头
char tmpbuff[1024] = {0};
ssize_t ret = 0;
off_t cur = 0;
lseek(fp, -1, SEEK_END);
while(i<=count)
{
ret = read(fp, tmpbuff, 1);
if(ret <= 0)
break;
if(tmpbuff[0] == '\n')
i++;
cur = lseek(fp, -1, SEEK_CUR);
//防止越界
if(cur > 0)
lseek(fp, -1, SEEK_CUR);
else if(cur == 0)
break;
}
if(cur > 0)
lseek(fp, 2, SEEK_CUR);
//越界判断
if(i < count)
{
printf("Out of range!\n");
return -1;
}
//打印后续日志
while(1)
{
ret = read(fp, tmpbuff, sizeof(tmpbuff));
if(ret <= 0)
break;
write(1, tmpbuff, ret);
}
return 0;
}
4.6 主函数
c
int main(int argc, char *argv[])
{
if(argc < 2)
{
HelpLog();
return -1;
}
/*
指定路径操作
*/
if(strcmp(argv[1], "-f") == 0)
{
char filename[32] = {0};
char *dirname = strtok(argv[2], "/");
char *logname = strtok(NULL, "/");
sprintf(filename, "%s/%s", dirname, logname);
if(strcmp(argv[3], "write") == 0)
{
if(argc < 6)
HelpLog();
else
WriteLog(argv[4], argv[5], filename);
}
else if(strcmp(argv[3], "list") == 0)
{
ListLog(filename);
}
else if(strcmp(argv[3], "find") == 0)
{
if(argc < 5)
HelpLog();
else
FindLog(argv[4], filename);
}
else if(strcmp(argv[3], "count") == 0)
{
CountLog(filename);
}
else if(strcmp(argv[3], "help") == 0)
{
HelpLog();
}
else if(strcmp(argv[3], "tail") == 0)
{
if(argc < 5)
HelpLog();
else
TailLog(argv[4], filename);
}
else
HelpLog();
}
/*
默认路径操作
*/
else
{
if(strcmp(argv[1], "write") == 0)
{
if(argc < 4)
HelpLog();
else
WriteLog(argv[2], argv[3], "logs/app.log");
}
else if(strcmp(argv[1], "list") == 0)
{
ListLog("logs/app.log");
}
else if(strcmp(argv[1], "find") == 0)
{
if(argc < 3)
HelpLog();
else
FindLog(argv[2], "logs/app.log");
}
else if(strcmp(argv[1], "count") == 0)
{
CountLog("logs/app.log");
}
else if(strcmp(argv[1], "help") == 0)
{
HelpLog();
}
else if(strcmp(argv[1], "tail") == 0)
{
if(argc < 3)
HelpLog();
else
TailLog(argv[2], "logs/app.log");
}
else
HelpLog();
}
return 0;
}
4.7 命令行解析
需要处理两种模式:
| 模式 | 示例 |
|---|---|
| 默认路径 | ./minilog write INFO "boot" → 写入 ./logs/app.log |
| 指定路径 | ./minilog -f /tmp/device.log write INFO "boot" → 写入指定文件 |
五、今日感悟
做这个项目之前,文件IO对我来说就是一堆函数接口------open、read、write、lseek,知道怎么用但没真正写过。通过这个项目,我把整套流程走了一遍,从创建目录、打开文件、写入日志到查询统计,每个功能都成功实现,使我对IO知识的掌握逐渐加深,并更深入了解了如何完成一个项目。