一、基础环境准备
实现 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日志搜索的资料请关注代码网其它相关文章!
发表评论