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
相关产品推荐
相关产品推荐

