基于SpringBoot實(shí)現(xiàn)簡(jiǎn)單的ELK日志搜索系統(tǒng)
一、基礎(chǔ)環(huán)境準(zhǔn)備
實(shí)現(xiàn) ELK 系統(tǒng)的首要前提是搭建好運(yùn)行所需的基礎(chǔ)環(huán)境,確保各組件能正常啟動(dòng)和通信。
Java 環(huán)境
- Elasticsearch、Logstash、Spring Boot 均基于 Java 開(kāi)發(fā),需安裝JDK 8 及以上版本(推薦 JDK 11,兼容性更好)。
- 配置
JAVA_HOME
環(huán)境變量,確保命令行可識(shí)別java
和javac
命令。
操作系統(tǒng)
- 支持 Windows、Linux、macOS 等主流系統(tǒng),但生產(chǎn)環(huán)境推薦 Linux(如 CentOS、Ubuntu),穩(wěn)定性和性能更優(yōu)。
- 注意:Elasticsearch 在 Linux 下需配置用戶權(quán)限(避免 root 用戶直接啟動(dòng)),并調(diào)整虛擬內(nèi)存參數(shù)(如
vm.max_map_count=262144
)。
網(wǎng)絡(luò)環(huán)境
- 確保 ELK 各組件(Elasticsearch、Logstash、Kibana)及 Spring Boot 應(yīng)用在同一網(wǎng)絡(luò)環(huán)境中,端口可正常通信:
- Elasticsearch 默認(rèn)端口:9200(HTTP)、9300(節(jié)點(diǎn)間通信)
- Logstash 默認(rèn)端口:5044(接收 Beats 數(shù)據(jù))、9600(監(jiān)控)
- Kibana 默認(rèn)端口:5601
- 關(guān)閉防火墻或開(kāi)放上述端口(開(kāi)發(fā)環(huán)境可簡(jiǎn)化,生產(chǎn)環(huán)境需嚴(yán)格配置)。
二、ELK 組件安裝與配置
需單獨(dú)安裝 Elasticsearch、Logstash、Kibana,并完成基礎(chǔ)配置(以單機(jī)版為例,集群版需額外配置)。
Elasticsearch
- 作用:存儲(chǔ)和索引日志數(shù)據(jù)。
- 安裝:從官網(wǎng)下載對(duì)應(yīng)版本,解壓后即可運(yùn)行(
bin/elasticsearch
)。
基礎(chǔ)配置(config/elasticsearch.yml
):
yaml
cluster.name: my-elk-cluster # 集群名稱(單機(jī)可自定義) node.name: node-1 # 節(jié)點(diǎn)名稱 network.host: 0.0.0.0 # 允許所有IP訪問(wèn)(開(kāi)發(fā)環(huán)境) http.port: 9200 # HTTP端口
驗(yàn)證:訪問(wèn)http://localhost:9200
,返回節(jié)點(diǎn)信息即啟動(dòng)成功。如下:
Logstash
- 作用:收集、過(guò)濾、轉(zhuǎn)換日志數(shù)據(jù),發(fā)送到 Elasticsearch。
安裝:從官網(wǎng)下載,解壓后配置管道(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 } # 同時(shí)輸出到控制臺(tái)(調(diào)試用) }
啟動(dòng):bin/logstash -f config/logstash-simple.conf
。如下:
Kibana
- 作用:可視化展示 Elasticsearch 中的日志數(shù)據(jù)。
安裝:從官網(wǎng)下載,解壓后配置(config/kibana.yml
):
yaml
server.host: "0.0.0.0" # 允許所有IP訪問(wèn) elasticsearch.hosts: ["http://localhost:9200"] # 連接Elasticsearch
啟動(dòng):bin/kibana
,訪問(wèn)http://localhost:5601
進(jìn)入控制臺(tái)。如下:
三、Spring Boot 應(yīng)用準(zhǔn)備
需開(kāi)發(fā)或改造 Spring Boot 應(yīng)用,使其能生成結(jié)構(gòu)化日志并發(fā)送到 Logstash。
項(xiàng)目基礎(chǔ)
- 需創(chuàng)建一個(gè) Spring Boot 項(xiàng)目(推薦 2.x 或 3.x 版本),具備基礎(chǔ)的日志輸出功能(如使用
logback
或log4j2
)。 - 依賴:無(wú)需額外引入 ELK 相關(guān)依賴,但需確保日志框架支持 JSON 格式輸出(如
logstash-logback-encoder
)。
日志配置
- 目標(biāo):將 Spring Boot 日志以JSON 格式通過(guò) TCP 發(fā)送到 Logstash 的 5000 端口(與 Logstash 輸入配置對(duì)應(yīng))。
以logback
為例,在src/main/resources
下創(chuàng)建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" /> <!-- 同時(shí)輸出到控制臺(tái) --> </root> </configuration>
依賴:在pom.xml
中添加 Logstash 編碼器(若使用 logback):
xml
<dependency> <groupId>net.logstash.logback</groupId> <artifactId>logstash-logback-encoder</artifactId> <version>7.4.0</version> </dependency>
四、技術(shù)知識(shí)儲(chǔ)備
ELK 組件基礎(chǔ)
- 了解 Elasticsearch 的索引、文檔、映射(Mapping)概念,知道如何通過(guò) API 查看索引數(shù)據(jù)。
- 理解 Logstash 的管道(Pipeline)結(jié)構(gòu):Input(輸入)、Filter(過(guò)濾)、Output(輸出),能簡(jiǎn)單配置過(guò)濾規(guī)則(如過(guò)濾無(wú)用日志字段)。
- 熟悉 Kibana 的基本操作:創(chuàng)建索引模式(Index Pattern)、使用 Discover 查看日志、創(chuàng)建可視化圖表(Visualize)和儀表盤(Dashboard)。
Spring Boot 日志框架
- 了解 Spring Boot 默認(rèn)日志框架(logback)的配置方式,能自定義日志格式、級(jí)別、輸出目的地。
- 理解 JSON 日志的優(yōu)勢(shì)(結(jié)構(gòu)化數(shù)據(jù)便于 Elasticsearch 索引和查詢)。
網(wǎng)絡(luò)與調(diào)試能力
- 能使用
telnet
或nc
測(cè)試端口連通性(如檢查 Spring Boot 到 Logstash 的 5000 端口是否可通)。 - 會(huì)查看組件日志排查問(wèn)題:
- Elasticsearch 日志:
logs/elasticsearch.log
- Logstash 日志:
logs/logstash-plain.log
- Kibana 日志:
logs/kibana.log
- Elasticsearch 日志:
五、具體代碼實(shí)現(xiàn)
在springboot的配置文件中編寫訪問(wèn)地址:
spring.application.name=elkdemo logName= #日志的名稱catalina-2025.07.30 elasticsearchHost= #es的地址 elasticsearchPort= #es的端口號(hào)9200 elasticsearchDefaultHost= #默認(rèn)的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() { // 使用相同的連接配置創(chuàng)建ElasticsearchClient ClientConfiguration clientConfiguration = ClientConfiguration.builder() .connectedTo(elasticsearchDefaultHost) .build(); return ElasticsearchClients.createImperative(clientConfiguration); } @Bean public ElasticsearchTemplate elasticsearchTemplate() { return new ElasticsearchTemplate(elasticsearchClient()); } }
編寫兩個(gè)基礎(chǔ)的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就是要搜索的關(guān)鍵字 @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; } }
編寫對(duì)應(yīng)的實(shí)現(xiàn)類
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); //分頁(yè)開(kāi)始位置 sourceBuilder.from((page - 1) * size); //每一頁(yè)的大小 sourceBuilder.size(size); //按照時(shí)間降序排序 sourceBuilder.sort(SortBuilders.fieldSort("@timestamp").order(SortOrder.DESC)); //執(zhí)行搜索 searchRequest.source(sourceBuilder); SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT); List<Document> documents = new ArrayList<>(); //遍歷es中命中的文檔 for (SearchHit hit : searchResponse.getHits()) { //獲取到的源數(shù)據(jù)進(jìn)行類型轉(zhuǎn)換為map對(duì)象 Map<String, Object> source = hit.getSourceAsMap(); Document document = new Document(); document.setId(hit.getId()); //使用 @timestamp 作為標(biāo)題(時(shí)間戳) 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(); //轉(zhuǎn)換 GetResponse<Map> response = elasticsearchClient.get(request, Map.class); if (response.found()) { return response.source(); // 返回完整文檔內(nèi)容 } else { throw new RuntimeException("文檔不存在: " + id + " in index " + indexName); } } catch (IOException e) { throw new RuntimeException("查詢失敗", e); } } }
編寫Modle實(shí)體類
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目錄下編寫簡(jiǎn)單的前端代碼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 日志搜索系統(tǒng)</title> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <link 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 日志搜索系統(tǒng)</h2> <div> <input type="text" id="query" class="search-input" placeholder="請(qǐng)輸入搜索關(guān)鍵詞..."> <button class="search-button" onclick="search()"> <i class="fa fa-search"></i> 搜索 </button> </div> <div style="margin-top: 10px; font-size: 14px; color: #666;"> 支持關(guān)鍵詞搜索,例如: <code>ERROR</code>、<code>command line</code>、<code>2025-07-30</code> </div> </div> <div class="result-list" id="results"> <div class="no-results">請(qǐng)輸入關(guān)鍵詞進(jìn)行搜索</div> </div> <div class="pagination" id="pagination"> <!-- 分頁(yè)按鈕將動(dòng)態(tài)生成 --> </div> </div> <script> // 當(dāng)前頁(yè)碼和每頁(yè)大小 let currentPage = 1; const pageSize = 10; let totalPages = 1; let currentQuery = ''; // 搜索函數(shù) async function search(page = 1) { const queryInput = document.getElementById('query'); currentQuery = queryInput.value.trim(); currentPage = page; if (!currentQuery) { alert('請(qǐng)輸入搜索關(guān)鍵詞'); return; } try { // 顯示加載狀態(tài) 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> 搜索失敗,請(qǐng)重試</div>'; } } // 渲染搜索結(jié)果 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> 沒(méi)有找到匹配的結(jié)果</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 || '無(wú)標(biāo)題'}</div> <div class="result-meta"> <span><i class="fa fa-clock-o"></i> ${doc.timestamp || '未知時(shí)間'}</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) + '...' : '無(wú)內(nèi)容'}</div> </div> `).join(''); resultsDiv.innerHTML = resultItems; } // 渲染分頁(yè)控件 function renderPagination() { const paginationDiv = document.getElementById('pagination'); // 假設(shè)后端返回總頁(yè)數(shù) // 實(shí)際應(yīng)用中應(yīng)從后端獲取總記錄數(shù),計(jì)算總頁(yè)數(shù) totalPages = Math.ceil(50 / pageSize); // 示例:假設(shè)總共有50條記錄 let paginationHtml = ''; // 上一頁(yè)按鈕 if (currentPage > 1) { paginationHtml += `<button class="page-button" onclick="search(${currentPage - 1})">上一頁(yè)</button>`; } // 頁(yè)碼按鈕 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>`; } // 下一頁(yè)按鈕 if (currentPage < totalPages) { paginationHtml += `<button class="page-button" onclick="search(${currentPage + 1})">下一頁(yè)</button>`; } paginationDiv.innerHTML = paginationHtml; } // 打開(kāi)詳情頁(yè) function openDetail(id, indexName) { window.location.href = `detail.html?id=${id}&index=${indexName}`; } </script> </body> </html>
在resource目錄下編寫簡(jiǎn)單的前端代碼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 搜索系統(tǒng)</title> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <link 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> 返回搜索結(jié)果 </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> 正在加載數(shù)據(jù)... </div> <div id="error" class="error" style="display: none;"></div> <div id="json-container" class="json-container" style="display: none;"></div> </div> <script> // 原生JSON高亮格式化函數(shù) 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>'; }); } // 頁(yè)面加載完成后執(zhí)行 document.addEventListener('DOMContentLoaded', function() { // 獲取URL參數(shù) const urlParams = new URLSearchParams(window.location.search); const docId = urlParams.get('id'); const indexName = urlParams.get('index'); // 驗(yàn)證參數(shù) if (!docId || !indexName) { document.getElementById('loading').style.display = 'none'; document.getElementById('error').textContent = '錯(cuò)誤:缺少文檔ID或索引名參數(shù)'; document.getElementById('error').style.display = 'block'; return; } // 更新元信息 document.getElementById('document-id').textContent = `文檔ID: ${docId}`; document.getElementById('index-name').textContent = `索引: ${indexName}`; // 記錄開(kāi)始時(shí)間 const startTime = Date.now(); // 請(qǐng)求數(shù)據(jù) axios.get(`/api/document/${docId}`, { params: { indexName: indexName }, timeout: 15000 }) .then(response => { // 計(jì)算加載時(shí)間 const loadTime = Date.now() - startTime; document.getElementById('load-time').textContent = `加載時(shí)間: ${loadTime}ms`; // 隱藏加載狀態(tài),顯示內(nèi)容 document.getElementById('loading').style.display = 'none'; document.getElementById('json-container').style.display = 'block'; // 格式化并顯示JSON document.getElementById('json-container').innerHTML = syntaxHighlight(response.data); }) .catch(error => { // 處理錯(cuò)誤 document.getElementById('loading').style.display = 'none'; let errorMsg = '加載失敗: '; if (error.response) { errorMsg += `服務(wù)器返回 ${error.response.status} 錯(cuò)誤`; } else if (error.request) { errorMsg += '未收到服務(wù)器響應(yīng),請(qǐng)檢查網(wǎng)絡(luò)'; } else { errorMsg += error.message; } document.getElementById('error').textContent = errorMsg; document.getElementById('error').style.display = 'block'; }); }); // 返回上一頁(yè) function goBack() { window.history.back(); } </script> </body> </html>
六、效果展示
訪問(wèn)localhost:8080即可展示界面,如下:
當(dāng)我們搜索某個(gè)關(guān)鍵字時(shí),是支持全文索引的:
當(dāng)點(diǎn)擊某個(gè)具體的文檔時(shí),可以查看詳情:
七、其他注意事項(xiàng)
版本兼容性
- ELK 組件版本需保持一致(如均使用 7.17.x 或 8.x),避免版本不兼容導(dǎo)致通信失敗。
- Spring Boot 版本與日志組件版本兼容(如 logstash-logback-encoder 需與 logback 版本匹配)。
資源配置
- Elasticsearch 對(duì)內(nèi)存要求較高,建議開(kāi)發(fā)環(huán)境分配至少 2GB 內(nèi)存(修改
config/jvm.options
中的-Xms2g -Xmx2g
)。 - Logstash 和 Kibana 可根據(jù)需求調(diào)整內(nèi)存配置。
安全配置(可選)
- 生產(chǎn)環(huán)境需開(kāi)啟 ELK 的安全功能(如 Elasticsearch 的用戶名密碼認(rèn)證、SSL 加密),Spring Boot 和 Logstash 需配置對(duì)應(yīng)認(rèn)證信息。
以上就是基于SpringBoot實(shí)現(xiàn)簡(jiǎn)單的ELK日志搜索系統(tǒng)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot ELK日志搜索的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot開(kāi)發(fā)項(xiàng)目,引入JPA找不到findOne方法的解決
這篇文章主要介紹了SpringBoot開(kāi)發(fā)項(xiàng)目,引入JPA找不到findOne方法的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Spring學(xué)習(xí)筆記之RestTemplate使用小結(jié)
這篇文章主要給大家介紹了關(guān)于Spring學(xué)習(xí)筆記之RestTemplate使用的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08Java中字節(jié)流和字符流的理解(超精簡(jiǎn)!)
Java通過(guò)稱為流的抽象來(lái)執(zhí)行I/O操作,下面這篇文章主要給大家介紹了關(guān)于Java中字節(jié)流和字符流理解,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07idea tomcat亂碼問(wèn)題的解決及相關(guān)設(shè)置的步驟
這篇文章主要介紹了idea tomcat亂碼問(wèn)題的解決及相關(guān)設(shè)置的步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11java設(shè)計(jì)模式之橋接模式(Bridge)
這篇文章主要為大家詳細(xì)介紹了java設(shè)計(jì)模式之橋接模式Bridge,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01基于SpringBoot和Vue實(shí)現(xiàn)分片上傳系統(tǒng)
最近想做一個(gè)關(guān)于文件上傳的個(gè)人小網(wǎng)盤,一開(kāi)始嘗試使用了OSS的方案,但是該方案對(duì)于大文件來(lái)說(shuō)并不友好,所以開(kāi)始嘗試分片上傳方案的探索,接下來(lái)小編給大家詳細(xì)的介紹一下如何基于SpringBoot和Vue實(shí)現(xiàn)分片上傳系統(tǒng),需要的朋友可以參考下2023-12-12