一、基础环境准备
实现 elk 系统的首要前提是搭建好运行所需的基础环境,确保各组件能正常启动和通信。
java 环境
- elasticsearch、logstash、spring boot 均基于 java 开发,需安装jdk 8 及以上版本(推荐 jdk 11,兼容性更好)。
- 配置
java_home环境变量,确保命令行可识别java和javac命令。
操作系统
- 支持 windows、linux、macos 等主流系统,但生产环境推荐 linux(如 centos、ubuntu),稳定性和性能更优。
- 注意:elasticsearch 在 linux 下需配置用户权限(避免 root 用户直接启动),并调整虚拟内存参数(如
vm.max_map_count=262144)。
网络环境
- 确保 elk 各组件(elasticsearch、logstash、kibana)及 spring boot 应用在同一网络环境中,端口可正常通信:
- elasticsearch 默认端口:9200(http)、9300(节点间通信)
- logstash 默认端口:5044(接收 beats 数据)、9600(监控)
- kibana 默认端口:5601
- 关闭防火墙或开放上述端口(开发环境可简化,生产环境需严格配置)。
二、elk 组件安装与配置
需单独安装 elasticsearch、logstash、kibana,并完成基础配置(以单机版为例,集群版需额外配置)。
elasticsearch
- 作用:存储和索引日志数据。
- 安装:从官网下载对应版本,解压后即可运行(
bin/elasticsearch)。
基础配置(config/elasticsearch.yml):
yaml
cluster.name: my-elk-cluster # 集群名称(单机可自定义) node.name: node-1 # 节点名称 network.host: 0.0.0.0 # 允许所有ip访问(开发环境) http.port: 9200 # http端口
验证:访问http://localhost:9200,返回节点信息即启动成功。如下:

logstash
- 作用:收集、过滤、转换日志数据,发送到 elasticsearch。
安装:从官网下载,解压后配置管道(config/logstash-simple.conf):
conf
input {
tcp {
port => 5000 # 接收spring boot日志的端口
codec => json_lines # 解析json格式日志
}
}
output {
elasticsearch {
hosts => ["localhost:9200"] # elasticsearch地址
index => "springboot-logs-%{+yyyy.mm.dd}" # 日志索引名(按天分割)
}
stdout { codec => rubydebug } # 同时输出到控制台(调试用)
}
启动:bin/logstash -f config/logstash-simple.conf。如下:

kibana
- 作用:可视化展示 elasticsearch 中的日志数据。
安装:从官网下载,解压后配置(config/kibana.yml):
yaml
server.host: "0.0.0.0" # 允许所有ip访问 elasticsearch.hosts: ["http://localhost:9200"] # 连接elasticsearch
启动:bin/kibana,访问http://localhost:5601进入控制台。如下:

