# 摄影作品集网站 - 运维监控方案
## 🎯 方案概述
这是一个**简单实用的日志管理方案**,专注于日志收集和问题修复。日志查看功能集成到管理后台,提供友好的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 (