20260730worklog


博客说明:基于 C 语言的大日志文件统计工具实践
--------------------------------------------------

一、项目背景

最近做了两个基于 C 语言的日志分析小工具,主要用于处理体积较大的设备日志文件,例如 100MB、150MB 甚至更大的日志。

这类日志如果一次性全部读入内存,不但占用资源,而且不利于后续扩展。因此整体实现采用了"逐行读取 + 关键字匹配 + 状态机统计"的方式。这样做的好处是:

  1. 内存占用低

  2. 适合处理大体积日志

  3. 逻辑清晰,便于维护

  4. 后续新增统计维度时更容易扩展

从日志内容可以看到,原始日志中同时包含本地时间和 UTC 时间,例如:

2026-05-08 18:40:41.580 | 2026-05-08 10:40:41.580 UTC | main ... 1

这也为后续按时间维度做统计提供了基础。

二、工具一:Motion 事件链路统计

  1. 功能目标

第一份代码用于分析 motion 事件在日志中的完整处理链路,并统计成功的 motion 录制次数。

主要关注的关键节点包括:

  • motion wait start

  • motion record start

  • evr task create

  • motion video upload start

  • 上传成功日志中的 _av_x_1.nvt4s

其中最后一个节点最关键。

例如:

_av_9_1.nvt4s

表示第 10 个分片上传完成,并且 1 代表这是最后一个分片,也就是说整个 motion 视频上传完成。

如果是:

_av_9_0.nvt4s

则表示第 10 个分片上传完成,但整个视频还没有全部上传成功。

因此,只有识别到最后一个分片上传成功时,才将这一组 motion 计为一次真正的成功事件。

  1. 为什么使用状态机

这些关键字不是孤立出现的,而是有严格顺序关系的。因此程序使用状态机来控制流程。

一个完整的成功链路通常是:

  1. motion wait start

  2. motion record start

  3. evr task create

  4. motion video upload start

  5. put ... _av_x_1.nvt4s ... success

只有满足这种顺序,才算一组完整的成功记录。

如果出现如下情况:

motion wait start

之后紧接着出现

motion wait timeout

则说明本次 motion 录制超时失效,这一次不会被纳入统计。

  1. 增加 mqtt payload 信息提取

后续又增加了一个新的关键日志节点:

mqtt payload... = {... "alarmId":"20260708230350" ...}

这个日志里包含两个非常重要的信息:

  1. 该行日志本身对应的 UTC 时间

  2. alarmId 的值

例如:

alarmId = 20260708230350

因此在最终输出结果中,除了记录 motion 各阶段的时间,还额外记录:

  • mqtt payload 对应的 UTC 时间

  • alarmId

这样后续排查问题时,可以更方便地把上传成功日志与业务事件关联起来。

  1. 为什么按 alarmId 统计时间

最开始可以按上传完成时间来统计每天和每小时的次数,但这种方式并不总是准确,因为上传完成时间可能滞后于实际事件发生时间。

相比之下,alarmId 更接近 motion 事件的真实触发时间。

例如:

alarmId = 20260708230350

可以直接解析为:

  • 日期:20260708

  • 小时:2026070823

所以最终 CSV 统计改成按 alarmId 做时间划分,这样得到的每天、每小时分布更准确。

  1. 输出结果设计

这份工具最终输出两类结果。

第一类是 TXT 文件,用来保存每一组成功事件的详细信息,例如:

  • motion wait start 时间

  • motion record start 时间

  • evr task create 时间

  • upload start 时间

  • upload finish 时间

  • mqtt payload UTC 时间

  • alarmId

  • 分片信息,例如 9_1

TXT 更适合单条问题排查和人工核对。

第二类是 CSV 文件,用来保存汇总统计信息,例如:

time_key,count

20260720,42

2026072010,5

2026072011,7

2026072012,8

20260721,11

2026072113,6

2026072114,3

其中:

  • 8 位表示按天统计

  • 10 位表示按小时统计

这样可以直接导入 Excel 或其他工具进行可视化分析。