三、spring boot 应用准备
需开发或改造 spring boot 应用,使其能生成结构化日志并发送到 logstash。
项目基础
- 需创建一个 spring boot 项目(推荐 2.x 或 3.x 版本),具备基础的日志输出功能(如使用
logback或log4j2)。 - 依赖:无需额外引入 elk 相关依赖,但需确保日志框架支持 json 格式输出(如
logstash-logback-encoder)。
日志配置
- 目标:将 spring boot 日志以json 格式通过 tcp 发送到 logstash 的 5000 端口(与 logstash 输入配置对应)。
以logback为例,在src/main/resources下创建logback-spring.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appender name="logstash" class="net.logstash.logback.appender.logstashtcpsocketappender">
<destination>localhost:5000</destination> <!-- logstash地址和端口 -->
<encoder class="net.logstash.logback.encoder.logstashencoder">
<!-- 自定义字段(可选) -->
<includemdckeyname>requestid</includemdckeyname>
<customfields>{"application":"my-springboot-app"}</customfields>
</encoder>
</appender>
<root level="info">
<appender-ref ref="logstash" />
<appender-ref ref="console" /> <!-- 同时输出到控制台 -->
</root>
</configuration>
依赖:在pom.xml中添加 logstash 编码器(若使用 logback):
xml
<dependency> <groupid>net.logstash.logback</groupid> <artifactid>logstash-logback-encoder</artifactid> <version>7.4.0</version> </dependency>
四、技术知识储备
elk 组件基础
- 了解 elasticsearch 的索引、文档、映射(mapping)概念,知道如何通过 api 查看索引数据。
- 理解 logstash 的管道(pipeline)结构:input(输入)、filter(过滤)、output(输出),能简单配置过滤规则(如过滤无用日志字段)。
- 熟悉 kibana 的基本操作:创建索引模式(index pattern)、使用 discover 查看日志、创建可视化图表(visualize)和仪表盘(dashboard)。
spring boot 日志框架
- 了解 spring boot 默认日志框架(logback)的配置方式,能自定义日志格式、级别、输出目的地。
- 理解 json 日志的优势(结构化数据便于 elasticsearch 索引和查询)。
网络与调试能力
- 能使用
telnet或nc测试端口连通性(如检查 spring boot 到 logstash 的 5000 端口是否可通)。 - 会查看组件日志排查问题:
- elasticsearch 日志:
logs/elasticsearch.log - logstash 日志:
logs/logstash-plain.log - kibana 日志:
logs/kibana.log
- elasticsearch 日志:
五、具体代码实现
在springboot的配置文件中编写访问地址:
spring.application.name=elkdemo logname= #日志的名称catalina-2025.07.30 elasticsearchhost= #es的地址 elasticsearchport= #es的端口号9200 elasticsearchdefaulthost= #默认的es地址localhost:9200
编写es的config配置类
package com.example.demo.config;
import org.apache.http.httphost;
import org.elasticsearch.client.restclient;
import org.elasticsearch.client.resthighlevelclient;
import org.springframework.beans.factory.annotation.value;
import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.data.elasticsearch.client.elc.elasticsearchtemplate;
import co.elastic.clients.elasticsearch.elasticsearchclient;
import org.springframework.data.elasticsearch.client.clientconfiguration;
import org.springframework.data.elasticsearch.client.elc.elasticsearchclients;
@configuration
public class elasticsearchconfig {
@value("${elasticsearchhost}")
private string elasticsearchhost;
@value("${elasticsearchport}")
private integer elasticsearchport;
@value("${elasticsearchdefaulthost}")
private string elasticsearchdefaulthost;
@bean
public resthighlevelclient resthighlevelclient() {
// 配置elasticsearch地址
return new resthighlevelclient(
restclient.builder(
new httphost(elasticsearchhost, elasticsearchport, "http")
)
);
}
@bean
public elasticsearchclient elasticsearchclient() {
// 使用相同的连接配置创建elasticsearchclient
clientconfiguration clientconfiguration = clientconfiguration.builder()
.connectedto(elasticsearchdefaulthost)
.build();
return elasticsearchclients.createimperative(clientconfiguration);
}
@bean
public elasticsearchtemplate elasticsearchtemplate() {
return new elasticsearchtemplate(elasticsearchclient());
}
}
编写两个基础的controller接口
package com.example.demo.controller;
import com.example.demo.model.document;
import com.example.demo.service.searchservice;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.*;
import java.io.ioexception;
import java.util.list;
import java.util.map;
@restcontroller
@requestmapping("/api")
public class searchcontroller {
@autowired
private searchservice searchservice;
// 搜索接口
@getmapping("/search")
public list<document> search(
//query就是要搜索的关键字
@requestparam string query,
@requestparam(defaultvalue = "1") int page,
@requestparam(defaultvalue = "10") int size
) throws ioexception {
return searchservice.searchdocuments(query, page, size);
}
// 详情接口
@getmapping("/document/{id}")
public map<string, object> getdocumentdetail(
@pathvariable string id,
@requestparam string indexname){
map<string, object> documentbyid = searchservice.getdocumentbyid(id, indexname);
return documentbyid;
}
}编写对应的实现类
package com.example.demo.service;
import co.elastic.clients.elasticsearch.elasticsearchclient;
import co.elastic.clients.elasticsearch.core.getresponse;
import com.example.demo.model.document;
import co.elastic.clients.elasticsearch.core.getrequest;
import org.elasticsearch.action.search.searchrequest;
import org.elasticsearch.action.search.searchresponse;
import org.elasticsearch.client.requestoptions;
import org.elasticsearch.client.resthighlevelclient;
import org.elasticsearch.index.query.multimatchquerybuilder;
import org.elasticsearch.index.query.querybuilders;
import org.elasticsearch.search.searchhit;
import org.elasticsearch.search.builder.searchsourcebuilder;
import org.elasticsearch.search.sort.sortbuilders;
import org.elasticsearch.search.sort.sortorder;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.beans.factory.annotation.value;
import org.springframework.stereotype.service;
import java.io.ioexception;
import java.util.arraylist;
import java.util.list;
import java.util.map;
@service
public class searchservice {
@autowired
private resthighlevelclient client;
@value("${logname}")
private string logname;
@autowired
private elasticsearchclient elasticsearchclient;
public list<document> searchdocuments(string query, int page, int size) throws ioexception {
// 使用存在的索引名(在配置文件编写)
searchrequest searchrequest = new searchrequest(logname);
searchsourcebuilder sourcebuilder = new searchsourcebuilder();
// 只搜索映射中存在的字段
multimatchquerybuilder multimatchquery = querybuilders.multimatchquery(
query,
"@version",
"event.original", // 嵌套字段
"host.name",
"log.file.path",
"message",
"tags"
);
sourcebuilder.query(multimatchquery);
//分页开始位置
sourcebuilder.from((page - 1) * size);
//每一页的大小
sourcebuilder.size(size);
//按照时间降序排序
sourcebuilder.sort(sortbuilders.fieldsort("@timestamp").order(sortorder.desc));
//执行搜索
searchrequest.source(sourcebuilder);
searchresponse searchresponse = client.search(searchrequest, requestoptions.default);
list<document> documents = new arraylist<>();
//遍历es中命中的文档
for (searchhit hit : searchresponse.gethits()) {
//获取到的源数据进行类型转换为map对象
map<string, object> source = hit.getsourceasmap();
document document = new document();
document.setid(hit.getid());
//使用 @timestamp 作为标题(时间戳)
document.settitle((string) source.get("@timestamp"));
//处理嵌套字段 event
map<string, object> event = (map<string, object>) source.get("event");
if (event != null) {
document.setcontent((string) event.get("original"));
}
document.settimestamp((string) source.get("@timestamp"));
documents.add(document);
}
return documents;
}
public map<string,object> getdocumentbyid(string id, string indexname) {
try {
getrequest request = new getrequest.builder()
.index(indexname)
.id(id)
.build();
//转换
getresponse<map> response = elasticsearchclient.get(request, map.class);
if (response.found()) {
return response.source(); // 返回完整文档内容
} else {
throw new runtimeexception("文档不存在: " + id + " in index " + indexname);
}
} catch (ioexception e) {
throw new runtimeexception("查询失败", e);
}
}
}编写modle实体类
package com.example.demo.model;
import org.springframework.data.annotation.id;
import org.springframework.data.elasticsearch.annotations.field;
import org.springframework.data.elasticsearch.annotations.fieldtype;
public class document {
@id
private string id;
@field(type = fieldtype.text)
private string title;
@field(type = fieldtype.text)
private string content;
@field(type = fieldtype.date)
private string timestamp;
public string getid() { return id; }
public void setid(string id) { this.id = id; }
public string gettitle() { return title; }
public void settitle(string title) { this.title = title; }
public string getcontent() { return content; }
public void setcontent(string content) { this.content = content; }
public string gettimestamp() { return timestamp; }
public void settimestamp(string timestamp) { this.timestamp = timestamp; }
}在resource目录下编写简单的前端代码index.html
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>elk 日志搜索系统</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="external nofollow" rel="external nofollow" rel="stylesheet">
<style>
* {
box-sizing: border-box;
}
body {
font-family: 'segoe ui', tahoma, geneva, verdana, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.search-box {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.search-input {
width: 80%;
padding: 10px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 10px;
}
.search-button {
padding: 10px 20px;
background-color: #2196f3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.search-button:hover {
background-color: #0b7dda;
}
.result-list {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
}
.result-item {
border-bottom: 1px solid #eee;
padding: 15px 0;
cursor: pointer;
}
.result-item:last-child {
border-bottom: none;
}
.result-item:hover {
background-color: #f9f9f9;
}
.result-title {
font-size: 18px;
color: #2196f3;
margin-bottom: 5px;
}
.result-meta {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.result-content {
font-size: 15px;
color: #333;
line-height: 1.5;
max-height: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: center;
}
.page-button {
padding: 8px 16px;
margin: 0 5px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.page-button.active {
background-color: #2196f3;
color: white;
border-color: #2196f3;
}
.no-results {
text-align: center;
padding: 50px 0;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="search-box">
<h2>elk 日志搜索系统</h2>
<div>
<input type="text" id="query" class="search-input" placeholder="请输入搜索关键词...">
<button class="search-button" onclick="search()">
<i class="fa fa-search"></i> 搜索
</button>
</div>
<div style="margin-top: 10px; font-size: 14px; color: #666;">
支持关键词搜索,例如: <code>error</code>、<code>command line</code>、<code>2025-07-30</code>
</div>
</div>
<div class="result-list" id="results">
<div class="no-results">请输入关键词进行搜索</div>
</div>
<div class="pagination" id="pagination">
<!-- 分页按钮将动态生成 -->
</div>
</div>
<script>
// 当前页码和每页大小
let currentpage = 1;
const pagesize = 10;
let totalpages = 1;
let currentquery = '';
// 搜索函数
async function search(page = 1) {
const queryinput = document.getelementbyid('query');
currentquery = queryinput.value.trim();
currentpage = page;
if (!currentquery) {
alert('请输入搜索关键词');
return;
}
try {
// 显示加载状态
document.getelementbyid('results').innerhtml = '<div class="no-results"><i class="fa fa-spinner fa-spin"></i> 正在搜索...</div>';
const response = await axios.get('/api/search', {
params: {
query: currentquery,
page: currentpage,
size: pagesize
}
});
renderresults(response.data);
renderpagination();
} catch (error) {
console.error('搜索失败:', error);
document.getelementbyid('results').innerhtml = '<div class="no-results"><i class="fa fa-exclamation-triangle"></i> 搜索失败,请重试</div>';
}
}
// 渲染搜索结果
function renderresults(documents) {
const resultsdiv = document.getelementbyid('results');
if (!documents || documents.length === 0) {
resultsdiv.innerhtml = '<div class="no-results"><i class="fa fa-search"></i> 没有找到匹配的结果</div>';
return;
}
const resultitems = documents.map(doc => `
<div class="result-item" onclick="opendetail('${doc.id}', 'catalina-2025.07.30')">
<div class="result-title">${doc.title || '无标题'}</div>
<div class="result-meta">
<span><i class="fa fa-clock-o"></i> ${doc.timestamp || '未知时间'}</span>
<span style="margin-left: 15px;"><i class="fa fa-file-text-o"></i> ${doc.id}</span>
</div>
<div class="result-content">${doc.content ? doc.content.substr(0, 200) + '...' : '无内容'}</div>
</div>
`).join('');
resultsdiv.innerhtml = resultitems;
}
// 渲染分页控件
function renderpagination() {
const paginationdiv = document.getelementbyid('pagination');
// 假设后端返回总页数
// 实际应用中应从后端获取总记录数,计算总页数
totalpages = math.ceil(50 / pagesize); // 示例:假设总共有50条记录
let paginationhtml = '';
// 上一页按钮
if (currentpage > 1) {
paginationhtml += `<button class="page-button" onclick="search(${currentpage - 1})">上一页</button>`;
}
// 页码按钮
const maxvisiblepages = 5;
let startpage = math.max(1, currentpage - math.floor(maxvisiblepages / 2));
let endpage = math.min(startpage + maxvisiblepages - 1, totalpages);
if (endpage - startpage + 1 < maxvisiblepages) {
startpage = math.max(1, endpage - maxvisiblepages + 1);
}
for (let i = startpage; i <= endpage; i++) {
paginationhtml += `<button class="page-button ${i === currentpage ? 'active' : ''}" onclick="search(${i})">${i}</button>`;
}
// 下一页按钮
if (currentpage < totalpages) {
paginationhtml += `<button class="page-button" onclick="search(${currentpage + 1})">下一页</button>`;
}
paginationdiv.innerhtml = paginationhtml;
}
// 打开详情页
function opendetail(id, indexname) {
window.location.href = `detail.html?id=${id}&index=${indexname}`;
}
</script>
</body>
</html>在resource目录下编写简单的前端代码detail.html
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>日志详情 | elk 搜索系统</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="external nofollow" rel="external nofollow" rel="stylesheet">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'consolas', 'monaco', monospace;
background-color: #f5f5f5;
padding: 20px;
line-height: 1.5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
}
.header {
margin-bottom: 20px;
}
.back-button {
padding: 8px 16px;
background-color: #2196f3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 15px;
}
.back-button:hover {
background-color: #0b7dda;
}
.meta-info {
margin-bottom: 20px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
font-size: 14px;
}
.meta-item {
margin-right: 20px;
display: inline-block;
}
.json-container {
background-color: #f9f9f9;
border-radius: 4px;
padding: 20px;
overflow-x: auto;
white-space: pre-wrap;
}
.json-key {
color: #0033a0;
font-weight: bold;
}
.json-string {
color: #008000;
}
.json-number {
color: #800000;
}
.json-boolean {
color: #0000ff;
}
.json-null {
color: #808080;
}
.error {
color: #dc3545;
padding: 20px;
text-align: center;
background-color: #f8d7da;
border-radius: 4px;
}
.loading {
text-align: center;
padding: 50px 0;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<button class="back-button" onclick="goback()">
<i class="fa fa-arrow-left"></i> 返回搜索结果
</button>
<div class="meta-info">
<div class="meta-item">
<i class="fa fa-database"></i> <span id="index-name">加载中...</span>
</div>
<div class="meta-item">
<i class="fa fa-file-text-o"></i> <span id="document-id">加载中...</span>
</div>
<div class="meta-item">
<i class="fa fa-clock-o"></i> <span id="load-time">加载中...</span>
</div>
</div>
</div>
<div id="loading" class="loading">
<i class="fa fa-spinner fa-spin"></i> 正在加载数据...
</div>
<div id="error" class="error" style="display: none;"></div>
<div id="json-container" class="json-container" style="display: none;"></div>
</div>
<script>
// 原生json高亮格式化函数
function syntaxhighlight(json) {
if (typeof json !== 'string') {
json = json.stringify(json, undefined, 2);
}
// 正则匹配不同json元素并添加样式类
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
return json.replace(/("(\\u[a-za-z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[ee][+-]?\d+)?)/g, function (match) {
let cls = 'json-number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'json-key';
} else {
cls = 'json-string';
}
} else if (/true|false/.test(match)) {
cls = 'json-boolean';
} else if (/null/.test(match)) {
cls = 'json-null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
}
// 页面加载完成后执行
document.addeventlistener('domcontentloaded', function() {
// 获取url参数
const urlparams = new urlsearchparams(window.location.search);
const docid = urlparams.get('id');
const indexname = urlparams.get('index');
// 验证参数
if (!docid || !indexname) {
document.getelementbyid('loading').style.display = 'none';
document.getelementbyid('error').textcontent = '错误:缺少文档id或索引名参数';
document.getelementbyid('error').style.display = 'block';
return;
}
// 更新元信息
document.getelementbyid('document-id').textcontent = `文档id: ${docid}`;
document.getelementbyid('index-name').textcontent = `索引: ${indexname}`;
// 记录开始时间
const starttime = date.now();
// 请求数据
axios.get(`/api/document/${docid}`, {
params: {
indexname: indexname
},
timeout: 15000
})
.then(response => {
// 计算加载时间
const loadtime = date.now() - starttime;
document.getelementbyid('load-time').textcontent = `加载时间: ${loadtime}ms`;
// 隐藏加载状态,显示内容
document.getelementbyid('loading').style.display = 'none';
document.getelementbyid('json-container').style.display = 'block';
// 格式化并显示json
document.getelementbyid('json-container').innerhtml = syntaxhighlight(response.data);
})
.catch(error => {
// 处理错误
document.getelementbyid('loading').style.display = 'none';
let errormsg = '加载失败: ';
if (error.response) {
errormsg += `服务器返回 ${error.response.status} 错误`;
} else if (error.request) {
errormsg += '未收到服务器响应,请检查网络';
} else {
errormsg += error.message;
}
document.getelementbyid('error').textcontent = errormsg;
document.getelementbyid('error').style.display = 'block';
});
});
// 返回上一页
function goback() {
window.history.back();
}
</script>
</body>
</html>
六、效果展示
访问localhost:8080即可展示界面,如下:

当我们搜索某个关键字时,是支持全文索引的:

当点击某个具体的文档时,可以查看详情:

七、其他注意事项
版本兼容性
- elk 组件版本需保持一致(如均使用 7.17.x 或 8.x),避免版本不兼容导致通信失败。
- spring boot 版本与日志组件版本兼容(如 logstash-logback-encoder 需与 logback 版本匹配)。
资源配置
- elasticsearch 对内存要求较高,建议开发环境分配至少 2gb 内存(修改
config/jvm.options中的-xms2g -xmx2g)。 - logstash 和 kibana 可根据需求调整内存配置。
安全配置(可选)
- 生产环境需开启 elk 的安全功能(如 elasticsearch 的用户名密码认证、ssl 加密),spring boot 和 logstash 需配置对应认证信息。
以上就是基于springboot实现简单的elk日志搜索系统的详细内容,更多关于springboot elk日志搜索的资料请关注代码网其它相关文章!
发表评论