it编程 > 编程语言 > Java

使用SpringBoot构建一个轻量级的日志查看器

3人参与 2026-08-07 Java

前言

作为一名资深后端开发,你有没有遇到过这样的场景:服务器上应用出问题了,你需要查看日志排查问题,但又不想或者不能直接登录服务器,只能麻烦运维同学帮忙查看?

今天就来聊聊如何用springboot构建一个轻量级的日志查看器,让你在浏览器上就能实时查看服务器日志,省时又省力!

一、为什么需要web日志查看器?

在日常开发和运维过程中,查看日志是排查问题的重要手段。传统的日志查看方式存在以下痛点:

1.1 传统方式的痛点

  1. 登录服务器麻烦:需要记住服务器地址、用户名、密码,还要处理各种权限问题
  2. 命令行操作不友好:对于不熟悉linux命令的开发者来说,使用tail、grep等命令比较困难
  3. 无法共享日志:当多人协作排查问题时,需要反复沟通日志内容
  4. 安全性问题:直接暴露服务器访问权限存在安全风险

1.2 web日志查看器的优势

  1. 操作简单:通过浏览器访问,无需登录服务器
  2. 实时查看:支持类似tail -f的实时日志查看功能
  3. 易于分享:团队成员可以通过url共享日志信息
  4. 安全可控:通过权限控制,避免直接暴露服务器访问权限

二、技术方案选型

实现web日志查看器有多种技术方案,我们来对比一下:

2.1 websocket方案

websocket是一种全双工通信协议,适合需要频繁双向通信的场景。

优点:

缺点:

2.2 server-sent events (sse)方案

sse是html5提供的服务器推送技术,适合服务器向客户端单向推送数据的场景。

优点:

缺点:

2.3 长轮询方案

客户端定时向服务器发送请求获取最新日志。

优点:

缺点:

2.4 我们的选择

考虑到日志查看器主要是服务器向客户端推送日志数据,我们选择sse方案,它既满足了实时性要求,又实现简单。

三、核心实现原理

3.1 sse工作原理

sse通过http长连接实现服务器向客户端推送数据:

  1. 客户端发起请求到特定端点
  2. 服务器保持连接不关闭
  3. 服务器有新数据时主动推送给客户端
  4. 连接断开时自动重连

3.2 日志读取原理

  1. 通过java nio的filechannel监控日志文件变化
  2. 使用randomaccessfile随机访问文件末尾内容
  3. 实现类似tail -f的功能

四、项目结构设计

log-viewer/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/logviewer/
│   │   │       ├── logviewerapplication.java
│   │   │       ├── controller/
│   │   │       │   └── logcontroller.java
│   │   │       ├── service/
│   │   │       │   └── logservice.java
│   │   │       ├── config/
│   │   │       │   └── webconfig.java
│   │   │       └── util/
│   │   │           └── logfiletailer.java
│   │   └── resources/
│   │       ├── application.yml
│   │       ├── static/
│   │       │   ├── index.html
│   │       │   ├── css/
│   │       │   │   └── style.css
│   │       │   └── js/
│   │       │       └── log-viewer.js
│   │       └── templates/
│   └── test/
└── pom.xml

五、核心代码实现

5.1 maven依赖配置

<?xml version="1.0" encoding="utf-8"?>
<project xmlns="http://maven.apache.org/pom/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
         xsi:schemalocation="http://maven.apache.org/pom/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelversion>4.0.0</modelversion>
    <groupid>com.example</groupid>
    <artifactid>log-viewer</artifactid>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>log viewer</name>
    <description>springboot轻量级日志查看器</description>
    <parent>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-parent</artifactid>
        <version>2.7.0</version>
        <relativepath/>
    </parent>
    <properties>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-web</artifactid>
        </dependency>
        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-thymeleaf</artifactid>
        </dependency>
        <dependency>
            <groupid>org.projectlombok</groupid>
            <artifactid>lombok</artifactid>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-test</artifactid>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupid>org.springframework.boot</groupid>
                <artifactid>spring-boot-maven-plugin</artifactid>
            </plugin>
        </plugins>
    </build>
</project>

5.2 应用启动类

package com.example.logviewer;

import org.springframework.boot.springapplication;
import org.springframework.boot.autoconfigure.springbootapplication;

@springbootapplication
public class logviewerapplication {

    public static void main(string[] args) {
        springapplication.run(logviewerapplication.class, args);
    }

}

5.3 日志文件监控工具类

package com.example.logviewer.util;

import lombok.extern.slf4j.slf4j;
import org.springframework.web.servlet.mvc.method.annotation.sseemitter;

import java.io.file;
import java.io.ioexception;
import java.io.randomaccessfile;
import java.util.concurrent.copyonwritearraylist;

