嵌入式学习 day30:minilog

基于前几天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 写日志

步骤

  1. 校验日志等级是否合法(DEBUG/INFO/WARN/ERROR)

  2. 检查 logs/ 目录是否存在,不存在则创建

  3. 以追加模式打开日志文件(O_CREAT | O_APPEND

  4. 获取当前时间并格式化为 [2024-01-15 10:30:45] 格式

  5. 拼接日志行:时间 [等级] 内容\n

  6. 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知识的掌握逐渐加深,并更深入了解了如何完成一个项目。

相关推荐
嵌入式小能手1 小时前
飞凌嵌入式ElfBoard-Shell脚本编写基础-传参-$0
linux
丰锋ff1 小时前
初识Linux操作系统
linux·运维·服务器
xiaoxiangsiyan1 小时前
现代大型虚拟化智慧园区整体架构EVPN‑VXLAN落地全解
运维·网络·学习·架构
嵌入式小能手1 小时前
飞凌嵌入式ElfBoard-Shell脚本编写基础-特殊变量
linux·运维·服务器
mlidongfeng2 小时前
[AI][昇腾950]TP(Transport Layer,传输层)学习笔记
笔记·学习
小弥儿2 小时前
GPT Image 2 提示词库,把散落的生图提示词变成工程资产
开发语言·javascript·gpt·学习
汇智信科2 小时前
Java 开发汇智网络化学习系统 (E‑Learning) 技术特性与应用场景解析
人工智能·学习·架构·汇智信科·fastclaw·汇智龙虾
poiu12346573 小时前
中医执业医师考试如何选择培训课程|2026主流机构客观测评
学习
MartinYeung53 小时前
[论文学习]BadRobot:物理世界中具身大语言模型智能体的越狱攻击
人工智能·学习·语言模型