You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Grails实现服务器tail日志实时流式展示至UI并支持停止功能

嘿,作为Grails新手要实现实时流式展示服务器日志还带停止功能,其实用**Server-Sent Events (SSE)**就挺合适的——它天然适配单向实时推送的场景,再配合后台线程处理SSH的tail命令,刚好能满足你的需求。下面一步步来落地实现:

1. 优化后台SSH日志读取:用服务管理线程与状态

首先,直接在Controller里处理SSH的tail -f会阻塞请求线程,而且Controller是原型模式(每个请求生成新实例),没法跨请求维护isLogStreaming状态。所以我们把SSH操作和状态管理放到Grails服务里(默认单例,方便全局控制):

import com.jcraft.jsch.*

class LogStreamingService {
    // 控制日志流的开关
    boolean isLogStreaming = false
    // 处理SSH日志的线程
    Thread streamingThread

    /**
     * 启动日志流,通过回调传递每行日志
     * @param onLogLine 接收日志行的闭包
     */
    void startStreaming(Closure onLogLine) {
        if (isLogStreaming) return

        isLogStreaming = true
        streamingThread = Thread.start {
            try {
                // 初始化SSH连接
                JSch jsch = new JSch()
                Session session = jsch.getSession("你的SSH用户名", "服务器IP", 22)
                session.setPassword("你的SSH密码")
                // 跳过主机密钥校验(生产环境建议配置信任密钥)
                session.setConfig("StrictHostKeyChecking", "no")
                session.connect()

                // 执行tail命令
                ChannelExec channel = (ChannelExec) session.openChannel("exec")
                channel.setCommand("tail -n 1 -f /服务器上的日志路径/access.log")
                InputStream inStream = channel.getInputStream()
                channel.connect()

                // 读取日志行并通过回调传递
                BufferedReader reader = new BufferedReader(new InputStreamReader(inStream))
                String line
                while (isLogStreaming && (line = reader.readLine()) != null) {
                    onLogLine(line)
                }

                // 资源清理
                reader.close()
                channel.disconnect()
                session.disconnect()
            } catch (Exception e) {
                log.error("日志流处理出错", e)
            } finally {
                isLogStreaming = false
            }
        }
    }

    /**
     * 停止日志流
     */
    void stopStreaming() {
        isLogStreaming = false
        if (streamingThread?.isAlive()) {
            streamingThread.interrupt()
        }
    }
}
2. 用SSE实现Controller的实时推送

Grails支持SSE协议,我们写一个Controller Action来保持长连接,把服务传来的日志行推送给前端:

class LogController {
    def logStreamingService

    /**
     * 日志流推送接口(SSE)
     */
    def streamLogs() {
        // 设置SSE响应头
        response.contentType = "text/event-stream"
        response.characterEncoding = "UTF-8"
        response.setHeader("Cache-Control", "no-cache")
        response.setHeader("Connection", "keep-alive")

        // 注册日志行回调,将日志推送给前端
        logStreamingService.startStreaming { String line ->
            if (!response.writer.checkError()) {
                // SSE格式要求:data: 内容\n\n
                response.writer.write("data: ${line}\n\n")
                response.writer.flush()
            }
        }

        // 保持连接,直到停止或连接断开
        while (logStreamingService.isLogStreaming && !response.writer.checkError()) {
            Thread.sleep(100)
        }

        // 停止后清理资源
        logStreamingService.stopStreaming()
        response.writer.close()
    }

    /**
     * 停止日志流的接口
     */
    def stopLogs() {
        logStreamingService.stopStreaming()
        render "日志流已停止"
    }
}
3. 前端视图:展示日志+控制按钮

前端用EventSource接收SSE流,配合按钮控制启停,还要实现自动滚动到最新日志:

<!DOCTYPE html>
<html>
<head>
    <title>实时日志查看</title>
    <style>
        #logArea {
            width: 100%;
            height: 450px;
            border: 1px solid #ddd;
            padding: 12px;
            overflow-y: auto;
            font-family: "Consolas", monospace;
            line-height: 1.4;
        }
        .btn-group {
            margin: 15px 0;
        }
        button {
            padding: 8px 16px;
            margin-right: 10px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="btn-group">
        <button id="startBtn">开始查看日志</button>
        <button id="stopBtn">停止查看</button>
    </div>
    <div id="logArea"></div>

    <script>
        let eventSource = null;
        const logArea = document.getElementById('logArea');
        const startBtn = document.getElementById('startBtn');
        const stopBtn = document.getElementById('stopBtn');

        // 启动日志流
        startBtn.addEventListener('click', () => {
            if (eventSource) return;

            // 连接SSE接口
            eventSource = new EventSource('${createLink(controller: "log", action: "streamLogs")}');
            
            // 接收日志行并渲染
            eventSource.onmessage = function(e) {
                const logLine = document.createElement('div');
                logLine.textContent = e.data;
                logArea.appendChild(logLine);
                // 自动滚动到底部
                logArea.scrollTop = logArea.scrollHeight;
            };

            // 处理连接错误
            eventSource.onerror = function() {
                eventSource.close();
                eventSource = null;
                alert('日志流已断开,请检查连接');
            };
        });

        // 停止日志流
        stopBtn.addEventListener('click', () => {
            if (eventSource) {
                eventSource.close();
                eventSource = null;
            }
            // 通知后台停止线程
            fetch('${createLink(controller: "log", action: "stopLogs")}')
                .then(res => res.text())
                .then(msg => console.log(msg));
        });
    </script>
</body>
</html>
4. 关键注意事项
  • 线程安全:如果多用户同时使用,默认单例的LogStreamingService会出现状态冲突。可以改成用Session关联每个用户的流状态,或者用Map存储会话ID对应的isLogStreaming标志。
  • 资源泄漏防护:一定要在停止时断开SSH连接、中断线程,避免后台残留无效连接和线程。
  • 异常处理:给SSH连接、线程添加try-catch块,处理网络中断、权限不足等异常,防止后台崩溃。
  • 生产环境优化:SSH密码建议用配置文件存储,不要硬编码;可以添加权限校验,限制日志查看的访问范围。

内容的提问来源于stack exchange,提问作者divz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:52:20