Kafka + Elasticsearch 构建搜索型审计日志系统实战(含 Kibana 可视化)

🔍 Kafka + Elasticsearch 构建搜索型审计日志系统实战(含 Kibana 可视化)

🧱 背景:审计日志不仅要记录,还要可查、可搜、可视化

在实际中后台系统中,日志并非只是写入数据库做"留痕",我们还希望支持:

  • ✅ 精确/模糊搜索日志内容(如按action, entity, keyword等过滤)
  • ✅ 快速响应:即使在中大规模日志数据中,也可支持按action / entity / keyword等维度的全文查询,结合 Kibana 实现图形化展示。
  • ✅ 可视化浏览:图形界面展示最新日志流

因此,我们在已有 Kafka + MongoDB 的基础上,引入 Elasticsearch 作为全文索引引擎,并结合 Kibana 实现可视化界面。


✨ 方案结构一览

  • Kafka 仍然作为日志的消息中间件
  • MongoDB 用于保存原始日志数据(持久化)
  • Elasticsearch 用于构建索引,支持全文检索
  • Kibana 作为日志搜索和可视化界面
sequenceDiagram User->>SpringBoot: POST /api/generate SpringBoot->>KafkaProducer: Send AuditLogEvent KafkaProducer->>KafkaBroker: Publish to audit-log-topic KafkaConsumer->>KafkaBroker: Poll audit-log-topic KafkaConsumer->>MongoDB: Save to audit_logs KafkaConsumer->>Elasticsearch: Index log document

✅ 新增能力

  • 🔍 新增接口: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:

你可以通过浏览器访问:

👉 http://localhost:5601

在 Kibana 中搜索 audit-logs 索引,即可查看结构化日志内容。

🧪 核心代码与实现解读

以下是 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、留言交流!

相关推荐
liuhl091011 小时前
【无标题】
大数据·elasticsearch·搜索引擎
xbgRS12 小时前
Elasticsearch的查询
elasticsearch
Elasticsearch16 小时前
使用 Lucene 搜索你的 Bean —— Highlighting
elasticsearch
Elasticsearch16 小时前
使用 Lucene 搜索你的 Bean —— Elasticsearch
elasticsearch
Elasticsearch21 小时前
最好的 LLM 只有 59% 的时间能写出正确的 Elasticsearch ES|QL。以下是另外 41% 出错的原因
elasticsearch
垚垚学技术_聚焦云原生21 小时前
Ansible Role 生产环境标准目录结构与规范化实践
java·elasticsearch·ansible
智码看视界21 小时前
技术选型指南:Pulsar vs Kafka:消息队列选型终极对比与压测
kafka·消息队列·pulsar·消息中间件·流处理·高吞吐·架构选型
小林ixn21 小时前
从 MySQL 的 LIKE 到 ES 倒排索引:一次把全文检索和混合检索讲透
sql·elasticsearch·全文检索·agent·关键词
MayBaymax21 小时前
ES 基础总结
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客1 天前
两个依赖和一个配置块:通过 Prometheus 远程写入将 Spring Boot 指标发送到 Elasticsearch
大数据·数据库·spring boot·elasticsearch·搜索引擎·全文检索·prometheus