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 计为一次真正的成功事件。
2. 为什么使用状态机
这些关键字不是孤立出现的,而是有严格顺序关系的。因此程序使用状态机来控制流程。
一个完整的成功链路通常是:
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 录制超时失效,这一次不会被纳入统计。
3. 增加 mqtt payload 信息提取
后续又增加了一个新的关键日志节点:
mqtt payload[...] = {... "alarmId":"20260708230350" ...}
这个日志里包含两个非常重要的信息:
1. 该行日志本身对应的 UTC 时间
2. alarmId 的值
例如:
alarmId = 20260708230350
因此在最终输出结果中,除了记录 motion 各阶段的时间,还额外记录:
- mqtt payload 对应的 UTC 时间
- alarmId
这样后续排查问题时,可以更方便地把上传成功日志与业务事件关联起来。
4. 为什么按 alarmId 统计时间
最开始可以按上传完成时间来统计每天和每小时的次数,但这种方式并不总是准确,因为上传完成时间可能滞后于实际事件发生时间。
相比之下,alarmId 更接近 motion 事件的真实触发时间。
例如:
alarmId = 20260708230350
可以直接解析为:
- 日期:20260708
- 小时:2026070823
所以最终 CSV 统计改成按 alarmId 做时间划分,这样得到的每天、每小时分布更准确。
5. 输出结果设计
这份工具最终输出两类结果。
第一类是 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 |
----------------------------------
这类标题块通常用于标识某个子日志文件或某段日志区间。
2. 统计条件
统计逻辑如下:
- 第二行必须包含 nv_
- 并且文件名格式满足 _main.log.gz
- 第三行时间需要落在指定范围内
- 例如:20260711 到 20260729
只要满足这些条件,就记为一次有效统计。
3. 输出方式
这一份工具只输出 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. 逐行扫描日志
程序采用逐行读取方式处理日志,非常适合大文件。相比整文件读入内存,这种方式更节省资源,也更稳定。
2. 基于关键字匹配
通过 strstr、sscanf 等基础字符串处理方法,就可以完成大部分日志解析工作。
3. 状态机管理流程
对于像 motion 这种存在明确先后顺序的业务链路,使用状态机可以有效避免误匹配,也更容易扩展新的状态和节点。
4. 结果落盘输出
最终结果直接输出到日志所在目录下的 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(¤t); 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(¤t); 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(¤t); 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, ¤t, 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(¤t); 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; }

浙公网安备 33010602011771号