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

19 KiB
Raw Blame History

摄影作品集网站 - 运维监控方案

🎯 方案概述

这是一个简单实用的日志管理方案专注于日志收集和问题修复。日志查看功能集成到管理后台提供友好的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)

# 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. 日志格式标准化

统一的日志格式

{
  "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. 日志分类

三种核心日志类型

# 日志目录结构
logs/
├── app.log          # 应用日志 (所有级别)
├── error.log        # 错误日志 (ERROR级别)
└── access.log       # HTTP访问日志

日志级别使用

// 日志级别使用指南
logger.Info("正常业务操作")     // 记录重要的业务操作
logger.Warn("需要关注的情况")   // 记录警告信息
logger.Error("错误情况")       // 记录错误信息
logger.Debug("调试信息")       // 开发调试用

🚀 集成到管理后台

1. 后端集成步骤

在main.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")
}

日志处理器实现

// 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. 前端集成步骤

管理后台日志查看组件

// 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. 配置更新

更新应用配置文件

# 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. 一键部署脚本

部署管理后台日志功能

#!/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. 问题诊断步骤

# 快速诊断脚本
#!/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无法找到错误原因

# 1. 获取完整的错误上下文
grep -B 5 -A 5 "error_message" logs/app.log

# 2. 按时间范围查找
grep "2024-01-15T10:" logs/app.log | grep -i error

问题2需要追踪特定用户的操作

# 按用户ID查找
grep "user-123" logs/app.log | jq -r '.timestamp + " " + .message'

# 按操作类型查找
grep "create_photo" logs/app.log | jq -r '.timestamp + " " + .message'

问题3日志文件太大

# 手动轮转日志
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 - 获取日志统计

这就是最适合生产环境的日志管理方案 - 专业、安全、易用