使用Springboot实现简单的ELK日志搜索系统

前提:

要使用 Spring Boot 实现简单的 ELK(Elasticsearch、Logstash、Kibana)系统,需要满足一系列前提条件,涵盖环境准备、技术基础、组件认知等多个方面。以下是详细的前提说明:

一、基础环境准备

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

  1. Java 环境

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

    • 支持 Windows、Linux、macOS 等主流系统,但生产环境推荐 Linux(如 CentOS、Ubuntu),稳定性和性能更优。
    • 注意:Elasticsearch 在 Linux 下需配置用户权限(避免 root 用户直接启动),并调整虚拟内存参数(如vm.max_map_count=262144)。
  3. 网络环境

    • 确保 ELK 各组件(Elasticsearch、Logstash、Kibana)及 Spring Boot 应用在同一网络环境中,端口可正常通信:
      • Elasticsearch 默认端口:9200(HTTP)、9300(节点间通信)
      • Logstash 默认端口:5044(接收 Beats 数据)、9600(监控)
      • Kibana 默认端口:5601
    • 关闭防火墙或开放上述端口(开发环境可简化,生产环境需严格配置)。

二、ELK 组件安装与配置

需单独安装 Elasticsearch、Logstash、Kibana,并完成基础配置(以单机版为例,集群版需额外配置)。

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

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

  3. 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。

  1. 项目基础

    • 需创建一个 Spring Boot 项目(推荐 2.x 或 3.x 版本),具备基础的日志输出功能(如使用logbacklog4j2)。
    • 依赖:无需额外引入 ELK 相关依赖,但需确保日志框架支持 JSON 格式输出(如logstash-logback-encoder)。
  2. 日志配置

    • 目标:将 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>

四、技术知识储备

  1. ELK 组件基础

    • 了解 Elasticsearch 的索引、文档、映射(Mapping)概念,知道如何通过 API 查看索引数据。
    • 理解 Logstash 的管道(Pipeline)结构:Input(输入)、Filter(过滤)、Output(输出),能简单配置过滤规则(如过滤无用日志字段)。
    • 熟悉 Kibana 的基本操作:创建索引模式(Index Pattern)、使用 Discover 查看日志、创建可视化图表(Visualize)和仪表盘(Dashboard)。
  2. Spring Boot 日志框架

    • 了解 Spring Boot 默认日志框架(logback)的配置方式,能自定义日志格式、级别、输出目的地。
    • 理解 JSON 日志的优势(结构化数据便于 Elasticsearch 索引和查询)。
  3. 网络与调试能力

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

五、具体代码实现

  1. 在springboot的配置文件中编写访问地址:

    bash 复制代码
    spring.application.name=elkdemo
    logName= #日志的名称catalina-2025.07.30
    elasticsearchHost= #es的地址
    elasticsearchPort= #es的端口号9200
    elasticsearchDefaultHost= #默认的es地址localhost:9200
  2. 编写ES的config配置类

    java 复制代码
    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());
        }
    }
  3. 编写两个基础的controller接口

    java 复制代码
    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;
        }
    
    }
  4. 编写对应的实现类

    java 复制代码
    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);
            }
        }
    }
  5. 编写Modle实体类

    java 复制代码
    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; }
    }
  6. 在resource目录下编写简单的前端代码index.html

    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="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>
  7. 在resource目录下编写简单的前端代码detail.html

    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="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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
            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即可展示界面,如下:

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

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

七、其他注意事项

  1. 版本兼容性

    • ELK 组件版本需保持一致(如均使用 7.17.x 或 8.x),避免版本不兼容导致通信失败。
    • Spring Boot 版本与日志组件版本兼容(如 logstash-logback-encoder 需与 logback 版本匹配)。
  2. 资源配置

    • Elasticsearch 对内存要求较高,建议开发环境分配至少 2GB 内存(修改config/jvm.options中的-Xms2g -Xmx2g)。
    • Logstash 和 Kibana 可根据需求调整内存配置。
  3. 安全配置(可选)

    • 生产环境需开启 ELK 的安全功能(如 Elasticsearch 的用户名密码认证、SSL 加密),Spring Boot 和 Logstash 需配置对应认证信息。
相关推荐
码神本神23 分钟前
(附源码)基于Spring Boot的4S店信息管理系统 的设计与实现
java·spring boot·后端
天天摸鱼的java工程师27 分钟前
SpringBoot + Seata + MySQL + RabbitMQ:金融系统分布式交易对账与资金清算实战
java·后端·面试
anthem3729 分钟前
第三阶段_大模型应用开发-Day 3: 大模型推理优化与部署
后端
dylan_QAQ36 分钟前
【附录】Spring 资源访问 基础及应用
后端·spring
别来无恙14940 分钟前
Spring Boot文件上传功能实现详解
java·spring boot·文件上传
陈哥聊测试40 分钟前
英伟达被约谈?国产替代迎来新机遇
后端·安全·产品
花妖大人44 分钟前
Python和Js对比
前端·后端
anthem371 小时前
第三阶段_大模型应用开发-Day 2: 模型微调技术
后端
姑苏洛言1 小时前
使用 ECharts 实现菜品统计和销量统计
前端·javascript·后端
anthem371 小时前
第三阶段_大模型应用开发-Day 1: Hugging Face Transformers库
后端