三、工具二:统计 nv_xxx_main.log.gz 标题块出现次数

  1. 功能目标

第二份代码的目标比较简单,主要用于统计日志中某种固定格式标题块出现的次数,例如:


| nv_1783779133066_main.log.gz |

| 2026-07-11 14:12:13 UTC |


或者:


| nv_1783779297926_main.log.gz |

| 2026-07-11 14:14:57 UTC |


这类标题块通常用于标识某个子日志文件或某段日志区间。

  1. 统计条件

统计逻辑如下:

  • 第二行必须包含 nv_

  • 并且文件名格式满足 _main.log.gz

  • 第三行时间需要落在指定范围内

  • 例如:20260711 到 20260729

只要满足这些条件,就记为一次有效统计。

  1. 输出方式

这一份工具只输出 TXT 文件,内容包括:

  • 每个匹配到的标题块

  • 最终总次数

例如:

Count 1:


| nv_1783779133066_main.log.gz |

| 2026-07-11 14:12:13 UTC |


Count 2:


| nv_1783779297926_main.log.gz |

| 2026-07-11 14:14:57 UTC |


Total count in range 20260711 \~ 20260729: 2

四、实现思路总结

这两个工具虽然统计目标不同,但实现思路基本一致。

  1. 逐行扫描日志

程序采用逐行读取方式处理日志,非常适合大文件。相比整文件读入内存,这种方式更节省资源,也更稳定。

  1. 基于关键字匹配

通过 strstr、sscanf 等基础字符串处理方法,就可以完成大部分日志解析工作。

  1. 状态机管理流程

对于像 motion 这种存在明确先后顺序的业务链路,使用状态机可以有效避免误匹配,也更容易扩展新的状态和节点。

  1. 结果落盘输出

最终结果直接输出到日志所在目录下的 txt 或 csv 文件中,便于归档、分享和后续分析,也避免了控制台输出过多导致信息丢失的问题。

五、适用场景

这类工具适合以下场景:

  • 设备端日志分析

  • motion 事件成功率统计

  • 视频上传链路排查

  • 离线日志批量分析

  • 嵌入式设备运行日志核查

另外,从日志中也能看到上传相关信息,例如上传过程日志中包含 curl 传输速率和耗时信息 1,这类内容后续也可以继续纳入统计范围,例如:

  • 上传耗时统计

  • 上传平均速度统计

  • 上传失败率统计

六、总结

这两个小工具本质上都是围绕"日志结构化分析"来实现的。

一个主要解决复杂业务链路的统计问题;

另一个主要解决固定格式标题块的计数问题。

虽然实现上没有引入复杂框架,但对于嵌入式设备日志、边缘设备日志以及离线日志排查来说,这种"逐行扫描 + 关键字匹配 + 状态机"的实现方式通常已经足够高效,也足够实用。

后续如果需要,还可以继续扩展:

  • 支持 gzip 日志直接读取

  • 支持正则表达式匹配

  • 支持多文件批量处理

  • 支持输出更多报表格式

  • 支持接入数据库或可视化平台

复制代码
/**
**统计日志文件出现的次数 类似于
**   ----------------------------------
**     |  nv_xxx_main.log.gz  |
**     |    YYYY-MM-DD HH:MM:SS UTC     |
** ----------------------------------
**  带入年月日筛选  比如统计时间维度 20260711 到 20260729 的总次数
**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LINE 4096
#define MAX_PATH 1024

/* 判断 YYYYMMDD 是否在范围内 */
static int in_range(const char *date8, const char *start, const char *end) {
    return strcmp(date8, start) >= 0 && strcmp(date8, end) <= 0;
}

/*
 * 从这一行提取 YYYYMMDD
 * 例如:
 * |    2026-07-11 14:12:13 UTC     |
 * 提取成:
 * 20260711
 */
static int extract_date8_from_title_time(const char *line, char *date8, size_t size) {
    int y, m, d;
    if (sscanf(line, "| %d-%d-%d", &y, &m, &d) == 3) {
        snprintf(date8, size, "%04d%02d%02d", y, m, d);
        return 1;
    }
    return 0;
}

