Files
photography/docs/development/saved-docs/operations-monitoring.md
2025-07-09 14:32:52 +08:00

710 lines
19 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 摄影作品集网站 - 运维监控方案
## 🎯 方案概述
这是一个**简单实用的日志管理方案**专注于日志收集和问题修复。日志查看功能集成到管理后台提供友好的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` - 获取日志统计
这就是最适合生产环境的日志管理方案 - **专业、安全、易用**