710 lines
19 KiB
Markdown
710 lines
19 KiB
Markdown
# 摄影作品集网站 - 运维监控方案
|
||
|
||
## 🎯 方案概述
|
||
|
||
这是一个**简单实用的日志管理方案**,专注于日志收集和问题修复。日志查看功能集成到管理后台,提供友好的Web界面。
|
||
|
||
### 设计原则
|
||
- **集成化**: 日志查看功能集成到管理后台
|
||
- **用户友好**: 提供美观易用的Web界面
|
||
- **问题导向**: 专注于快速定位和修复问题
|
||
- **低维护成本**: 几乎零维护的方案
|
||
- **渐进式**: 后续可以根据需要扩展
|
||
|
||
## 📋 核心组件
|
||
|
||
### 日志管理架构
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 后端应用日志 │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||
│ │ 错误日志 │ │ 访问日志 │ │ 业务日志 │ │
|
||
│ │ (JSON格式) │ │ (HTTP日志) │ │ (操作日志) │ │
|
||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ 管理后台日志模块 │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||
│ │ 日志查看器 │ │ 实时监控 │ │ 统计分析 │ │
|
||
│ │ (Web界面) │ │ (自动刷新) │ │ (图表展示) │ │
|
||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ API接口层 │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||
│ │ 日志查询API │ │ 统计API │ │ 搜索API │ │
|
||
│ │(/api/logs) │ │(/api/stats) │ │(/api/search)│ │
|
||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 🔧 技术选择
|
||
|
||
### 日志方案
|
||
- **日志存储**: 本地文件 (JSON格式)
|
||
- **日志轮转**: lumberjack.v2
|
||
- **日志查看**: 管理后台Web界面
|
||
- **Trace ID**: 集成OpenTracing (已在架构文档中添加)
|
||
- **权限控制**: 基于管理后台的用户权限
|
||
|
||
### 集成方式
|
||
- **前端**: 管理后台的日志管理模块
|
||
- **后端**: Gin路由提供日志查询API
|
||
- **认证**: 复用管理后台的登录认证
|
||
- **权限**: 仅管理员可访问日志功能
|
||
|
||
## 📝 日志配置
|
||
|
||
### 1. 应用日志配置
|
||
|
||
#### 日志配置文件 (config.yaml)
|
||
```yaml
|
||
# config/config.yaml
|
||
logger:
|
||
level: "info"
|
||
format: "json"
|
||
output: "file"
|
||
filename: "/app/logs/app.log"
|
||
max_size: 100 # MB
|
||
max_age: 7 # days
|
||
compress: true
|
||
|
||
# 可选:如果需要链路追踪
|
||
tracing:
|
||
enabled: true
|
||
service_name: "photography-backend"
|
||
jaeger:
|
||
endpoint: "http://localhost:14268/api/traces"
|
||
sampling_rate: 1.0
|
||
```
|
||
|
||
### 2. 日志格式标准化
|
||
|
||
#### 统一的日志格式
|
||
```json
|
||
{
|
||
"timestamp": "2024-01-15T10:30:00Z",
|
||
"level": "info",
|
||
"message": "Photo created successfully",
|
||
"service": "photography-backend",
|
||
"trace_id": "abc123def456",
|
||
"request_id": "req-789",
|
||
"user_id": "user-123",
|
||
"operation": "create_photo",
|
||
"photo_id": 1001,
|
||
"duration": 0.5,
|
||
"error": null
|
||
}
|
||
```
|
||
|
||
### 3. 日志分类
|
||
|
||
#### 三种核心日志类型
|
||
```bash
|
||
# 日志目录结构
|
||
logs/
|
||
├── app.log # 应用日志 (所有级别)
|
||
├── error.log # 错误日志 (ERROR级别)
|
||
└── access.log # HTTP访问日志
|
||
```
|
||
|
||
#### 日志级别使用
|
||
```go
|
||
// 日志级别使用指南
|
||
logger.Info("正常业务操作") // 记录重要的业务操作
|
||
logger.Warn("需要关注的情况") // 记录警告信息
|
||
logger.Error("错误情况") // 记录错误信息
|
||
logger.Debug("调试信息") // 开发调试用
|
||
```
|
||
|
||
## 🚀 集成到管理后台
|
||
|
||
### 1. 后端集成步骤
|
||
|
||
#### 在main.go中注册日志路由
|
||
```go
|
||
// cmd/server/main.go
|
||
func main() {
|
||
// ... 其他初始化代码
|
||
|
||
// 创建Gin引擎
|
||
r := gin.Default()
|
||
|
||
// 注册API路由
|
||
apiGroup := r.Group("/api")
|
||
{
|
||
// 其他API路由...
|
||
}
|
||
|
||
// 注册管理后台路由
|
||
adminGroup := r.Group("/admin/api")
|
||
{
|
||
// 注册日志管理路由
|
||
admin.RegisterLogRoutes(adminGroup, "logs/app.log")
|
||
|
||
// 其他管理后台路由...
|
||
}
|
||
|
||
r.Run(":8080")
|
||
}
|
||
```
|
||
|
||
#### 日志处理器实现
|
||
```go
|
||
// internal/api/handlers/admin/logs_handler.go
|
||
package admin
|
||
|
||
import (
|
||
"bufio"
|
||
"encoding/json"
|
||
"net/http"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"photography-backend/pkg/middleware"
|
||
)
|
||
|
||
// LogEntry 日志条目
|
||
type LogEntry struct {
|
||
Timestamp string `json:"timestamp"`
|
||
Level string `json:"level"`
|
||
Message string `json:"message"`
|
||
TraceID string `json:"trace_id,omitempty"`
|
||
UserID string `json:"user_id,omitempty"`
|
||
Operation string `json:"operation,omitempty"`
|
||
}
|
||
|
||
// LogHandler 日志处理器
|
||
type LogHandler struct {
|
||
logFile string
|
||
}
|
||
|
||
// GetLogs 获取日志列表
|
||
func (h *LogHandler) GetLogs(c *gin.Context) {
|
||
// 获取参数
|
||
levelFilter := c.Query("level")
|
||
searchFilter := c.Query("search")
|
||
traceID := c.Query("trace_id")
|
||
lines := 100
|
||
if l := c.Query("lines"); l != "" {
|
||
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 1000 {
|
||
lines = parsed
|
||
}
|
||
}
|
||
|
||
// 读取日志文件
|
||
logs, err := h.readLogs(lines, levelFilter, searchFilter, traceID)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "读取日志失败",
|
||
"details": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"code": 0,
|
||
"message": "success",
|
||
"data": gin.H{
|
||
"logs": logs,
|
||
"total": len(logs),
|
||
},
|
||
})
|
||
}
|
||
|
||
// GetLogStats 获取日志统计
|
||
func (h *LogHandler) GetLogStats(c *gin.Context) {
|
||
stats := map[string]int{
|
||
"total": 0,
|
||
"error": 0,
|
||
"warn": 0,
|
||
"info": 0,
|
||
"debug": 0,
|
||
}
|
||
|
||
file, err := os.Open(h.logFile)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "读取日志文件失败",
|
||
"details": err.Error(),
|
||
})
|
||
return
|
||
}
|
||
defer file.Close()
|
||
|
||
scanner := bufio.NewScanner(file)
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
if line == "" {
|
||
continue
|
||
}
|
||
|
||
stats["total"]++
|
||
|
||
// 解析日志级别
|
||
var entry LogEntry
|
||
if err := json.Unmarshal([]byte(line), &entry); err == nil {
|
||
if count, exists := stats[entry.Level]; exists {
|
||
stats[entry.Level] = count + 1
|
||
}
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"code": 0,
|
||
"message": "success",
|
||
"data": stats,
|
||
})
|
||
}
|
||
|
||
// RegisterLogRoutes 注册日志相关路由
|
||
func RegisterLogRoutes(r *gin.RouterGroup, logFile string) {
|
||
logHandler := NewLogHandler(logFile)
|
||
|
||
// 需要管理员权限的路由组
|
||
adminGroup := r.Group("")
|
||
adminGroup.Use(middleware.RequireAuth()) // 需要登录
|
||
adminGroup.Use(middleware.RequireAdmin()) // 需要管理员权限
|
||
|
||
{
|
||
adminGroup.GET("/logs", logHandler.GetLogs)
|
||
adminGroup.GET("/logs/stats", logHandler.GetLogStats)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 前端集成步骤
|
||
|
||
#### 管理后台日志查看组件
|
||
```javascript
|
||
// admin/src/pages/Logs/LogViewer.jsx
|
||
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
Card,
|
||
Table,
|
||
Select,
|
||
Input,
|
||
Button,
|
||
Tag,
|
||
Space,
|
||
Statistic,
|
||
Row,
|
||
Col,
|
||
message
|
||
} from 'antd';
|
||
|
||
const LogViewer = () => {
|
||
const [logs, setLogs] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [filters, setFilters] = useState({
|
||
level: '',
|
||
search: '',
|
||
trace_id: '',
|
||
lines: 100,
|
||
});
|
||
const [stats, setStats] = useState({});
|
||
const [autoRefresh, setAutoRefresh] = useState(false);
|
||
|
||
// 日志级别配置
|
||
const levelConfig = {
|
||
error: { color: 'red', icon: '❌' },
|
||
warn: { color: 'orange', icon: '⚠️' },
|
||
info: { color: 'blue', icon: 'ℹ️' },
|
||
debug: { color: 'default', icon: '🐛' },
|
||
};
|
||
|
||
// 获取日志数据
|
||
const fetchLogs = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const response = await adminApi.get('/logs', { params: filters });
|
||
setLogs(response.data.data.logs || []);
|
||
} catch (error) {
|
||
message.error('获取日志失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 获取统计数据
|
||
const fetchStats = async () => {
|
||
try {
|
||
const response = await adminApi.get('/logs/stats');
|
||
setStats(response.data.data || {});
|
||
} catch (error) {
|
||
console.error('获取统计数据失败', error);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
fetchLogs();
|
||
fetchStats();
|
||
}, [filters]);
|
||
|
||
// 自动刷新
|
||
useEffect(() => {
|
||
let interval;
|
||
if (autoRefresh) {
|
||
interval = setInterval(() => {
|
||
fetchLogs();
|
||
fetchStats();
|
||
}, 5000);
|
||
}
|
||
return () => interval && clearInterval(interval);
|
||
}, [autoRefresh, filters]);
|
||
|
||
// 表格列配置
|
||
const columns = [
|
||
{
|
||
title: '时间',
|
||
dataIndex: 'timestamp',
|
||
key: 'timestamp',
|
||
width: 180,
|
||
render: (timestamp) => new Date(timestamp).toLocaleString(),
|
||
},
|
||
{
|
||
title: '级别',
|
||
dataIndex: 'level',
|
||
key: 'level',
|
||
width: 80,
|
||
render: (level) => {
|
||
const config = levelConfig[level] || levelConfig.info;
|
||
return (
|
||
<Tag color={config.color}>
|
||
{config.icon} {level.toUpperCase()}
|
||
</Tag>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: 'Trace ID',
|
||
dataIndex: 'trace_id',
|
||
key: 'trace_id',
|
||
width: 120,
|
||
render: (traceId) => traceId ? (
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
onClick={() => setFilters(prev => ({ ...prev, trace_id: traceId }))}
|
||
>
|
||
{traceId}
|
||
</Button>
|
||
) : '-',
|
||
},
|
||
{
|
||
title: '消息',
|
||
dataIndex: 'message',
|
||
key: 'message',
|
||
ellipsis: true,
|
||
render: (message, record) => {
|
||
// 高亮搜索关键词
|
||
if (filters.search && message.toLowerCase().includes(filters.search.toLowerCase())) {
|
||
const regex = new RegExp(`(${filters.search})`, 'gi');
|
||
const parts = message.split(regex);
|
||
return parts.map((part, index) =>
|
||
part.toLowerCase() === filters.search.toLowerCase() ?
|
||
<mark key={index}>{part}</mark> : part
|
||
);
|
||
}
|
||
return message;
|
||
},
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div className="log-viewer">
|
||
{/* 统计卡片 */}
|
||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||
<Col span={6}>
|
||
<Card>
|
||
<Statistic title="总计" value={stats.total || 0} />
|
||
</Card>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Card>
|
||
<Statistic
|
||
title="错误"
|
||
value={stats.error || 0}
|
||
valueStyle={{ color: '#ff4d4f' }}
|
||
prefix="❌"
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Card>
|
||
<Statistic
|
||
title="警告"
|
||
value={stats.warn || 0}
|
||
valueStyle={{ color: '#fa8c16' }}
|
||
prefix="⚠️"
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
<Col span={6}>
|
||
<Card>
|
||
<Statistic
|
||
title="信息"
|
||
value={stats.info || 0}
|
||
valueStyle={{ color: '#1890ff' }}
|
||
prefix="ℹ️"
|
||
/>
|
||
</Card>
|
||
</Col>
|
||
</Row>
|
||
|
||
{/* 过滤控件 */}
|
||
<Card style={{ marginBottom: 16 }}>
|
||
<Space wrap>
|
||
<Select
|
||
placeholder="选择日志级别"
|
||
style={{ width: 120 }}
|
||
value={filters.level}
|
||
onChange={(value) => setFilters(prev => ({ ...prev, level: value }))}
|
||
>
|
||
<Option value="">全部级别</Option>
|
||
<Option value="error">错误</Option>
|
||
<Option value="warn">警告</Option>
|
||
<Option value="info">信息</Option>
|
||
<Option value="debug">调试</Option>
|
||
</Select>
|
||
|
||
<Input
|
||
placeholder="搜索关键词"
|
||
style={{ width: 200 }}
|
||
value={filters.search}
|
||
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
|
||
/>
|
||
|
||
<Input
|
||
placeholder="Trace ID"
|
||
style={{ width: 150 }}
|
||
value={filters.trace_id}
|
||
onChange={(e) => setFilters(prev => ({ ...prev, trace_id: e.target.value }))}
|
||
/>
|
||
|
||
<Select
|
||
value={filters.lines}
|
||
style={{ width: 100 }}
|
||
onChange={(value) => setFilters(prev => ({ ...prev, lines: value }))}
|
||
>
|
||
<Option value={50}>50 条</Option>
|
||
<Option value={100}>100 条</Option>
|
||
<Option value={200}>200 条</Option>
|
||
<Option value={500}>500 条</Option>
|
||
</Select>
|
||
|
||
<Button
|
||
onClick={fetchLogs}
|
||
loading={loading}
|
||
>
|
||
🔄 刷新
|
||
</Button>
|
||
|
||
<Button
|
||
type={autoRefresh ? 'primary' : 'default'}
|
||
onClick={() => setAutoRefresh(!autoRefresh)}
|
||
>
|
||
{autoRefresh ? '⏸️ 停止自动刷新' : '⏰ 自动刷新'}
|
||
</Button>
|
||
</Space>
|
||
</Card>
|
||
|
||
{/* 日志表格 */}
|
||
<Card>
|
||
<Table
|
||
columns={columns}
|
||
dataSource={logs}
|
||
loading={loading}
|
||
rowKey={(record, index) => `${record.timestamp}-${index}`}
|
||
pagination={{
|
||
showSizeChanger: false,
|
||
showQuickJumper: true,
|
||
showTotal: (total) => `共 ${total} 条日志`,
|
||
}}
|
||
scroll={{ x: 800 }}
|
||
/>
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default LogViewer;
|
||
```
|
||
|
||
### 3. 配置更新
|
||
|
||
#### 更新应用配置文件
|
||
```yaml
|
||
# config/config.yaml
|
||
app:
|
||
name: "photography-backend"
|
||
version: "1.0.0"
|
||
environment: "production"
|
||
|
||
logger:
|
||
level: "info"
|
||
format: "json"
|
||
output: "file"
|
||
filename: "logs/app.log"
|
||
max_size: 100
|
||
max_age: 7
|
||
compress: true
|
||
|
||
tracing:
|
||
enabled: true
|
||
service_name: "photography-backend"
|
||
sampling_rate: 1.0
|
||
|
||
admin:
|
||
log_access: true # 启用日志访问功能
|
||
log_retention_days: 30 # 日志保留天数
|
||
```
|
||
|
||
### 4. 一键部署脚本
|
||
|
||
#### 部署管理后台日志功能
|
||
```bash
|
||
#!/bin/bash
|
||
# deploy-admin-logs.sh
|
||
|
||
echo "🚀 部署管理后台日志功能..."
|
||
|
||
# 创建必要目录
|
||
mkdir -p logs
|
||
mkdir -p admin/src/pages/Logs
|
||
|
||
# 确保日志文件存在
|
||
touch logs/app.log
|
||
chmod 664 logs/app.log
|
||
|
||
# 创建日志轮转配置
|
||
cat > /etc/logrotate.d/photography-backend << 'EOF'
|
||
/path/to/photography/logs/*.log {
|
||
daily
|
||
rotate 30
|
||
compress
|
||
delaycompress
|
||
missingok
|
||
notifempty
|
||
create 664 app app
|
||
postrotate
|
||
systemctl reload photography-backend
|
||
endscript
|
||
}
|
||
EOF
|
||
|
||
# 设置权限
|
||
chown -R app:app logs/
|
||
chmod 755 logs/
|
||
|
||
echo "✅ 管理后台日志功能部署完成!"
|
||
echo ""
|
||
echo "📋 访问方式:"
|
||
echo " 1. 登录管理后台: http://localhost:8080/admin"
|
||
echo " 2. 进入日志管理页面"
|
||
echo " 3. 使用管理员账号访问"
|
||
echo ""
|
||
echo "🔧 API地址:"
|
||
echo " 日志列表: GET /admin/api/logs"
|
||
echo " 日志统计: GET /admin/api/logs/stats"
|
||
```
|
||
|
||
## 🔧 故障排查流程
|
||
|
||
### 1. 问题诊断步骤
|
||
```bash
|
||
# 快速诊断脚本
|
||
#!/bin/bash
|
||
# scripts/quick-diagnosis.sh
|
||
|
||
echo "🔍 快速问题诊断"
|
||
|
||
# 1. 检查服务状态
|
||
echo "1. 检查服务状态..."
|
||
docker-compose ps
|
||
|
||
# 2. 检查最近错误
|
||
echo "2. 最近10条错误日志..."
|
||
tail -n 1000 logs/app.log | grep -i error | tail -10 | jq -r '.timestamp + " " + .message'
|
||
|
||
# 3. 检查磁盘空间
|
||
echo "3. 检查磁盘空间..."
|
||
df -h
|
||
|
||
# 4. 检查日志文件大小
|
||
echo "4. 检查日志文件大小..."
|
||
ls -lh logs/
|
||
|
||
# 5. 检查内存使用
|
||
echo "5. 检查内存使用..."
|
||
docker stats --no-stream photography-backend
|
||
|
||
echo "✅ 诊断完成"
|
||
```
|
||
|
||
### 2. 常见问题解决
|
||
|
||
#### 问题1:无法找到错误原因
|
||
```bash
|
||
# 1. 获取完整的错误上下文
|
||
grep -B 5 -A 5 "error_message" logs/app.log
|
||
|
||
# 2. 按时间范围查找
|
||
grep "2024-01-15T10:" logs/app.log | grep -i error
|
||
```
|
||
|
||
#### 问题2:需要追踪特定用户的操作
|
||
```bash
|
||
# 按用户ID查找
|
||
grep "user-123" logs/app.log | jq -r '.timestamp + " " + .message'
|
||
|
||
# 按操作类型查找
|
||
grep "create_photo" logs/app.log | jq -r '.timestamp + " " + .message'
|
||
```
|
||
|
||
#### 问题3:日志文件太大
|
||
```bash
|
||
# 手动轮转日志
|
||
mv logs/app.log logs/app.log.old
|
||
sudo systemctl restart photography-backend
|
||
|
||
# 或者使用logrotate
|
||
sudo logrotate -f /etc/logrotate.d/photography-backend
|
||
```
|
||
|
||
## 🎯 总结
|
||
|
||
这个集成到管理后台的日志方案提供了:
|
||
|
||
### ✨ 核心特性
|
||
- **🏢 集成化管理** - 完全集成到管理后台,统一的用户体验
|
||
- **🔐 权限控制** - 基于管理后台的认证和授权体系
|
||
- **📊 专业界面** - 使用React组件,美观且专业
|
||
- **🔍 强大搜索** - 支持关键词、级别、Trace ID多维度过滤
|
||
- **⏰ 实时监控** - 自动刷新功能,实时观察系统状态
|
||
- **📈 统计分析** - 直观的统计卡片,快速了解系统健康状况
|
||
|
||
### 🚀 部署简单
|
||
1. 运行部署脚本: `./deploy-admin-logs.sh`
|
||
2. 访问管理后台: `http://localhost:8080/admin`
|
||
3. 登录管理员账号,进入日志管理页面
|
||
|
||
### 💡 使用场景
|
||
- **错误排查**: 管理员快速定位和分析错误日志
|
||
- **性能监控**: 通过Trace ID追踪完整请求链路
|
||
- **运营监控**: 实时观察系统运行状态
|
||
- **历史分析**: 搜索和分析历史日志数据
|
||
- **团队协作**: 多个管理员可以同时查看和分析日志
|
||
|
||
### 🔧 技术优势
|
||
- **集成性**: 完全集成到现有管理后台
|
||
- **安全性**: 基于管理后台的权限控制
|
||
- **专业性**: 使用成熟的UI组件库
|
||
- **可扩展**: 易于添加新的日志分析功能
|
||
- **用户友好**: 直观的界面,无需学习成本
|
||
|
||
### 📋 API接口
|
||
- `GET /admin/api/logs` - 获取日志列表
|
||
- `GET /admin/api/logs/stats` - 获取日志统计
|
||
|
||
这就是最适合生产环境的日志管理方案 - **专业、安全、易用**! |