/* 生成输出文件路径:xxx_title_count.txt */
static void build_output_path(const char *log_path, char *out_path, size_t out_size) {
    const char *slash1 = strrchr(log_path, '/');
    const char *slash2 = strrchr(log_path, '\\');
    const char *slash = slash1 > slash2 ? slash1 : slash2;

    const char *filename = slash ? slash + 1 : log_path;
    char dir[MAX_PATH] = {0};
    char base[MAX_PATH] = {0};
    const char *dot = strrchr(filename, '.');

    if (slash) {
        size_t dir_len = slash - log_path + 1;
        if (dir_len >= sizeof(dir)) dir_len = sizeof(dir) - 1;
        memcpy(dir, log_path, dir_len);
        dir[dir_len] = '\0';
    }

    if (dot && dot != filename) {
        size_t base_len = dot - filename;
        if (base_len >= sizeof(base)) base_len = sizeof(base) - 1;
        memcpy(base, filename, base_len);
        base[base_len] = '\0';
    } else {
        snprintf(base, sizeof(base), "%s", filename);
    }

    snprintf(out_path, out_size, "%s%s_title_count.txt", dir, base);
}

int main(int argc, char *argv[]) {
    FILE *fp = NULL;
    FILE *out = NULL;
    char line[MAX_LINE];
    char next1[MAX_LINE];
    char next2[MAX_LINE];
    char date8[16];
    char out_path[MAX_PATH];

    const char *start_date = "20260711";
    const char *end_date   = "20260729";

    long long total_count = 0;

    if (argc < 2) {
        fprintf(stderr, "Usage: %s <log_file>\n", argv[0]);
        return 1;
    }

    fp = fopen(argv[1], "r");
    if (!fp) {
        perror("open log file failed");
        return 1;
    }

    build_output_path(argv[1], out_path, sizeof(out_path));

    out = fopen(out_path, "w");
    if (!out) {
        perror("open output file failed");
        fclose(fp);
        return 1;
    }

    while (fgets(line, sizeof(line), fp)) {
        /* 找标题块第一行 */
        if (strstr(line, "----------------------------------")) {
            long pos = ftell(fp);

            if (fgets(next1, sizeof(next1), fp) && fgets(next2, sizeof(next2), fp)) {
                /* next1: |  nv_xxx_main.log.gz  | */
                /* next2: |    2026-07-11 14:12:13 UTC     | */

                if (strstr(next1, "nv_") && strstr(next1, "_main.log.gz")) {
                    if (extract_date8_from_title_time(next2, date8, sizeof(date8))) {
                        if (in_range(date8, start_date, end_date)) {
                            total_count++;
                            fprintf(out, "Count %lld:\n", total_count);
                            fprintf(out, "%s", line);
                            fprintf(out, "%s", next1);
                            fprintf(out, "%s", next2);

                            /* 再读第四行分隔线并输出 */
                            if (fgets(next1, sizeof(next1), fp)) {
                                fprintf(out, "%s\n", next1);
                            } else {
                                fprintf(out, "\n");
                            }
                        } else {
                            fseek(fp, pos, SEEK_SET);
                        }
                    } else {
                        fseek(fp, pos, SEEK_SET);
                    }
                } else {
                    fseek(fp, pos, SEEK_SET);
                }
            }
        }
    }

    fprintf(out, "\nTotal count in range [%s ~ %s]: %lld\n",
            start_date, end_date, total_count);

    fclose(out);
    fclose(fp);

    printf("Done.\n");
    printf("Output file: %s\n", out_path);
    printf("Total count in range [%s ~ %s]: %lld\n",
           start_date, end_date, total_count);

    return 0;
}
复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LINE 16384
#define MAX_TIME 128
#define MAX_PATH 1024
#define MAX_STATS 10000
#define MAX_ALARMID 32

#define KW_WAIT_START      "motion wait start"
#define KW_WAIT_TIMEOUT    "motion wait timeout"
#define KW_RECORD_START    "motion record start"
#define KW_EVR_CREATE      "evr task create"
#define KW_UPLOAD_START    "motion video upload start"
#define KW_MQTT_PAYLOAD    "mqtt payload["

