ELK 从入门到精通:Spring Boot 实战三部曲(一)------ 基础核心与快速上手
专题导读:本系列共三篇,从基础到高级,带你系统掌握 ELK(Elasticsearch + Logstash + Kibana)在 Spring Boot 项目中的实战应用。
- 第一篇:基础核心与快速上手(本文)
- 第二篇:进阶特性与性能优化
- 第三篇:高级应用与架构设计
📖 前言
在当今的分布式系统中,日志管理和数据分析已成为运维和开发的核心需求。ELK Stack(Elasticsearch + Logstash + Kibana)作为最流行的日志分析平台,以其强大的搜索能力、灵活的数据处理和直观的可视化界面著称。
本文将从 ELK 的基础概念出发,结合 Spring Boot 项目,带你快速上手 ELK 开发,掌握日志收集、存储和可视化的完整流程。
学完本文你将掌握:
- ✅ ELK 的核心组件与应用场景
- ✅ Elasticsearch 基础操作与 DSL 查询
- ✅ Logstash 日志收集与处理
- ✅ Kibana 数据可视化
- ✅ Spring Boot 集成 ELK 的完整配置
- ✅ 实际业务场景中的日志管理
一、ELK 是什么?为什么需要它?
1.1 ELK Stack 简介
ELK Stack 是三个开源项目的首字母缩写:
- Elasticsearch:分布式搜索和分析引擎
- Logstash:服务器端数据处理管道
- Kibana:数据可视化平台
核心特点:
- 🔍 强大搜索:全文检索、模糊匹配、聚合分析
- 📊 实时可视化:丰富的图表类型,实时监控
- 🔄 灵活处理:支持多种数据源和格式
- 🌐 分布式架构:水平扩展,高可用
- 🛠️ 生态丰富:Beats、APM 等组件
1.2 典型应用场景
┌─────────────────────────────────────────────┐
│ ELK 应用场景 │
├──────────────┬──────────────────────────────┤
│ 日志管理 │ 应用日志、系统日志、访问日志 │
│ 实时监控 │ 业务指标、性能监控、告警 │
│ 数据分析 │ 用户行为分析、业务数据统计 │
│ 安全审计 │ 入侵检测、异常行为分析 │
│ APM │ 应用性能监控、链路追踪 │
│ 搜索引擎 │ 商品搜索、文档检索 │
└──────────────┴──────────────────────────────┘
1.3 与传统方案对比
| 特性 | ELK | 传统日志方案 |
|---|---|---|
| 搜索能力 | 全文检索、毫秒级响应 | 关键字 grep,慢 |
| 可视化 | 丰富图表、实时更新 | 无或简单 |
| 扩展性 | 分布式、水平扩展 | 单机、受限 |
| 数据处理 | 灵活管道、多种格式 | 固定格式 |
| 学习成本 | 中等 | 低 |
二、环境搭建与快速开始
2.1 Docker Compose 安装(推荐)
docker-compose.yml:
yaml
version: '3.8'
services:
elasticsearch:
image: elasticsearch:8.11.0
container_name: elasticsearch
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- xpack.security.enabled=false
ports:
- "9200:9200"
- "9300:9300"
volumes:
- es-data:/usr/share/elasticsearch/data
networks:
- elk-network
logstash:
image: logstash:8.11.0
container_name: logstash
ports:
- "5044:5044"
- "9600:9600"
volumes:
- ./logstash/pipeline:/usr/share/logstash/pipeline
- ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml
environment:
- LS_JAVA_OPTS=-Xms256m -Xmx256m
depends_on:
- elasticsearch
networks:
- elk-network
kibana:
image: kibana:8.11.0
container_name: kibana
ports:
- "5601:5601"
environment:
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
depends_on:
- elasticsearch
networks:
- elk-network
volumes:
es-data:
networks:
elk-network:
driver: bridge
启动命令:
bash
# 创建目录
mkdir -p logstash/pipeline logstash/config
# 启动 ELK
docker-compose up -d
# 查看状态
docker-compose ps
# 查看日志
docker-compose logs -f
验证安装:
bash
# 测试 Elasticsearch
curl http://localhost:9200
# 测试 Kibana
# 浏览器访问:http://localhost:5601
2.2 核心概念
┌──────────────────────────────────────────────┐
│ Elasticsearch 核心概念 │
├──────────┬───────────────────────────────────┤
│ Index │ 索引,类似数据库中的表 │
│ Document │ 文档,类似数据库中的行 │
│ Field │ 字段,类似数据库中的列 │
│ Mapping │ 映射,定义字段类型 │
│ Shard │ 分片,数据的物理分区 │
│ Replica │ 副本,分片的备份 │
└──────────┴───────────────────────────────────┘
数据层级:
Cluster(集群)
└─ Node(节点)
└─ Index(索引)
└─ Type(类型,7.x后废弃)
└─ Document(文档)
└─ Field(字段)
三、Elasticsearch 基础操作
3.1 RESTful API 基本操作
索引操作
bash
# 创建索引
PUT /user_index
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"name": { "type": "text" },
"age": { "type": "integer" },
"email": { "type": "keyword" },
"createTime": { "type": "date" }
}
}
}
# 查看索引
GET /user_index
# 删除索引
DELETE /user_index
# 查看所有索引
GET /_cat/indices?v
文档操作
bash
# 添加文档(自动生成ID)
POST /user_index/_doc
{
"name": "张三",
"age": 25,
"email": "zhangsan@example.com",
"createTime": "2024-01-01T10:00:00"
}
# 添加文档(指定ID)
PUT /user_index/_doc/1
{
"name": "李四",
"age": 30,
"email": "lisi@example.com",
"createTime": "2024-01-01T11:00:00"
}
# 查询文档
GET /user_index/_doc/1
# 更新文档
POST /user_index/_update/1
{
"doc": {
"age": 31
}
}
# 删除文档
DELETE /user_index/_doc/1
# 批量操作
POST /_bulk
{ "index": { "_index": "user_index", "_id": "2" } }
{ "name": "王五", "age": 28, "email": "wangwu@example.com" }
{ "index": { "_index": "user_index", "_id": "3" } }
{ "name": "赵六", "age": 35, "email": "zhaoliu@example.com" }
3.2 DSL 查询语言
基础查询
bash
# Match Query(全文检索)
GET /user_index/_search
{
"query": {
"match": {
"name": "张三"
}
}
}
# Term Query(精确匹配)
GET /user_index/_search
{
"query": {
"term": {
"email": {
"value": "zhangsan@example.com"
}
}
}
}
# Range Query(范围查询)
GET /user_index/_search
{
"query": {
"range": {
"age": {
"gte": 20,
"lte": 30
}
}
}
}
# Bool Query(组合查询)
GET /user_index/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "张" } }
],
"filter": [
{ "range": { "age": { "gte": 20, "lte": 30 } } }
]
}
}
}
聚合查询
bash
# 统计各年龄段人数
GET /user_index/_search
{
"size": 0,
"aggs": {
"age_groups": {
"range": {
"field": "age",
"ranges": [
{ "to": 25 },
{ "from": 25, "to": 35 },
{ "from": 35 }
]
}
}
}
}
# 平均值统计
GET /user_index/_search
{
"size": 0,
"aggs": {
"avg_age": {
"avg": {
"field": "age"
}
}
}
}
四、Spring Boot 集成 Elasticsearch
4.1 Maven 依赖
xml
<dependencies>
<!-- Spring Data Elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
4.2 application.yml 配置
yaml
spring:
elasticsearch:
uris: http://localhost:9200
connection-timeout: 5s
socket-timeout: 30s
4.3 实体类定义
java
@Data
@Document(indexName = "user_index")
public class UserDocument {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String name;
@Field(type = FieldType.Integer)
private Integer age;
@Field(type = FieldType.Keyword)
private String email;
@Field(type = FieldType.Date, format = DateFormat.date_time)
private LocalDateTime createTime;
@Field(type = FieldType.Text)
private String address;
}
4.4 Repository 接口
java
@Repository
public interface UserRepository extends ElasticsearchRepository<UserDocument, String> {
/**
* 根据姓名查询
*/
List<UserDocument> findByName(String name);
/**
* 根据年龄范围查询
*/
List<UserDocument> findByAgeBetween(Integer minAge, Integer maxAge);
/**
* 根据邮箱查询
*/
Optional<UserDocument> findByEmail(String email);
/**
* 复合查询
*/
List<UserDocument> findByNameAndAgeBetween(String name, Integer minAge, Integer maxAge);
}
4.5 Service 层实现
java
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
/**
* 保存用户
*/
public UserDocument saveUser(UserDocument user) {
if (user.getCreateTime() == null) {
user.setCreateTime(LocalDateTime.now());
}
UserDocument saved = userRepository.save(user);
log.info("用户保存成功: id={}", saved.getId());
return saved;
}
/**
* 批量保存
*/
public void batchSaveUsers(List<UserDocument> users) {
userRepository.saveAll(users);
log.info("批量保存用户: {} 条", users.size());
}
/**
* 根据ID查询
*/
public Optional<UserDocument> getUserById(String id) {
return userRepository.findById(id);
}
/**
* 根据姓名搜索
*/
public List<UserDocument> searchByName(String name) {
return userRepository.findByName(name);
}
/**
* 复杂查询
*/
public List<UserDocument> searchUsers(String keyword, Integer minAge, Integer maxAge) {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.boolQuery()
.must(QueryBuilders.multiMatchQuery(keyword, "name", "address"))
.filter(QueryBuilders.rangeQuery("age")
.gte(minAge)
.lte(maxAge))
)
.withSort(SortBuilders.fieldSort("age").order(SortOrder.ASC))
.withPageable(PageRequest.of(0, 10))
.build();
SearchHits<UserDocument> hits = elasticsearchTemplate.search(query, UserDocument.class);
return hits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
/**
* 删除用户
*/
public void deleteUser(String id) {
userRepository.deleteById(id);
log.info("用户删除成功: id={}", id);
}
/**
* 聚合统计
*/
public Map<String, Object> getUserStats() {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.addAggregation(AggregationBuilders.avg("avg_age").field("age"))
.addAggregation(AggregationBuilders.terms("age_distribution")
.field("age")
.size(100))
.build();
SearchHits<UserDocument> hits = elasticsearchTemplate.search(query, UserDocument.class);
Map<String, Object> stats = new HashMap<>();
// 平均年龄
Avg avgAge = hits.getAggregations().get("avg_age");
stats.put("avgAge", avgAge.getValue());
// 年龄分布
Terms ageDistribution = hits.getAggregations().get("age_distribution");
Map<String, Long> distribution = new HashMap<>();
for (Terms.Bucket bucket : ageDistribution.getBuckets()) {
distribution.put(bucket.getKeyAsString(), bucket.getDocCount());
}
stats.put("ageDistribution", distribution);
return stats;
}
}
4.6 Controller 层
java
@RestController
@RequestMapping("/api/users")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
/**
* 创建用户
*/
@PostMapping
public ResponseEntity<UserDocument> createUser(@RequestBody UserDocument user) {
UserDocument saved = userService.saveUser(user);
return ResponseEntity.ok(saved);
}
/**
* 批量创建
*/
@PostMapping("/batch")
public ResponseEntity<Void> batchCreate(@RequestBody List<UserDocument> users) {
userService.batchSaveUsers(users);
return ResponseEntity.ok().build();
}
/**
* 查询用户
*/
@GetMapping("/{id}")
public ResponseEntity<UserDocument> getUser(@PathVariable String id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
/**
* 搜索用户
*/
@GetMapping("/search")
public ResponseEntity<List<UserDocument>> search(
@RequestParam String keyword,
@RequestParam(required = false) Integer minAge,
@RequestParam(required = false) Integer maxAge) {
minAge = minAge != null ? minAge : 0;
maxAge = maxAge != null ? maxAge : 150;
List<UserDocument> users = userService.searchUsers(keyword, minAge, maxAge);
return ResponseEntity.ok(users);
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable String id) {
userService.deleteUser(id);
return ResponseEntity.ok().build();
}
/**
* 获取统计信息
*/
@GetMapping("/stats")
public ResponseEntity<Map<String, Object>> getStats() {
return ResponseEntity.ok(userService.getUserStats());
}
}
五、Logstash 日志收集
5.1 Logstash 配置文件
logstash/config/logstash.yml:
yaml
http.host: "0.0.0.0"
xpack.monitoring.enabled: false
logstash/pipeline/logstash.conf:
conf
input {
# TCP 输入
tcp {
port => 5044
codec => json_lines
}
# File 输入(读取日志文件)
file {
path => "/var/log/app/*.log"
start_position => "beginning"
sincedb_path => "/dev/null"
codec => json
}
}
filter {
# 解析日期
date {
match => ["timestamp", "yyyy-MM-dd HH:mm:ss.SSS"]
target => "@timestamp"
}
# 添加字段
mutate {
add_field => { "environment" => "production" }
}
# 删除不需要的字段
mutate {
remove_field => ["host", "port"]
}
}
output {
# 输出到 Elasticsearch
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
}
# 控制台输出(调试用)
stdout {
codec => rubydebug
}
}
5.2 Spring Boot 发送日志到 Logstash
添加依赖:
xml
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
logback-spring.xml 配置:
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Logstash 输出 -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>localhost:5044</destination>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>{"app_name":"my-application","environment":"production"}</customFields>
</encoder>
</appender>
<!-- Root Logger -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="LOGSTASH"/>
</root>
<!-- 特定包的日志级别 -->
<logger name="com.example" level="DEBUG"/>
</configuration>
5.3 测试日志发送
java
@RestController
@Slf4j
public class TestController {
@GetMapping("/test/log")
public String testLog() {
log.info("这是一条INFO日志");
log.warn("这是一条WARN日志");
log.error("这是一条ERROR日志");
// 结构化日志
Map<String, Object> data = new HashMap<>();
data.put("userId", 123);
data.put("action", "login");
data.put("ip", "192.168.1.100");
log.info("用户操作: {}", data);
return "日志已发送";
}
}
六、Kibana 数据可视化
6.1 访问 Kibana
URL: http://localhost:5601
6.2 创建索引模式
- 进入 Stack Management → Index Patterns
- 点击 Create index pattern
- 输入索引模式:
app-logs-* - 选择时间字段:
@timestamp - 点击 Create index pattern
6.3 查看日志
- 进入 Discover 页面
- 选择索引模式:
app-logs-* - 设置时间范围
- 查看和搜索日志
搜索示例:
message: "ERROR"
level: "ERROR"
app_name: "my-application"
6.4 创建可视化图表
柱状图:日志级别分布
- 进入 Visualize Library → Create visualization
- 选择 Vertical Bar
- 选择索引模式
- 配置:
- X轴:
level.keyword - Y轴:Count
- X轴:
折线图:日志量趋势
- 选择 Line
- 配置:
- X轴:
@timestamp(按小时聚合) - Y轴:Count
- X轴:
饼图:错误类型分布
- 选择 Pie
- 配置:
- Split slices:
error_type.keyword - 过滤器:
level: "ERROR"
- Split slices:
6.5 创建 Dashboard
- 进入 Dashboard → Create new dashboard
- 点击 Add from library
- 添加之前创建的可视化图表
- 调整布局和大小
- 保存 Dashboard
七、实战项目:应用日志管理系统
7.1 项目结构
log-management/
├── config/
│ ├── ElasticsearchConfig.java
│ └── LogstashConfig.java
├── document/
│ ├── AppLogDocument.java
│ └── ErrorLogDocument.java
├── repository/
│ ├── AppLogRepository.java
│ └── ErrorLogRepository.java
├── service/
│ ├── LogService.java
│ └── LogAnalysisService.java
└── controller/
└── LogController.java
7.2 日志文档定义
java
@Data
@Document(indexName = "app-logs")
public class AppLogDocument {
@Id
private String id;
@Field(type = FieldType.Keyword)
private String appName;
@Field(type = FieldType.Keyword)
private String level;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String message;
@Field(type = FieldType.Keyword)
private String logger;
@Field(type = FieldType.Keyword)
private String thread;
@Field(type = FieldType.Date, format = DateFormat.date_time)
private LocalDateTime timestamp;
@Field(type = FieldType.Keyword)
private String environment;
@Field(type = FieldType.Object)
private Map<String, Object> additionalData;
}
7.3 日志服务
java
@Service
@Slf4j
public class LogService {
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
/**
* 保存日志
*/
public void saveLog(AppLogDocument logDocument) {
if (logDocument.getTimestamp() == null) {
logDocument.setTimestamp(LocalDateTime.now());
}
IndexQuery indexQuery = new IndexQueryBuilder()
.withId(logDocument.getId())
.withObject(logDocument)
.build();
elasticsearchTemplate.index(indexQuery, IndexCoordinates.of("app-logs"));
}
/**
* 查询日志
*/
public List<AppLogDocument> searchLogs(String keyword, String level,
LocalDateTime startTime, LocalDateTime endTime,
int page, int size) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键字搜索
if (StringUtils.hasText(keyword)) {
boolQuery.must(QueryBuilders.multiMatchQuery(keyword, "message", "logger"));
}
// 日志级别过滤
if (StringUtils.hasText(level)) {
boolQuery.filter(QueryBuilders.termQuery("level", level));
}
// 时间范围过滤
if (startTime != null && endTime != null) {
boolQuery.filter(QueryBuilders.rangeQuery("timestamp")
.gte(startTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
.lte(endTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)));
}
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withSort(SortBuilders.fieldSort("timestamp").order(SortOrder.DESC))
.withPageable(PageRequest.of(page, size))
.build();
SearchHits<AppLogDocument> hits = elasticsearchTemplate.search(
query, AppLogDocument.class, IndexCoordinates.of("app-logs")
);
return hits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
/**
* 统计日志数量
*/
public Map<String, Long> countLogsByLevel(LocalDateTime startTime, LocalDateTime endTime) {
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if (startTime != null && endTime != null) {
boolQuery.filter(QueryBuilders.rangeQuery("timestamp")
.gte(startTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
.lte(endTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)));
}
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.addAggregation(AggregationBuilders.terms("level_count")
.field("level")
.size(10))
.build();
SearchHits<AppLogDocument> hits = elasticsearchTemplate.search(
query, AppLogDocument.class, IndexCoordinates.of("app-logs")
);
Terms terms = hits.getAggregations().get("level_count");
Map<String, Long> result = new HashMap<>();
for (Terms.Bucket bucket : terms.getBuckets()) {
result.put(bucket.getKeyAsString(), bucket.getDocCount());
}
return result;
}
}
八、常见问题与解决方案
8.1 Elasticsearch 连接失败
问题: Connection refused
解决方案:
bash
# 检查 ES 是否启动
docker ps | grep elasticsearch
# 查看 ES 日志
docker logs elasticsearch
# 检查端口
curl http://localhost:9200
8.2 中文分词问题
问题: 中文搜索不准确
解决方案:安装 IK 分词器
bash
# 进入 ES 容器
docker exec -it elasticsearch bash
# 安装 IK 分词器
bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.11.0/elasticsearch-analysis-ik-8.11.0.zip
# 重启 ES
exit
docker restart elasticsearch
使用 IK 分词器:
java
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String content;
8.3 内存不足
问题: OutOfMemoryError
解决方案:
yaml
# docker-compose.yml
environment:
- "ES_JAVA_OPTS=-Xms1g -Xmx1g"
九、总结与展望
9.1 本文要点回顾
✅ ELK 基础概念 :理解三大组件的作用
✅ Elasticsearch 操作 :索引、文档、DSL 查询
✅ Spring Boot 集成 :完整配置与代码实现
✅ Logstash 日志收集 :配置文件与日志发送
✅ Kibana 可视化 :索引模式、图表、Dashboard
✅ 实战案例:应用日志管理系统
9.2 下篇预告
在下一篇文章《ELK 从入门到精通:Spring Boot 实战三部曲(二)------ 进阶特性与性能优化》中,我们将深入探讨:
- 🔍 Elasticsearch 高级查询与聚合
- ⚡ 性能优化技巧与调优实战
- 🔄 Logstash 高级过滤与处理
- 📊 Kibana 高级可视化与告警
- 🛡️ 安全加固与权限控制
- 💾 数据备份与恢复
9.3 学习建议
- 动手实践:亲自搭建 ELK 环境,运行示例代码
- 理解原理:不仅要会用,更要理解底层机制
- 关注性能:生产环境必须考虑性能和资源消耗
- 善用文档:官方文档是最好的学习资料
📚 参考资料
- Elasticsearch 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
- Logstash 官方文档:https://www.elastic.co/guide/en/logstash/current/index.html
- Kibana 官方文档:https://www.elastic.co/guide/en/kibana/current/index.html
- Spring Data Elasticsearch:https://spring.io/projects/spring-data-elasticsearch
觉得有用?欢迎点赞、收藏、转发!
下一篇更精彩,敬请期待! 🚀
系列文章:
- 第一篇 ELK 从入门到精通:Spring Boot 实战三部曲(一)------ 基础核心与快速上手
- 第二篇 ELK 从入门到精通:Spring Boot 实战三部曲(二)------ 进阶特性与性能优化
- 第三篇 ELK 从入门到精通:Spring Boot 实战三部曲(三)------ 高级应用与架构设计