🔍 Kafka + Elasticsearch 构建搜索型审计日志系统实战(含 Kibana 可视化)
🧱 背景:审计日志不仅要记录,还要可查、可搜、可视化
在实际中后台系统中,日志并非只是写入数据库做"留痕",我们还希望支持:
- ✅ 精确/模糊搜索日志内容(如按action, entity, keyword等过滤)
- ✅ 快速响应:即使在中大规模日志数据中,也可支持按action / entity / keyword等维度的全文查询,结合 Kibana 实现图形化展示。
- ✅ 可视化浏览:图形界面展示最新日志流
因此,我们在已有 Kafka + MongoDB 的基础上,引入 Elasticsearch 作为全文索引引擎,并结合 Kibana 实现可视化界面。
✨ 方案结构一览
- Kafka 仍然作为日志的消息中间件
- MongoDB 用于保存原始日志数据(持久化)
- Elasticsearch 用于构建索引,支持全文检索
- Kibana 作为日志搜索和可视化界面
✅ 新增能力
-
🔍 新增接口:
POST /api/logs/search
支持按
action
、entity
、关键词(payload 全文)过滤日志支持分页:
page
+size
控制结果数量 -
📦 支持 Elasticsearch 客户端写入
-
📈 整合 Kibana:在浏览器中可视化查看日志内容
🔧 搜索接口示例
http
POST /api/logs/search
Content-Type: application/json
请求示例:
json
{
"action": "GENERATE",
"entity": "user",
"keyword": "Product",
"page": 0,
"size": 10
}
🔎 查询字段说明
字段 | 类型 | 说明 |
---|---|---|
action | 字符串 | 日志行为,如 GENERATE 、DELETE |
entity | 字符串 | 实体名称,如 User 、Product |
keyword | 字符串 | payload 中的关键词全文搜索 |
page | 数字 | 页码,从 0 开始 |
size | 数字 | 每页返回数量 |
💻 Elasticsearch 配置简述
我们使用官方 Java 客户端 elasticsearch-java
:
xml
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.12.2</version>
</dependency>
初始化方式:
java
@Configuration
public class ElasticsearchConfig {
@Bean
public ElasticsearchClient elasticsearchClient() {
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200, "http")
).build();
RestClientTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper()
);
return new ElasticsearchClient(transport);
}
}
📺 Elasticsearch 安装和Kibana 可视化
在项目中,我们通过 Docker Compose 启动了 Kibana:docker-compose up -d
yml
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- bootstrap.memory_lock=true
- ES_JAVA_OPTS=-Xms512m -Xmx512m
ports:
- "9200:9200"
volumes:
- es-data:/usr/share/elasticsearch/data
kibana:
image: docker.elastic.co/kibana/kibana:8.13.0
container_name: kibana
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
- elasticsearch
volumes:
es-data:
你可以通过浏览器访问:
在 Kibana 中搜索 audit-logs
索引,即可查看结构化日志内容。
🧪 核心代码与实现解读
1️⃣ Search Controller:REST 接口查询日志
以下是 POST /api/logs/search
的 Controller 实现,支持根据多个条件组合查询 Elasticsearch 中的日志数据:
java
@RestController
@RequestMapping("/api/logs")
@RequiredArgsConstructor
public class AuditLogSearchController {
private final ElasticsearchClient elasticsearchClient;
@PostMapping("/search")
public List<Map<String, Object>> search(@RequestBody AuditLogSearchRequest req) throws IOException {
BoolQuery.Builder boolBuilder = new BoolQuery.Builder();
// 精确匹配 action 字段
if (StringUtils.hasText(req.getAction())) {
boolBuilder.must(mq -> mq.match(m -> m.field("action").query(req.getAction())));
}
// 精确匹配 entity 字段
if (StringUtils.hasText(req.getEntity())) {
boolBuilder.must(mq -> mq.match(m -> m.field("entity").query(req.getEntity())));
}
// 对 payload 做全文关键词匹配
if (StringUtils.hasText(req.getKeyword())) {
boolBuilder.should(mq -> mq.match(m -> m.field("payload").query(req.getKeyword())));
}
Query query = Query.of(q -> q.bool(boolBuilder.build()));
SearchRequest searchRequest = SearchRequest.of(s -> s
.index("audit-logs")
.from(req.getPage() * req.getSize())
.size(req.getSize())
.query(query)
);
SearchResponse<Map<String, Object>> response =
elasticsearchClient.search(searchRequest, Map.class);
return response.hits().hits().stream()
.map(Hit::source)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
📝 说明:
- 使用
BoolQuery
实现多个查询条件组合(must + should) - 支持分页:
from + size
- 返回的结果是 List<Map<String, Object>>,可直接序列化为 JSON 返回前端
- 若只传
keyword
,也能模糊搜索 payload 字段中的关键词
2️⃣ Kafka 消费端:写入 Elasticsearch
每次生成代码都会触发一个 Kafka 异步事件,在消费者中我们将该日志同时写入 MongoDB 和 Elasticsearch:
java
@KafkaListener(topics = "audit-log-topic", groupId = "audit-consumer-group")
public void consume(AuditLogEvent event, Acknowledgment ack) throws IOException {
// MongoDB 落库逻辑
// 写入 Elasticsearch
try {
// ✅ 使用 Spring 注入的 ObjectMapper 序列化事件
String json = objectMapper.writeValueAsString(event);
// ✅ 再反序列化为 Map 结构(ES 文档支持 Map 类型)
Map<String, Object> jsonMap = objectMapper.readValue(json, new TypeReference<>() {});
IndexRequest<Map<String, Object>> request = IndexRequest.of(builder ->
builder.index("audit-logs").document(jsonMap)
);
IndexResponse response = elasticsearchClient.index(request);
log.info("✅ Indexed to Elasticsearch with ID: {}", response.id());
// ✅ 确认提交 Kafka offset,避免重复消费
ack.acknowledge();
} catch (Exception e) {
log.error("❌ Elasticsearch indexing failed", e);
}
}
📝 说明:
- Elasticsearch 客户端使用官方的
elasticsearch-java
- Kafka 消费者使用
manual_immediate
ack 模式,确保写入成功再确认 offset elasticsearchClient.index(...)
是同步阻塞操作,因此考虑优化的话,建议配合线程池异步处理
📝 遇到的坑
❌ ObjectMapper 无法序列化 LocalDateTime
Elasticsearch 客户端默认使用 Jackson 进行 JSON 序列化,如果你项目中手动声明了一个新的 ObjectMapper
,却没有注册 JavaTimeModule
,或在序列化时没有使用 Spring 注入的 ObjectMapper 实例,就会导致如下错误:
bash
InvalidDefinitionException: Java 8 date/time type `LocalDateTime` not supported
✅ 解决方案详见 👉 Spring Boot 中 ObjectMapper 配置踩坑实录:LocalDateTime 无法序列化的终极解决方案
🎯 成果总结
目前我们已完成:
- ✅ Kafka 日志异步发送
- ✅ MongoDB + Elasticsearch 双通道持久化
- ✅ 支持基于action / entity / keyword 的全文搜索
- ✅ 提供 RESTful 搜索接口
- ✅ Kibana 图形化日志浏览界面
📦 项目源码
项目名称:rapid-crud-generator
这是一个从 JSON Schema 自动生成后端 API 与前端管理页面的全栈工具,已集成:
- 异步日志系统(Kafka)
- MongoDB 存储
- Elasticsearch 搜索
- Kibana 可视化
- Prometheus + Grafana 指标监控
- Swagger 接口管理
- 一键 zip 下载
👉 GitHub 地址:github.com/xmyLydia/ra...
欢迎 Star、Fork、留言交流!