typedef enum {
    STATE_IDLE = 0,
    STATE_WAIT_STARTED,
    STATE_RECORD_STARTED,
    STATE_EVR_CREATED,
    STATE_UPLOAD_STARTED
} MotionState;

typedef struct {
    char wait_start[MAX_TIME];
    char record_start[MAX_TIME];
    char evr_create[MAX_TIME];
    char upload_start[MAX_TIME];
    char upload_finish[MAX_TIME];

    char mqtt_payload_time[MAX_TIME];
    char alarm_id[MAX_ALARMID];

    int seg_index;
    int seg_last_flag;
} MotionGroup;

typedef struct {
    char key[32];   // YYYYMMDD 或 YYYYMMDDHH
    int count;
} StatItem;

typedef struct {
    char utc_time[MAX_TIME];
    char alarm_id[MAX_ALARMID];
    int valid;
} MqttPayloadInfo;

static void reset_group(MotionGroup *g) {
    memset(g, 0, sizeof(MotionGroup));
    g->seg_index = -1;
    g->seg_last_flag = -1;
}

static int extract_utc_time(const char *line, char *out, size_t out_sz) {
    const char *p1, *p2;
    size_t len;

    if (!line || !out || out_sz == 0) return 0;

    p1 = strstr(line, " | ");
    if (!p1) return 0;
    p1 += 3;

    p2 = strstr(p1, " | ");
    if (!p2) return 0;

    len = (size_t)(p2 - p1);
    if (len >= out_sz) len = out_sz - 1;

    memcpy(out, p1, len);
    out[len] = '\0';
    return 1;
}

static int extract_av_xy(const char *line, int *x, int *y) {
    const char *p;
    if (!line || !x || !y) return 0;

    p = strstr(line, "_av_");
    if (!p) return 0;
    p += 4;

    if (sscanf(p, "%d_%d.nvt4s", x, y) == 2) {
        return 1;
    }
    return 0;
}

static int extract_alarm_id(const char *line, char *alarm_id, size_t sz) {
    const char *p = strstr(line, "\"alarmId\":\"");
    const char *q;
    size_t len;

    if (!p || !alarm_id || sz == 0) return 0;
    p += strlen("\"alarmId\":\"");

    q = strchr(p, '"');
    if (!q) return 0;

    len = (size_t)(q - p);
    if (len >= sz) len = sz - 1;

    memcpy(alarm_id, p, len);
    alarm_id[len] = '\0';
    return 1;
}

/* 从 alarmId 提取 day/hour key
 * alarmId 示例: 20260708230350
 * day  = 20260708
 * hour = 2026070823
 */
static int extract_day_hour_from_alarmid(const char *alarm_id,
                                         char *day_key, size_t day_sz,
                                         char *hour_key, size_t hour_sz) {
    if (!alarm_id) return 0;
    if (strlen(alarm_id) < 10) return 0;

    snprintf(day_key, day_sz, "%.8s", alarm_id);
    snprintf(hour_key, hour_sz, "%.10s", alarm_id);
    return 1;
}

static void add_stat(StatItem *stats, int *stat_count, const char *key) {
    int i;
    for (i = 0; i < *stat_count; i++) {
        if (strcmp(stats[i].key, key) == 0) {
            stats[i].count++;
            return;
        }
    }

    if (*stat_count < MAX_STATS) {
        strncpy(stats[*stat_count].key, key, sizeof(stats[*stat_count].key) - 1);
        stats[*stat_count].count = 1;
        (*stat_count)++;
    }
}

static int cmp_stats(const void *a, const void *b) {
    const StatItem *x = (const StatItem *)a;
    const StatItem *y = (const StatItem *)b;
    return strcmp(x->key, y->key);
}

