前言
在微服务架构中,几十个服务的日志分散在不同的服务器上。出问题时,你需要登录每台服务器、用grep慢慢翻日志------太慢了。
ELK(Elasticsearch + Logstash + Kibana)是分布式日志聚合的经典方案。
今天我们从零实现一个完整的日志平台:
· 日志采集(Filebeat风格)
· 日志传输(Kafka模拟)
· 日志解析(Logstash风格)
· 日志存储(Elasticsearch风格)
· 日志搜索(全文检索)
· 日志可视化(Kibana风格)
一、日志平台核心原理
- 架构图
```
┌─────────────────────────────────────────────────────────────┐
│ 应用服务 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 服务A │ │ 服务B │ │ 服务C │ │
│ │ 日志文件 │ │ 日志文件 │ │ 日志文件 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Beats (日志采集) │
│ 监控日志文件,实时发送 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Logstash (日志处理) │
│ 解析/过滤/格式化 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Elasticsearch (日志存储) │
│ 索引/搜索 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Kibana (可视化) │
│ 查询/Dashboard │
└─────────────────────────────────────────────────────────────┘
```
- 核心组件
组件 功能
Beats 轻量级日志采集
Logstash 日志解析和过滤
Elasticsearch 日志存储和搜索
Kibana 日志可视化
二、完整代码实现
- 基础数据结构
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <errno.h>
#include <dirent.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#define MAX_LOG_LINE 8192
#define MAX_INDEX_NAME 64
#define MAX_FIELD_NAME 64
#define MAX_TOKENS 100
#define MAX_LOGS 100000
// 日志条目
typedef struct log_entry {
char timestamp32;
char level16;
char service64;
char messageMAX_LOG_LINE;
char trace_id32;
char span_id16;
char host32;
char ip16;
long long offset;
time_t timestamp_sec;
struct log_entry *next;
} log_entry_t;
// 索引映射
typedef struct index_mapping {
char name64;
char type16;
int indexed;
} index_mapping_t;
// 索引定义
typedef struct index_definition {
char nameMAX_INDEX_NAME;
index_mapping_t mappings20;
int mapping_count;
int shard_count;
int replica_count;
struct index_definition *next;
} index_definition_t;
// 倒排索引(术语→文档ID列表)
typedef struct inverted_entry {
char term64;
long long *doc_ids;
int doc_count;
int capacity;
struct inverted_entry *next;
} inverted_entry_t;
// 日志存储引擎
typedef struct log_storage {
log_entry_t **logs;
int log_count;
int capacity;
inverted_entry_t *inverted_index;
index_definition_t *indices;
pthread_mutex_t mutex;
char data_dir256;
int current_index_id;
} log_storage_t;
```
- 日志采集(Filebeat模拟)
```c
// 采集器配置
typedef struct filebeat_config {
char log_dir256;
char pattern64;
int scan_frequency_ms;
int close_inactive_sec;
} filebeat_config_t;
// 采集器
typedef struct filebeat {
filebeat_config_t config;
FILE *current_file;
char current_path512;
long long file_offset;
pthread_t thread;
int running;
void (*on_event)(log_entry_t *log);
} filebeat_t;
// 创建采集器
filebeat_t *filebeat_create(const char *log_dir, const char *pattern,
void (*on_event)(log_entry_t*)) {
filebeat_t *fb = malloc(sizeof(filebeat_t));
strcpy(fb->config.log_dir, log_dir);
strcpy(fb->config.pattern, pattern);
fb->config.scan_frequency_ms = 1000;
fb->current_file = NULL;
fb->file_offset = 0;
fb->running = 1;
fb->on_event = on_event;
return fb;
}
// 解析日志行
log_entry_t *parse_log_line(const char *line) {
log_entry_t *log = malloc(sizeof(log_entry_t));
memset(log, 0, sizeof(log_entry_t));
char *p = (char*)line;
// 格式1: 2025-01-15T10:30:00Z INFO user-service msg
if (*p == '[') {
p++;
char *end = strchr(p, ']');
if (end) {
int len = end - p;
if (len < 32) {
strncpy(log->timestamp, p, len);
log->timestamplen = '\0';
// 解析时间戳
struct tm tm;
strptime(log->timestamp, "%Y-%m-%dT%H:%M:%S", &tm);
log->timestamp_sec = mktime(&tm);
}
p = end + 2;
}
}
if (*p == '[') {
p++;
char *end = strchr(p, ']');
if (end) {
int len = end - p;
if (len < 16) {
strncpy(log->level, p, len);
log->levellen = '\0';
}
p = end + 2;
}
}
if (*p == '[') {
p++;
char *end = strchr(p, ']');
if (end) {
int len = end - p;
if (len < 64) {
strncpy(log->service, p, len);
log->servicelen = '\0';
}
p = end + 2;
}
}
strcpy(log->message, p);
return log;
}
```
- 日志处理(Logstash模拟)
```c
// 过滤器
typedef struct filter {
char field64;
char pattern128;
char replacement128;
int action; // 0: replace, 1: remove, 2: add
struct filter *next;
} filter_t;
// Logstash管道
typedef struct logstash_pipeline {
filter_t *filters;
char grok_pattern256;
int enable_grok;
pthread_mutex_t mutex;
} logstash_pipeline_t;
// 创建管道
logstash_pipeline_t *pipeline_create(void) {
logstash_pipeline_t *p = malloc(sizeof(logstash_pipeline_t));
p->filters = NULL;
p->enable_grok = 1;
pthread_mutex_init(&p->mutex, NULL);
return p;
}
// 添加过滤器
void pipeline_add_filter(logstash_pipeline_t *p, const char *field,
const char *pattern, const char *replacement) {
filter_t *f = malloc(sizeof(filter_t));
strcpy(f->field, field);
strcpy(f->pattern, pattern);
strcpy(f->replacement, replacement);
f->action = 0;
f->next = p->filters;
p->filters = f;
}
// 处理日志(解析和过滤)
void pipeline_process(logstash_pipeline_t *p, log_entry_t *log) {
pthread_mutex_lock(&p->mutex);
filter_t *f = p->filters;
while (f) {
if (strcmp(f->field, "message") == 0) {
// 简单替换
char *pos = strstr(log->message, f->pattern);
if (pos) {
// 替换模式
char new_msgMAX_LOG_LINE;
int prefix_len = pos - log->message;
strncpy(new_msg, log->message, prefix_len);
new_msgprefix_len = '\0';
strcat(new_msg, f->replacement);
strcat(new_msg, pos + strlen(f->pattern));
strcpy(log->message, new_msg);
}
}
f = f->next;
}
pthread_mutex_unlock(&p->mutex);
}
```
- 日志存储(Elasticsearch模拟)
```c
// 创建存储
log_storage_t *storage_create(const char *data_dir, int capacity) {
log_storage_t *s = malloc(sizeof(log_storage_t));
s->logs = malloc(sizeof(log_entry_t*) * capacity);
s->log_count = 0;
s->capacity = capacity;
s->inverted_index = NULL;
s->indices = NULL;
s->current_index_id = 0;
pthread_mutex_init(&s->mutex, NULL);
strcpy(s->data_dir, data_dir);
mkdir(data_dir, 0755);
printf("Elasticsearch 存储创建,容量: %d\n", capacity);
return s;
}
// 创建索引
void storage_create_index(log_storage_t *s, const char *name,
int shard_count, int replica_count) {
index_definition_t *idx = malloc(sizeof(index_definition_t));
strcpy(idx->name, name);
idx->shard_count = shard_count;
idx->replica_count = replica_count;
idx->mapping_count = 0;
idx->next = s->indices;
s->indices = idx;
}
// 添加映射
void storage_add_mapping(index_definition_t *idx, const char *name,
const char *type, int indexed) {
if (idx->mapping_count >= 20) return;
strcpy(idx->mappingsidx-\>mapping_count.name, name);
strcpy(idx->mappingsidx-\>mapping_count.type, type);
idx->mappingsidx-\>mapping_count.indexed = indexed;
idx->mapping_count++;
}
// 分词
int tokenize_text(const char *text, char tokens\[\]64) {
int count = 0;
char buffer1024;
strcpy(buffer, text);
char *p = buffer;
while (*p && count < MAX_TOKENS) {
while (*p && !isalnum(*p)) p++;
if (!*p) break;
char *start = p;
while (*p && (isalnum(*p) || *p == '_' || *p == '.' || *p == '-')) p++;
int len = p - start;
if (len < 64) {
strncpy(tokenscount, start, len);
tokenscountlen = '\0';
for (int i = 0; i < len; i++) {
tokenscounti = tolower(tokenscounti);
}
count++;
}
}
return count;
}
// 索引日志
void storage_index(log_storage_t *s, log_entry_t *log) {
pthread_mutex_lock(&s->mutex);
if (s->log_count >= s->capacity) {
free(s->logs0);
for (int i = 1; i < s->log_count; i++) {
s->logsi-1 = s->logsi;
}
s->log_count--;
}
s->logss-\>log_count++ = log;
long long doc_id = s->log_count - 1;
// 建立倒排索引(对message分词)
char tokensMAX_TOKENS64;
int token_count = tokenize_text(log->message, tokens);
for (int i = 0; i < token_count; i++) {
inverted_entry_t *entry = s->inverted_index;
int found = 0;
while (entry) {
if (strcmp(entry->term, tokensi) == 0) {
found = 1;
break;
}
entry = entry->next;
}
if (found) {
if (entry->doc_count >= entry->capacity) {
entry->capacity *= 2;
entry->doc_ids = realloc(entry->doc_ids,
sizeof(long long) * entry->capacity);
}
entry->doc_idsentry-\>doc_count++ = doc_id;
} else {
entry = malloc(sizeof(inverted_entry_t));
strcpy(entry->term, tokensi);
entry->doc_ids = malloc(sizeof(long long) * 10);
entry->doc_count = 1;
entry->capacity = 10;
entry->doc_ids0 = doc_id;
entry->next = s->inverted_index;
s->inverted_index = entry;
}
}
pthread_mutex_unlock(&s->mutex);
}
```
- 日志搜索
```c
// 搜索日志
log_entry_t **storage_search(log_storage_t *s, const char *query,
int from, int size, int *total) {
pthread_mutex_lock(&s->mutex);
char tokensMAX_TOKENS64;
int token_count = tokenize_text(query, tokens);
if (token_count == 0) {
*total = 0;
pthread_mutex_unlock(&s->mutex);
return NULL;
}
// 收集匹配的文档ID
long long *matched_docs = NULL;
int matched_count = 0;
for (int i = 0; i < s->log_count; i++) {
int match = 1;
for (int j = 0; j < token_count; j++) {
inverted_entry_t *entry = s->inverted_index;
int found = 0;
while (entry) {
if (strcmp(entry->term, tokensj) == 0) {
for (int k = 0; k < entry->doc_count; k++) {
if (entry->doc_idsk == i) {
found = 1;
break;
}
}
break;
}
entry = entry->next;
}
if (!found) {
match = 0;
break;
}
}
if (match) {
matched_docs = realloc(matched_docs, sizeof(long long) * (matched_count + 1));
matched_docsmatched_count++ = i;
}
}
*total = matched_count;
// 分页
int start = from < matched_count ? from : matched_count;
int end = (from + size) < matched_count ? (from + size) : matched_count;
int result_count = end - start;
log_entry_t **results = malloc(sizeof(log_entry_t*) * (result_count + 1));
for (int i = 0; i < result_count; i++) {
resultsi = s->logsmatched_docs\[start + i];
}
resultsresult_count = NULL;
free(matched_docs);
pthread_mutex_unlock(&s->mutex);
return results;
}
```
- 测试代码
```c
void test_elk() {
printf("=== ELK日志平台测试 ===\n\n");
// 创建存储
log_storage_t *storage = storage_create("./elk_data", 10000);
storage_create_index(storage, "logstash-2025.01.15", 3, 1);
// 创建管道
logstash_pipeline_t *pipeline = pipeline_create();
pipeline_add_filter(pipeline, "message", "password", "REDACTED");
// 生成日志
printf("索引日志...\n");
const char *log_lines\[\] = {
"2025-01-15T10:30:00Z INFO user-service User login successful user_id=12345",
"2025-01-15T10:30:05Z ERROR order-service Database connection failed timeout=30s",
"2025-01-15T10:30:10Z WARN payment-service Payment gateway slow took=2500ms",
"2025-01-15T10:30:15Z INFO user-service User logout user_id=12345",
"2025-01-15T10:30:20Z ERROR user-service Invalid password attempt from 192.168.1.100"
};
for (int i = 0; i < 5; i++) {
log_entry_t *log = parse_log_line(log_linesi);
pipeline_process(pipeline, log);
storage_index(storage, log);
}
// 搜索测试
printf("\n搜索 'ERROR':\n");
int total;
log_entry_t **results = storage_search(storage, "ERROR", 0, 10, &total);
printf(" 找到 %d 条\n", total);
for (int i = 0; resultsi; i++) {
printf(" %s %s\n", resultsi->level, resultsi->message);
}
free(results);
printf("\n搜索 'user-service':\n");
results = storage_search(storage, "user-service", 0, 10, &total);
printf(" 找到 %d 条\n", total);
for (int i = 0; resultsi; i++) {
printf(" %s %s\n", resultsi->level, resultsi->message);
}
free(results);
printf("\n搜索 'Database connection failed':\n");
results = storage_search(storage, "Database connection failed", 0, 10, &total);
printf(" 找到 %d 条\n", total);
for (int i = 0; resultsi; i++) {
printf(" %s %s\n", resultsi->level, resultsi->message);
}
free(results);
printf("\n索引统计: %d 条日志\n", storage->log_count);
free(storage);
free(pipeline);
}
int main() {
test_elk();
return 0;
}
```
三、编译和运行
```bash
gcc -o elk elk.c -lpthread
./elk
```
四、ELK vs 本实现
特性 本实现 ELK
日志采集 ✅ ✅ Filebeat
日志处理 ✅ ✅ Logstash
倒排索引 ✅ ✅ Elasticsearch
全文搜索 ✅ ✅
可视化 ❌ ✅ Kibana
分布式 ❌ ✅
集群支持 ❌ ✅
五、总结
通过这篇文章,你学会了:
· ELK的核心架构(Beats + Logstash + Elasticsearch + Kibana)
· 日志采集(Filebeat模拟)
· 日志处理(Logstash模拟)
· 倒排索引构建
· 全文搜索实现
· 日志管道处理
ELK是日志聚合的经典方案。掌握它,你就理解了分布式日志平台的核心设计。
下一篇预告:《从零实现一个分布式监控:Grafana的核心设计》
评论区分享一下你用ELK解决过什么场景~