@slf4j
public class logfiletailer implements runnable {
    
    private final file logfile;
    private final copyonwritearraylist<sseemitter> emitters;
    private volatile boolean running = true;
    private long lastposition = 0;
    
    public logfiletailer(file logfile, copyonwritearraylist<sseemitter> emitters) {
        this.logfile = logfile;
        this.emitters = emitters;
        // 初始化文件位置
        if (logfile.exists()) {
            this.lastposition = logfile.length();
        }
    }
    
    @override
    public void run() {
        try (randomaccessfile file = new randomaccessfile(logfile, "r")) {
            while (running) {
                long filelength = logfile.length();
                
                // 文件被截断的情况
                if (filelength < lastposition) {
                    lastposition = filelength;
                }
                
                // 文件有新增内容
                if (filelength > lastposition) {
                    file.seek(lastposition);
                    string line;
                    while ((line = file.readline()) != null) {
                        // 处理中文编码问题
                        string logline = new string(line.getbytes("iso-8859-1"), "utf-8");
                        broadcastlog(logline);
                    }
                    lastposition = file.getfilepointer();
                }
                
                // 休眠1秒再检查
                thread.sleep(1000);
            }
        } catch (exception e) {
            log.error("读取日志文件出错", e);
            broadcasterror("读取日志文件出错: " + e.getmessage());
        }
    }
    
    /**
     * 广播日志到所有连接的客户端
     */
    private void broadcastlog(string logline) {
        emitters.removeif(emitter -> {
            try {
                emitter.send(sseemitter.event().name("message").data(logline));
                return false;
            } catch (ioexception e) {
                log.warn("发送日志到客户端失败", e);
                return true;
            }
        });
    }
    
    /**
     * 广播错误信息
     */
    private void broadcasterror(string errormessage) {
        emitters.removeif(emitter -> {
            try {
                emitter.send(sseemitter.event().name("error").data(errormessage));
                return false;
            } catch (ioexception e) {
                log.warn("发送错误信息到客户端失败", e);
                return true;
            }
        });
    }
    
    /**
     * 停止监控
     */
    public void stop() {
        running = false;
    }
}

5.4 日志服务类

package com.example.logviewer.service;

import com.example.logviewer.util.logfiletailer;
import lombok.extern.slf4j.slf4j;
import org.springframework.beans.factory.annotation.value;
import org.springframework.stereotype.service;
import org.springframework.web.servlet.mvc.method.annotation.sseemitter;

import javax.annotation.postconstruct;
import java.io.file;
import java.util.concurrent.copyonwritearraylist;

@slf4j
@service
public class logservice {
    
    @value("${log.viewer.file-path:/var/log/application.log}")
    private string logfilepath;
    
    private final copyonwritearraylist<sseemitter> emitters = new copyonwritearraylist<>();
    private logfiletailer logfiletailer;
    private thread tailerthread;
    
    @postconstruct
    public void init() {
        file logfile = new file(logfilepath);
        if (!logfile.exists()) {
            log.warn("日志文件不存在: {}", logfilepath);
            return;
        }
        
        logfiletailer = new logfiletailer(logfile, emitters);
        tailerthread = new thread(logfiletailer);
        tailerthread.setdaemon(true);
        tailerthread.start();
        
        log.info("日志监控已启动,监控文件: {}", logfilepath);
    }
    
    /**
     * 获取sse连接
     */
    public sseemitter connect() {
        sseemitter emitter = new sseemitter(long.max_value);
        
        // 添加到连接列表
        emitters.add(emitter);
        
        // 连接完成时移除
        emitter.oncompletion(() -> emitters.remove(emitter));
        
        // 连接超时处理
        emitter.ontimeout(() -> {
            emitters.remove(emitter);
            emitter.complete();
        });
        
        // 连接错误处理
        emitter.onerror(throwable -> {
            emitters.remove(emitter);
            emitter.complete();
        });
        
        return emitter;
    }
}

5.5 控制器类

package com.example.logviewer.controller;

import com.example.logviewer.service.logservice;
import lombok.requiredargsconstructor;
import lombok.extern.slf4j.slf4j;
import org.springframework.http.mediatype;
import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.requestmapping;
import org.springframework.web.bind.annotation.restcontroller;
import org.springframework.web.servlet.mvc.method.annotation.sseemitter;

@slf4j
@restcontroller
@requestmapping("/api/logs")
@requiredargsconstructor
public class logcontroller {
    
    private final logservice logservice;
    
    /**
     * sse实时日志流
     */
    @getmapping(value = "/stream", produces = mediatype.text_event_stream_value)
    public sseemitter streamlogs() {
        return logservice.connect();
    }
}

5.6 前端页面