static void write_group(FILE *out, const MotionGroup *g, long long idx) {
    fprintf(out, "Group %lld\n", idx);
    fprintf(out, "  motion wait start    : %s\n", g->wait_start[0] ? g->wait_start : "-");
    fprintf(out, "  motion record start  : %s\n", g->record_start[0] ? g->record_start : "-");
    fprintf(out, "  evr task create      : %s\n", g->evr_create[0] ? g->evr_create : "-");
    fprintf(out, "  upload start         : %s\n", g->upload_start[0] ? g->upload_start : "-");
    fprintf(out, "  upload finish        : %s\n", g->upload_finish[0] ? g->upload_finish : "-");
    fprintf(out, "  mqtt payload time    : %s\n", g->mqtt_payload_time[0] ? g->mqtt_payload_time : "-");
    fprintf(out, "  alarmId              : %s\n", g->alarm_id[0] ? g->alarm_id : "-");
    fprintf(out, "  segment              : ");
    if (g->seg_index >= 0) {
        fprintf(out, "%d_%d\n", g->seg_index, g->seg_last_flag);
    } else {
        fprintf(out, "-\n");
    }
    fprintf(out, "\n");
}

static void build_output_path(const char *log_path, const char *suffix,
                              char *out_path, size_t out_size) {
    const char *slash1 = strrchr(log_path, '/');
    const char *slash2 = strrchr(log_path, '\\');
    const char *slash = slash1 > slash2 ? slash1 : slash2;

    const char *filename = slash ? slash + 1 : log_path;
    char dir[MAX_PATH] = {0};
    char base[MAX_PATH] = {0};
    const char *dot = strrchr(filename, '.');

    if (slash) {
        size_t dir_len = slash - log_path + 1;
        if (dir_len >= sizeof(dir)) dir_len = sizeof(dir) - 1;
        memcpy(dir, log_path, dir_len);
        dir[dir_len] = '\0';
    }

    if (dot && dot != filename) {
        size_t base_len = dot - filename;
        if (base_len >= sizeof(base)) base_len = sizeof(base) - 1;
        memcpy(base, filename, base_len);
        base[base_len] = '\0';
    } else {
        snprintf(base, sizeof(base), "%s", filename);
    }

    snprintf(out_path, out_size, "%s%s%s", dir, base, suffix);
}

