博客说明:基于 C 语言的大日志文件统计工具实践
--------------------------------------------------
一、项目背景
最近做了两个基于 C 语言的日志分析小工具,主要用于处理体积较大的设备日志文件,例如 100MB、150MB 甚至更大的日志。
这类日志如果一次性全部读入内存,不但占用资源,而且不利于后续扩展。因此整体实现采用了"逐行读取 + 关键字匹配 + 状态机统计"的方式。这样做的好处是:
-
内存占用低
-
适合处理大体积日志
-
逻辑清晰,便于维护
-
后续新增统计维度时更容易扩展
从日志内容可以看到,原始日志中同时包含本地时间和 UTC 时间,例如:
2026-05-08 18:40:41.580 | 2026-05-08 10:40:41.580 UTC | main ... 1
这也为后续按时间维度做统计提供了基础。
二、工具一:Motion 事件链路统计
- 功能目标
第一份代码用于分析 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 计为一次真正的成功事件。
- 为什么使用状态机
这些关键字不是孤立出现的,而是有严格顺序关系的。因此程序使用状态机来控制流程。
一个完整的成功链路通常是:
-
motion wait start
-
motion record start
-
evr task create
-
motion video upload start
-
put ... _av_x_1.nvt4s ... success
只有满足这种顺序,才算一组完整的成功记录。
如果出现如下情况:
motion wait start
之后紧接着出现
motion wait timeout
则说明本次 motion 录制超时失效,这一次不会被纳入统计。
- 增加 mqtt payload 信息提取
后续又增加了一个新的关键日志节点:
mqtt payload... = {... "alarmId":"20260708230350" ...}
这个日志里包含两个非常重要的信息:
-
该行日志本身对应的 UTC 时间
-
alarmId 的值
例如:
alarmId = 20260708230350
因此在最终输出结果中,除了记录 motion 各阶段的时间,还额外记录:
-
mqtt payload 对应的 UTC 时间
-
alarmId
这样后续排查问题时,可以更方便地把上传成功日志与业务事件关联起来。
- 为什么按 alarmId 统计时间
最开始可以按上传完成时间来统计每天和每小时的次数,但这种方式并不总是准确,因为上传完成时间可能滞后于实际事件发生时间。
相比之下,alarmId 更接近 motion 事件的真实触发时间。
例如:
alarmId = 20260708230350
可以直接解析为:
-
日期:20260708
-
小时:2026070823
所以最终 CSV 统计改成按 alarmId 做时间划分,这样得到的每天、每小时分布更准确。
- 输出结果设计
这份工具最终输出两类结果。
第一类是 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 标题块出现次数
- 功能目标
第二份代码的目标比较简单,主要用于统计日志中某种固定格式标题块出现的次数,例如:
| nv_1783779133066_main.log.gz |
| 2026-07-11 14:12:13 UTC |
或者:
| nv_1783779297926_main.log.gz |
| 2026-07-11 14:14:57 UTC |
这类标题块通常用于标识某个子日志文件或某段日志区间。
- 统计条件
统计逻辑如下:
-
第二行必须包含 nv_
-
并且文件名格式满足 _main.log.gz
-
第三行时间需要落在指定范围内
-
例如:20260711 到 20260729
只要满足这些条件,就记为一次有效统计。
- 输出方式
这一份工具只输出 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
四、实现思路总结
这两个工具虽然统计目标不同,但实现思路基本一致。
- 逐行扫描日志
程序采用逐行读取方式处理日志,非常适合大文件。相比整文件读入内存,这种方式更节省资源,也更稳定。
- 基于关键字匹配
通过 strstr、sscanf 等基础字符串处理方法,就可以完成大部分日志解析工作。
- 状态机管理流程
对于像 motion 这种存在明确先后顺序的业务链路,使用状态机可以有效避免误匹配,也更容易扩展新的状态和节点。
- 结果落盘输出
最终结果直接输出到日志所在目录下的 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;
}