<!doctype html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>轻量级日志查看器</title>
    <link rel="stylesheet" href="/css/style.css" rel="external nofollow" >
</head>
<body>
    <div class="container">
        <header>
            <h1>🚀 轻量级日志查看器</h1>
            <p>实时查看应用日志,无需登录服务器</p>
        </header>
        <div class="controls">
            <button id="connectbtn">连接日志</button>
            <button id="clearbtn">清空日志</button>
            <input type="text" id="searchinput" placeholder="搜索日志内容...">
        </div>
        <div class="log-container">
            <pre id="logoutput"></pre>
        </div>
    </div>
    <script src="/js/log-viewer.js"></script>
</body>
</html>

5.7 前端css样式

/* static/css/style.css */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    font-family: 'courier new', monospace;
    background-color: #1e1e1e;
    color: #d4d4d4;
    line-height: 1.6;
}
.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}
header {
    text-align: center;
    margin-bottom: 30px;
}
header h1 {
    color: #569cd6;
    margin-bottom: 10px;
}
.controls {
    display: flex;
    gap: 15px;
    margin-bottom: 20px;
    flex-wrap: wrap;
}
button {
    padding: 10px 20px;
    background-color: #007acc;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}
button:hover {
    background-color: #005a9e;
}
#searchinput {
    flex: 1;
    min-width: 200px;
    padding: 10px;
    border: 1px solid #3c3c3c;
    border-radius: 4px;
    background-color: #3c3c3c;
    color: #d4d4d4;
}
.log-container {
    background-color: #1e1e1e;
    border: 1px solid #3c3c3c;
    border-radius: 4px;
    height: 70vh;
    overflow-y: auto;
}
#logoutput {
    padding: 20px;
    white-space: pre-wrap;
    word-wrap: break-word;
    font-size: 14px;
}
.log-line {
    margin: 2px 0;
    padding: 2px 0;
}
.log-line.error {
    color: #f48771;
}
.log-line.warn {
    color: #cca700;
}
.log-line.info {
    color: #75beff;
}
.highlight {
    background-color: #ffff00;
    color: #000000;
}

5.8 前端javascript

// static/js/log-viewer.js
class logviewer {
    constructor() {
        this.eventsource = null;
        this.isconnected = false;
        this.logoutput = document.getelementbyid('logoutput');
        this.connectbtn = document.getelementbyid('connectbtn');
        this.clearbtn = document.getelementbyid('clearbtn');
        this.searchinput = document.getelementbyid('searchinput');
        this.initeventlisteners();
    }
    initeventlisteners() {
        this.connectbtn.addeventlistener('click', () => {
            if (this.isconnected) {
                this.disconnect();
            } else {
                this.connect();
            }
        });
        this.clearbtn.addeventlistener('click', () => {
            this.logoutput.innerhtml = '';
        });
        this.searchinput.addeventlistener('input', () => {
            this.highlightsearchterm();
        });
    }
    connect() {
        if (this.eventsource) {
            this.eventsource.close();
        }
        this.eventsource = new eventsource('/api/logs/stream');
        this.isconnected = true;
        this.connectbtn.textcontent = '断开连接';
        this.eventsource.addeventlistener('message', (event) => {
            this.appendlog(event.data);
        });
        this.eventsource.addeventlistener('error', (event) => {
            console.error('sse连接错误:', event);
            this.appendlog('❌ 连接错误,请检查服务器状态');
        });
        this.appendlog('✅ 已连接到日志服务器,开始接收实时日志...');
    }
    disconnect() {
        if (this.eventsource) {
            this.eventsource.close();
            this.eventsource = null;
        }
        this.isconnected = false;
        this.connectbtn.textcontent = '连接日志';
        this.appendlog('🔚 已断开日志连接');
    }
    appendlog(logline) {
        const logelement = document.createelement('div');
        logelement.classname = 'log-line';
        logelement.textcontent = `[${new date().tolocalestring()}] ${logline}`;
        // 根据日志级别添加样式
        if (logline.includes('error') || logline.includes('error')) {
            logelement.classlist.add('error');
        } else if (logline.includes('warn') || logline.includes('warn')) {
            logelement.classlist.add('warn');
        } else if (logline.includes('info') || logline.includes('info')) {
            logelement.classlist.add('info');
        }
        this.logoutput.appendchild(logelement);
        // 自动滚动到底部
        this.logoutput.scrolltop = this.logoutput.scrollheight;
        // 应用搜索高亮
        this.highlightsearchterm();
    }
    highlightsearchterm() {
        const searchterm = this.searchinput.value.trim();
        if (!searchterm) {
            // 清除所有高亮
            const highlightedelements = this.logoutput.queryselectorall('.highlight');
            highlightedelements.foreach(element => {
                element.outerhtml = element.innerhtml;
            });
            return;
        }
        // 重新应用高亮
        const loglines = this.logoutput.queryselectorall('.log-line');
        loglines.foreach(line => {
            const text = line.textcontent;
            if (text.includes(searchterm)) {
                const regex = new regexp(`(${searchterm})`, 'gi');
                const highlightedtext = text.replace(regex, '<span class="highlight">$1</span>');
                line.innerhtml = highlightedtext;
            }
        });
    }
}
// 页面加载完成后初始化
document.addeventlistener('domcontentloaded', () => {
    new logviewer();
});