int main(int argc, char *argv[]) {
    FILE *fp = NULL;
    FILE *txt_out = NULL;
    FILE *csv_out = NULL;
    char line[MAX_LINE];
    char utc[MAX_TIME];
    char txt_path[MAX_PATH];
    char csv_path[MAX_PATH];

    MotionState state = STATE_IDLE;
    MotionGroup current;
    MqttPayloadInfo last_mqtt;

    long long total_groups = 0;

    StatItem day_stats[MAX_STATS];
    StatItem hour_stats[MAX_STATS];
    int day_count = 0;
    int hour_count = 0;

    memset(day_stats, 0, sizeof(day_stats));
    memset(hour_stats, 0, sizeof(hour_stats));
    memset(&last_mqtt, 0, sizeof(last_mqtt));

    if (argc < 2) {
        fprintf(stderr, "Usage: %s <log_file>\n", argv[0]);
        return 1;
    }

    fp = fopen(argv[1], "r");
    if (!fp) {
        perror("open log file failed");
        return 1;
    }

    build_output_path(argv[1], "_result.txt", txt_path, sizeof(txt_path));
    build_output_path(argv[1], "_stat.csv", csv_path, sizeof(csv_path));

    txt_out = fopen(txt_path, "w");
    if (!txt_out) {
        perror("open txt output failed");
        fclose(fp);
        return 1;
    }

    csv_out = fopen(csv_path, "w");
    if (!csv_out) {
        perror("open csv output failed");
        fclose(txt_out);
        fclose(fp);
        return 1;
    }

    reset_group(&current);

    while (fgets(line, sizeof(line), fp)) {
        if (!extract_utc_time(line, utc, sizeof(utc))) {
            strcpy(utc, "UNKNOWN UTC");
        }

        /* 新增:缓存最近一条 mqtt payload 信息 */
        if (strstr(line, KW_MQTT_PAYLOAD) && strstr(line, "\"alarmId\":\"")) {
            char alarm_id[MAX_ALARMID] = {0};
            if (extract_alarm_id(line, alarm_id, sizeof(alarm_id))) {
                strncpy(last_mqtt.utc_time, utc, sizeof(last_mqtt.utc_time) - 1);
                strncpy(last_mqtt.alarm_id, alarm_id, sizeof(last_mqtt.alarm_id) - 1);
                last_mqtt.valid = 1;
            }
        }

        if (strstr(line, KW_WAIT_START)) {
            reset_group(&current);
            strncpy(current.wait_start, utc, sizeof(current.wait_start) - 1);
            state = STATE_WAIT_STARTED;
            continue;
        }

        if (strstr(line, KW_WAIT_TIMEOUT)) {
            if (state == STATE_WAIT_STARTED) {
                reset_group(&current);
                state = STATE_IDLE;
            }
            continue;
        }

        if (strstr(line, KW_RECORD_START)) {
            if (state == STATE_WAIT_STARTED) {
                strncpy(current.record_start, utc, sizeof(current.record_start) - 1);
                state = STATE_RECORD_STARTED;
            }
            continue;
        }

        if (strstr(line, KW_EVR_CREATE)) {
            if (state == STATE_RECORD_STARTED) {
                strncpy(current.evr_create, utc, sizeof(current.evr_create) - 1);
                state = STATE_EVR_CREATED;
            }
            continue;
        }

        if (strstr(line, KW_UPLOAD_START)) {
            if (state == STATE_EVR_CREATED) {
                strncpy(current.upload_start, utc, sizeof(current.upload_start) - 1);
                state = STATE_UPLOAD_STARTED;
            }
            continue;
        }

        if ((state == STATE_EVR_CREATED || state == STATE_UPLOAD_STARTED) &&
            strstr(line, "[uploading]") &&
            strstr(line, "put ") &&
            strstr(line, ".nvt4s") &&
            strstr(line, "success")) {

            int x, y;
            if (extract_av_xy(line, &x, &y)) {
                current.seg_index = x;
                current.seg_last_flag = y;
                strncpy(current.upload_finish, utc, sizeof(current.upload_finish) - 1);

                if (y == 1) {
                    char day_key[32], hour_key[32];

                    if (last_mqtt.valid) {
                        strncpy(current.mqtt_payload_time, last_mqtt.utc_time,
                                sizeof(current.mqtt_payload_time) - 1);
                        strncpy(current.alarm_id, last_mqtt.alarm_id,
                                sizeof(current.alarm_id) - 1);
                    }

                    total_groups++;
                    write_group(txt_out, &current, total_groups);

                    if (current.alarm_id[0] &&
                        extract_day_hour_from_alarmid(current.alarm_id,
                                                      day_key, sizeof(day_key),
                                                      hour_key, sizeof(hour_key))) {
                        add_stat(day_stats, &day_count, day_key);
                        add_stat(hour_stats, &hour_count, hour_key);
                    }

                    reset_group(&current);
                    state = STATE_IDLE;
                }
            }
            continue;
        }
    }

    fprintf(txt_out, "Total complete groups: %lld\n", total_groups);

    qsort(day_stats, day_count, sizeof(StatItem), cmp_stats);
    qsort(hour_stats, hour_count, sizeof(StatItem), cmp_stats);

    fprintf(csv_out, "time_key,count\n");
    for (int i = 0; i < day_count; i++) {
        fprintf(csv_out, "%s,%d\n", day_stats[i].key, day_stats[i].count);
        for (int j = 0; j < hour_count; j++) {
            if (strncmp(hour_stats[j].key, day_stats[i].key, 8) == 0) {
                fprintf(csv_out, "%s,%d\n", hour_stats[j].key, hour_stats[j].count);
            }
        }
    }

    fclose(csv_out);
    fclose(txt_out);
    fclose(fp);

    printf("Done.\n");
    printf("TXT : %s\n", txt_path);
    printf("CSV : %s\n", csv_path);
    printf("Total complete groups: %lld\n", total_groups);

    return 0;
}