开发者

基于SpringBoot实现简单的ELK日志搜索系统

目录
  • 一、基础环境准备
  • 二、ELK 组件安装与配置
  • 三、Spring Boot 应用准备
  • 四、技术知识储备
  • 五、具体代码实现
  • 六、效果展示
  • 七、其他注意事项

一、基础环境准备

实现 ELK 系统的首要前提是搭建好运行所需的基础环境,确保各组件能正常启动和通信。

Java 环境

  • Elasticsearch、Logstash、Spring Boot 均基于 Java 开发,需安装JDK 8 及以上版本(推荐 JDK 11,兼容性更好)。
  • 配置JAVA_HOME环境变量,确保命令行可识别javajavac命令。

操作系统

  • 支持 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,返回节点信息即启动成功。如下:

基于SpringBoot实现简单的ELK日志搜索系统

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。如下:

基于SpringBoot实现简单的ELK日志搜索系统

Kibana

  • 作用:可视化展示 Elasticsearch 中的日志数据。

安装:从官网下载,解压后配置(config/kibana.yml):

yaml

server.host: "0.0.0.0"  # 允许所有IP访问
elasticsearch.hosts: ["http://localhost:9200"]  # 连接Elasticsearch

启动:bin/kibana,访问http://localhost:5601进入控制台。如下:

基于SpringBoot实现简单的ELK日志搜索系统

三、Spring Boot 应用准备

需开发或改造 Spring Boot 应用,使其能生成结构化日志并发送到 Logstash。

项目基础

  • 需创建一个 Spring Boot 项目(推荐 2.x 或 3.x 版本),具备基础的日志输出功能(如使用logbacklog4j2)。
  • 依赖:无需额外引入 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=android"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 索引和查询)。

网络与调试能力

  • 能使用telnetnc测试端口连通性(如检查 Spring Boot 到 Logstash 的 5000 端口是否可通)。
  • 会查看组件日志排查问题:
    • Elasticsearch 日志:logs/elasticsearch.log
    • Logstash 日志:logs/logstash-plain.log
    • Kibana 日志:logs/kibana.log

五、具体代码实现

在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">
    &lpythont;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>
            支持关键词搜索,例如: <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><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"></div>

    <div id="json-container" class="json-container"></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')python;

        // 验证参数
        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即可展示界面,如下:

基于SpringBoot实现简单的ELK日志搜索系统

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

基于SpringBoot实现简单的ELK日志搜索系统

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

基于SpringBoot实现简单的ELK日志搜索系统

七、其他注意事项

版本兼容性

  • 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日志搜索的资料请关注编程客栈(www.devze.com)其它相关文章!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新开发

开发排行榜