基于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)和儀表盤(pán)(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的配置文件中編寫(xiě)訪問(wèn)地址:
spring.application.name=elkdemo logName= #日志的名稱catalina-2025.07.30 elasticsearchHost= #es的地址 elasticsearchPort= #es的端口號(hào)9200 elasticsearchDefaultHost= #默認(rèn)的es地址localhost:9200
編寫(xiě)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());
}
}
編寫(xiě)兩個(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;
}
}編寫(xiě)對(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 {
// 使用存在的索引名(在配置文件編寫(xiě))
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);
}
}
}編寫(xiě)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目錄下編寫(xiě)簡(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目錄下編寫(xiě)簡(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-11
Spring學(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-08
Java中字節(jié)流和字符流的理解(超精簡(jiǎn)!)
Java通過(guò)稱為流的抽象來(lái)執(zhí)行I/O操作,下面這篇文章主要給大家介紹了關(guān)于Java中字節(jié)流和字符流理解,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
idea 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-11
java設(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)盤(pán),一開(kāi)始嘗試使用了OSS的方案,但是該方案對(duì)于大文件來(lái)說(shuō)并不友好,所以開(kāi)始嘗試分片上傳方案的探索,接下來(lái)小編給大家詳細(xì)的介紹一下如何基于SpringBoot和Vue實(shí)現(xiàn)分片上傳系統(tǒng),需要的朋友可以參考下2023-12-12

