Glive 是一个基于 Go 语言开发的实时日志监控工具,支持通过 Web 界面实时查看多个日志文件,具有 WebSocket 实时推送、日志过滤、历史日志查看、Base64解码、消息合并等功能。

┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Web Browser │◄────►│ WebSocket │ │ Log Files │ │ (Frontend) │ │ Server │ │ │ └─────────────────┘ └────────┬────────┘ └────────┬────────┘ │ │ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ │ Tailer │◄────►│ nxadm/tail │ │ (Core) │ │ (File Monitor) │ └─────────────────┘ └─────────────────┘
| 组件 | 技术 | 说明 |
|---|---|---|
| 后端框架 | Gin | HTTP Web 框架 |
| WebSocket | gorilla/websocket | 实时通信 |
| 日志监控 | nxadm/tail | 跨平台文件 tail |
| 配置管理 | Viper | 配置解析与热重载 |
| 前端 | 原生 HTML/CSS/JS | 无需构建工具 |
# 克隆仓库
git clone https://github.com/yourusername/glive.git
cd glive
# 安装依赖
go mod download
# 编译
go build -o glive.exe main.go
# 使用默认配置(config.yaml)
./glive.exe
# 指定配置文件
./glive.exe -c /path/to/config.yaml
打开浏览器访问:http://localhost:8080
配置文件采用 YAML 格式,默认文件名为 config.yaml。
# 🚀 Glive 配置文件
# 支持 Windows/Linux/macOS
server:
port: 8080 # 监听端口
ssl:
enabled: false # 是否启用HTTPS
cert_file: "" # SSL证书路径
key_file: "" # SSL私钥路径
# 📁 日志监控配置
logs:
# 支持多文件监控,数组形式
# Windows 示例: "C:\\logs\\app.log"
# Linux 示例: "/var/log/nginx/access.log"
files:
- "./test.log" # 测试用,默认监控当前目录的 test.log
- "/var/log/nginx/access.log"
- "C:\\logs\\app.log"
# 或监控整个目录(自动监控目录内匹配的文件)
watch_dir: "/var/log/app"
file_pattern: "*.log" # 文件匹配模式
# 🎨 前端配置
frontend:
static_path: "./static" # HTML静态文件路径
title: "Glive 日志监控" # 页面标题
theme: "dark" # 主题: dark 或 light
# 🔧 高级配置
advanced:
tail_from_end: true # 启动时从文件末尾开始(true)或从头(false)
reopen_on_rotate: true # 日志轮转时自动重新打开
buffer_size: 100 # 消息缓冲区大小
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
server.port | int | 8080 | HTTP 服务监听端口 |
server.ssl.enabled | bool | false | 是否启用 HTTPS |
server.ssl.cert_file | string | "" | SSL 证书文件路径 |
server.ssl.key_file | string | "" | SSL 私钥文件路径 |
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
logs.files | array | [] | 要监控的日志文件列表 |
logs.watch_dir | string | "" | 要监控的目录路径 |
logs.file_pattern | string | "*.log" | 目录监控时的文件匹配模式 |
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
frontend.static_path | string | "./static" | 静态文件目录 |
frontend.title | string | "Glive 日志监控" | 页面标题 |
frontend.theme | string | "dark" | 主题样式: dark 或 light |
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
advanced.tail_from_end | bool | true | 启动时是否从文件末尾开始读取 |
advanced.reopen_on_rotate | bool | true | 日志轮转时是否自动重新打开 |
advanced.buffer_size | int | 100 | WebSocket 消息缓冲区大小 |
修改 config.yaml 后,系统会自动检测变更并重新加载配置(无需重启服务)。控制台会输出:
📝 配置文件已变更: config.yaml ✅ 配置已热重载
| 参数 | 简写 | 默认值 | 说明 |
|---|---|---|---|
--config | -c | "config.yaml" | 指定配置文件路径 |
i 标志表示忽略大小写Ctrl+SpaceCtrl+K5x\xHH 格式、Base64、JSON 等多种编码前端自动识别日志中的编码内容,提供一键解码能力。
/**
* 智能解码函数 - 支持多种编码格式
* 1. \xHH 格式的 UTF-8 转义序列
* 2. Base64 编码
* 3. 多重兜底验证机制
*/
function tryBase64Decode(str) {
if (!str || str.length < 10) return null;
// 策略1: 解码 \xHH 格式的 UTF-8 转义序列
const hexResult = tryDecodeHexEscapes(str);
if (hexResult && isValidDecodedContent(hexResult.decoded)) {
return hexResult;
}
// 策略2: 尝试 Base64 解码
const base64Result = tryDecodeBase64(str);
if (base64Result && isValidDecodedContent(base64Result.decoded)) {
return base64Result;
}
// 策略3: 尝试混合解码 (先解 hex,再解 base64)
if (hexResult) {
const nestedBase64 = tryDecodeBase64(hexResult.decoded);
if (nestedBase64 && isValidDecodedContent(nestedBase64.decoded)) {
return {
encoded: hexResult.encoded,
decoded: nestedBase64.decoded
};
}
}
// 策略4: 尝试 JSON 解析
const jsonResult = tryDecodeJSON(str);
if (jsonResult) return jsonResult;
return null;
}
| 策略 | 函数名 | 处理思路 |
|---|---|---|
| Hex 转义解码 | tryDecodeHexEscapes(str) | 识别 \xHH 格式,提取十六进制字节数组,使用 UTF-8 解码器转换为字符串 |
| Base64 解码 | tryDecodeBase64(str) | 使用正则提取可能的 Base64 片段,验证长度有效性后使用 atob() 解码 |
| 混合解码 | - | 先解 Hex 转义,再尝试 Base64 解码(处理嵌套编码) |
| JSON 解码 | tryDecodeJSON(str) | 检测 JSON 特征(\x22 : \x22),先解 Hex 再解析 JSON |
function tryDecodeHexEscapes(str) {
const hexPattern = /\\x([0-9A-Fa-f]{2})/g;
if (!hexPattern.test(str)) return null;
// 至少要有3个 \xHH 序列才尝试解码
const matches = str.match(/\\x[0-9A-Fa-f]{2}/g);
if (!matches || matches.length < 3) return null;
// 提取所有十六进制字节
const hexBytes = [];
let match;
hexPattern.lastIndex = 0;
while ((match = hexPattern.exec(str)) !== null) {
hexBytes.push(parseInt(match[1], 16));
}
// 使用 UTF-8 解码器解码
const uint8Array = new Uint8Array(hexBytes);
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(uint8Array);
return { encoded: str, decoded: decoded };
}
function tryDecodeBase64(str) {
// 提取可能的 Base64 片段
const base64Pattern = /[A-Za-z0-9+/]{20,}={0,2}/g;
const matches = str.match(base64Pattern);
if (!matches) return null;
for (let match of matches) {
if (match.length < 16) continue;
// 验证 Base64 长度有效性
const padding = (match.match(/=/g) || []).length;
const baseLen = match.length - padding;
if (baseLen % 4 === 1) continue; // 无效长度
try {
const decoded = atob(match);
if (decoded.length > 5 && hasReadableChars(decoded)) {
return { encoded: match, decoded: decoded };
}
} catch (e) {}
}
return null;
}
function isValidDecodedContent(str) {
if (!str || str.length < 2) return false;
// 检查可读字符比例 >= 60%
const readableRatio = getReadableRatio(str);
if (readableRatio < 0.6) return false;
// 检查控制字符比例 < 10%
const controlChars = str.match(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g);
if (controlChars && controlChars.length > str.length * 0.1) return false;
// 验证 Unicode 有效性
try { encodeURIComponent(str); } catch (e) { return false; }
return true;
}
当检测到可解码内容时,日志行右侧会显示 解码 按钮:
自动合并连续出现的相同日志消息,减少界面冗余。
// 重复消息合并配置
const GROUP_TIMEOUT = 5000; // 5秒内相同消息视为重复
let messageGroups = new Map(); // 存储消息指纹和合并信息
function getMessageFingerprint(filename, content) {
// 移除时间戳后生成指纹
const cleanContent = content.replace(/\[\d{2}:\d{2}:\d{2}\]/g, '').trim();
return filename + ':' + cleanContent;
}
function processMessage(data) {
if (isMergeEnabled && !data.includes('✅ 已连接')) {
const fingerprint = getMessageFingerprint(filename, content);
const existingGroup = messageGroups.get(fingerprint);
// 如果在超时时间内,增加计数
if (existingGroup && (Date.now() - existingGroup.time) < GROUP_TIMEOUT) {
existingGroup.count++;
// 更新徽章显示
const badge = existingGroup.element.querySelector('.count-badge');
badge.textContent = existingGroup.count + 'x';
badge.style.display = 'inline-flex';
return; // 不添加新行
}
}
// 创建新行并记录指纹
const element = addLogLine(html, level, content);
messageGroups.set(fingerprint, {
element: element,
count: 1,
time: Date.now()
});
}
5x自动折叠过长的日志行,保持界面整洁。
function addLogLine(html, level, rawContent, autoScroll) {
const isLongContent = rawContent && rawContent.length > 200;
if (isLongContent) {
div.classList.add('collapsed'); // 添加折叠样式
}
// 添加展开/折叠按钮
if (isLongContent) {
actionsHtml += '<button class="line-action-btn" onclick="toggleCollapse(this.parentElement.parentElement)">展开</button>';
}
}
function toggleCollapse(lineElement) {
const isCollapsed = lineElement.classList.contains('collapsed');
if (isCollapsed) {
lineElement.classList.remove('collapsed');
btn.textContent = '折叠';
} else {
lineElement.classList.add('collapsed');
btn.textContent = '展开';
}
}
.line.collapsed .log-content {
max-height: 3.2em; /* 限制高度为3行 */
overflow: hidden; /* 隐藏溢出 */
position: relative;
}
/* 底部渐变遮罩 */
.line.collapsed .log-content::after {
content: '';
position: absolute;
bottom: 0;
left: 0; right: 0;
height: 1.5em;
background: linear-gradient(transparent, #0d1117);
}
根据日志内容自动识别并标记级别。
function detectLevel(content) {
if (/\b(error|err|fail|failed|exception|critical|fatal)\b/i.test(content)) {
return 'error'; // 红色
} else if (/\b(warn|warning)\b/i.test(content)) {
return 'warn'; // 橙色
}
return ''; // 默认颜色
}
| 级别 | 关键词 | 颜色 |
|---|---|---|
| Error | error, err, fail, failed, exception, critical, fatal | 红色 (#f85149) |
| Warn | warn, warning | 橙色 (#f0883e) |
| Info | - | 蓝色 (#58a6ff) |
| System | 系统消息 | 灰色 (#858585) |
| 快捷键 | 功能 |
|---|---|
Ctrl+K | 清空日志 |
Ctrl+Space | 暂停/继续 |
Esc | 关闭弹窗 |
GET /health
响应示例:
{
"status": "ok",
"clients": 5
}
GET /api/status
响应示例:
{
"status": "ok",
"clients": 5,
"monitored_files": ["./test.log", "/var/log/app.log"],
"config": {
"port": 8080,
"tail_from_end": true,
"reopen_on_rotate": true
}
}
GET /api/history?file=<文件路径>&lines=<行数>
参数说明:
file (必填): 日志文件路径lines (可选): 返回的行数,默认 1000响应示例:
{
"file": "./test.log",
"lines": ["2024-01-01 10:00:00 Log line 1", "2024-01-01 10:00:01 Log line 2"],
"count": 2
}
错误响应:
400 Bad Request: 缺少 file 参数403 Forbidden: 文件不在监控列表中500 Internal Server Error: 读取文件失败ws://localhost:8080/ws
服务端推送的消息:
{
"type": "log",
"file": "./test.log",
"content": "2024-01-01 10:00:00 Log message here",
"timestamp": "2024-01-01T10:00:00Z"
}
系统消息:
{
"type": "system",
"content": "📁 开始监控: ./test.log"
}
Glive/ ├── main.go # 程序入口 ├── config.yaml # 默认配置文件 ├── go.mod # Go 模块定义 ├── go.sum # 依赖校验 ├── config/ # 配置管理包 │ └── config.go # 配置解析与热重载 ├── tailer/ # 日志监控核心 │ ├── tailer.go # 跨平台 tailer 实现 │ ├── tailer_unix.go # Unix 系统优化 │ └── tailer_windows.go # Windows 系统优化 ├── static/ # 前端静态文件 │ └── index.html # Web 界面 └── README.md # 项目说明
tailer/tailer.go)负责日志文件的实时监控和广播:
type Tailer struct {
clients map[*websocket.Conn]bool // WebSocket 客户端
broadcast chan string // 广播通道
tails map[string]*tail.Tail // 文件 tail 实例
mu sync.RWMutex // 互斥锁
wg sync.WaitGroup // 等待组
stopCh chan struct{} // 停止信号
cfg *config.Config // 配置
}
主要方法:
New(cfg) - 创建 Tailer 实例Start() - 启动监控Stop() - 停止监控RegisterClient(conn) - 注册 WebSocket 客户端UnregisterClient(conn) - 注销 WebSocket 客户端tailFile(filePath) - 监控单个文件watchDirectory(dir) - 监控整个目录broadcaster() - 广播消息给所有客户端safeBroadcast(msg) - 安全广播(带超时防阻塞)config/config.go)负责配置管理和热重载:
func Init(configPath string) error // 初始化配置
func GetConfig() *Config // 获取当前配置
热重载实现:
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
var newConf Config
if err := v.Unmarshal(&newConf); err != nil {
fmt.Printf("❌ 重载配置失败: %v\n", err)
} else {
Conf = &newConf
fmt.Printf("✅ 配置已热重载\n")
}
})
Gin 框架提供 HTTP 和 WebSocket 服务:
| 路由 | 功能 |
|---|---|
GET / | 首页(Web 界面) |
GET /ws | WebSocket 连接 |
GET /static/* | 静态文件服务 |
GET /api/status | 服务状态 API |
GET /api/history | 历史日志 API |
GET /health | 健康检查 |
WebSocket 升级配置:
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 生产环境建议限制域名
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
static/index.html)纯原生 JavaScript 实现,无需构建工具。
| 功能 | 函数/变量 | 行号 |
|---|---|---|
| WebSocket 连接 | connect() | 460 |
| 消息处理 | processMessage(data) | 545 |
| 智能解码 | tryBase64Decode(str) | 630 |
| Hex 解码 | tryDecodeHexEscapes(str) | 669 |
| Base64 解码 | tryDecodeBase64(str) | 775 |
| JSON 解码 | tryDecodeJSON(str) | 803 |
| 内容验证 | isValidDecodedContent(str) | 830 |
| 消息合并 | getMessageFingerprint() | 540 |
| 行折叠 | toggleCollapse() | 930 |
| 日志级别识别 | detectLevel() | 609 |
| 过滤功能 | applyFilter() | 448 |
| 历史日志 | fetchHistory() | 1003 |
在 main.go 中的路由设置部分添加:
r.GET("/api/new-endpoint", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "New endpoint",
})
})
在 tailer/tailer.go 中修改:
func (t *Tailer) tailFile(filePath string) {
// 添加自定义处理逻辑
for line := range tl.Lines {
processedLine := customProcess(line.Text)
t.safeBroadcast(processedLine)
}
}
项目包含 build.sh 构建脚本:
# Windows 构建
./build.sh windows
# Linux 构建
./build.sh linux
# macOS 构建
./build.sh darwin
解决方案:
chmod 644 /var/log/your-app.log
解决方案: 在配置中启用日志轮转检测:
advanced:
reopen_on_rotate: true
解决方案: 使用卷挂载将容器日志映射到主机:
docker run -v /var/lib/docker/containers:/var/log/containers:ro glive
然后在配置中:
logs:
files:
- "/var/log/containers/container-id/container-name-json.log"
排查步骤:
curl http://localhost:8080/healthLinux (systemd):
创建服务文件 /etc/systemd/system/glive.service:
[Unit]
Description=Glive Log Monitor
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/glive -c /etc/glive/config.yaml
Restart=always
User=glive
[Install]
WantedBy=multi-user.target
启用服务:
sudo systemctl enable glive
sudo systemctl start glive
Windows: 使用 NSSM 创建服务:
nssm install Glive "C:\Glive\glive.exe"
nssm set Glive AppDirectory "C:\Glive"
nssm start Glive
server:
port: 8443
ssl:
enabled: true
cert_file: "/path/to/cert.pem"
key_file: "/path/to/key.pem"
可能原因:
本项目采用 MIT 许可证 - 详见 LICENSE 文件
欢迎提交 Issue 和 Pull Request!
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)Made with ❤️ by Glive Team