5.9 配置文件

# application.yml
server:
  port: 8080
spring:
  application:
    name: log-viewer
  thymeleaf:
    cache: false
    encoding: utf-8
    mode: html
    prefix: classpath:/static/
    suffix: .html
log:
  viewer:
    file-path: ./logs/application.log  # 日志文件路径
logging:
  level:
    com.example.logviewer: info
  file:
    name: ./logs/application.log

六、功能特性

6.1 核心功能

  1. 实时日志查看:通过sse实现类似tail -f的实时日志查看功能
  2. 日志级别高亮:自动识别error、warn、info等日志级别并高亮显示
  3. 日志搜索:支持关键字搜索和高亮显示
  4. 日志清空:支持清空当前显示的日志内容
  5. 连接管理:支持手动连接和断开日志流

6.2 安全特性

  1. 权限控制:可通过spring security添加访问权限控制
  2. 日志文件限制:可配置允许查看的日志文件路径
  3. 连接数限制:可限制同时连接的客户端数量

七、部署与使用

7.1 项目构建

# 克隆项目
git clone <项目地址>
# 进入项目目录
cd log-viewer
# 构建项目
mvn clean package
# 运行项目
java -jar target/log-viewer-1.0.0.jar

7.2 配置说明

在application.yml中配置日志文件路径:

log:
  viewer:
    file-path: /var/log/myapp/application.log  # 实际日志文件路径

7.3 访问方式

启动应用后,通过浏览器访问:

http://localhost:8080/index.html

八、性能优化建议

8.1 内存优化

  1. 限制缓存行数:避免日志过多导致内存溢出
  2. 及时清理连接:断开的sse连接要及时清理

8.2 网络优化

  1. 压缩传输:启用gzip压缩减少网络传输量
  2. 连接复用:合理设置sse连接超时时间

8.3 文件读取优化

  1. 缓冲读取:使用合适的缓冲区大小
  2. 文件监控:使用nio的watchservice监控文件变化

九、扩展功能

9.1 多文件支持

可以扩展支持多个日志文件的查看:

// 添加文件选择功能
@getmapping("/files")
public list<string> getlogfiles() {
    // 返回可查看的日志文件列表
    return arrays.aslist("application.log", "error.log", "access.log");
}

9.2 日志过滤

支持按时间、级别等条件过滤日志:

// 添加过滤参数
@getmapping(value = "/stream", produces = mediatype.text_event_stream_value)
public sseemitter streamlogs(
    @requestparam(required = false) string level,
    @requestparam(required = false) string keyword) {
    // 根据参数过滤日志
    return logservice.connect(level, keyword);
}

9.3 历史日志查看

支持查看历史日志内容:

// 添加历史日志查询接口
@getmapping("/history")
public list<string> gethistorylogs(
    @requestparam(defaultvalue = "100") int lines) {
    // 返回最近n行日志
    return logservice.gethistorylogs(lines);
}

十、总结

通过springboot构建的轻量级日志查看器具有以下优势:

  1. 实现简单:基于springboot和sse技术,开发难度低
  2. 实时性强:接近tail -f的实时查看体验
  3. 易于部署:打包成jar包即可运行,无需额外依赖
  4. 功能丰富:支持日志高亮、搜索、清空等实用功能
  5. 扩展性好:可根据需要添加更多功能

掌握了这个技术,相信你再面对日志查看需求时会更加从容不迫,让你的开发效率更上一层楼!

以上就是使用springboot构建一个轻量级的日志查看器的详细内容,更多关于springboot轻量级日志查看器的资料请关注代码网其它相关文章!

(0)

您想发表意见!!点此发布评论

推荐阅读

MyBatis映射值报错的罪魁祸首竟然是Lombok的@Builder详解

08-07

SpringBoot方法级耗时监控实现方案

08-07

基于SpringBoot+AOP+注解实现自动数据变更追踪

08-07

Linux服务器下Java环境部署全攻略

08-07

Mybatis插件机制(拦截器)和缓存机制示例详解

08-07

C#与Java在日期时间处理方面的差异对比详解

08-07

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论