Files
photography/docs/v1/backend/运维监控方案.md
2025-07-09 14:32:52 +08:00

38 KiB
Raw Permalink 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. 命令行工具

快速查看错误

#!/bin/bash
# scripts/check-errors.sh

echo "📋 最近的错误日志 (最近1小时):"
tail -f /app/logs/app.log | grep -i error | jq -r '.timestamp + " " + .message'

echo "📊 错误统计:"
grep -i error /app/logs/app.log | jq -r '.message' | sort | uniq -c | sort -nr | head -10

按trace ID查找日志

#!/bin/bash
# scripts/find-by-trace.sh

TRACE_ID=$1
if [ -z "$TRACE_ID" ]; then
    echo "用法: ./find-by-trace.sh <trace_id>"
    exit 1
fi

echo "🔍 查找 trace ID: $TRACE_ID"
grep "$TRACE_ID" /app/logs/app.log | jq -r '.timestamp + " [" + .level + "] " + .message'

实时监控错误

#!/bin/bash
# scripts/monitor-errors.sh

echo "🚨 实时错误监控 (按Ctrl+C退出):"
tail -f /app/logs/app.log | grep --line-buffered -i error | while read line; do
    echo -e "\033[31m[ERROR]\033[0m $(echo "$line" | jq -r '.timestamp + " " + .message')"
done

2. 管理后台日志模块

日志查看组件 (React/Vue)

<!DOCTYPE html>
<html>
<head>
    <title>📋 日志查看器</title>
    <meta charset="utf-8">
    <style>
        * { box-sizing: border-box; }
        body { 
            font-family: 'Monaco', 'Consolas', monospace; 
            margin: 0; padding: 20px; 
            background: #f5f5f5;
        }
        .container { max-width: 1200px; margin: 0 auto; }
        .header { 
            background: white; 
            padding: 20px; 
            border-radius: 8px; 
            margin-bottom: 20px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .controls { 
            display: flex; 
            gap: 15px; 
            flex-wrap: wrap; 
            align-items: center;
        }
        .controls input, .controls select, .controls button { 
            padding: 8px 12px; 
            border: 1px solid #ddd; 
            border-radius: 4px; 
            font-size: 14px;
        }
        .controls button {
            background: #007bff;
            color: white;
            border: 1px solid #007bff;
            cursor: pointer;
        }
        .controls button:hover { background: #0056b3; }
        .controls button.active { background: #28a745; }
        
        .stats { 
            background: white; 
            padding: 15px; 
            border-radius: 8px; 
            margin-bottom: 20px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        
        .logs-container { 
            background: white; 
            border-radius: 8px; 
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            max-height: 70vh; 
            overflow-y: auto;
        }
        
        .log-entry { 
            padding: 12px 15px; 
            border-bottom: 1px solid #eee; 
            border-left: 4px solid #ddd;
            font-size: 13px;
            line-height: 1.4;
        }
        .log-entry:hover { background: #f8f9fa; }
        .log-entry.error { border-left-color: #dc3545; background: #fff5f5; }
        .log-entry.warn { border-left-color: #ffc107; background: #fff8e1; }
        .log-entry.info { border-left-color: #28a745; background: #f0fff4; }
        
        .log-time { color: #666; font-weight: bold; }
        .log-level { 
            display: inline-block; 
            padding: 2px 6px; 
            border-radius: 3px; 
            font-size: 11px; 
            font-weight: bold;
            margin: 0 8px;
        }
        .log-level.error { background: #dc3545; color: white; }
        .log-level.warn { background: #ffc107; color: #212529; }
        .log-level.info { background: #28a745; color: white; }
        .log-level.debug { background: #6c757d; color: white; }
        
        .log-trace { 
            color: #007bff; 
            cursor: pointer; 
            text-decoration: underline;
            margin: 0 8px;
        }
        .log-trace:hover { background: #e3f2fd; }
        
        .log-message { 
            margin-top: 5px; 
            color: #333;
            word-break: break-word;
        }
        
        .loading { 
            text-align: center; 
            padding: 40px; 
            color: #666; 
        }
        .error-msg { 
            text-align: center; 
            padding: 40px; 
            color: #dc3545; 
        }
        
        .search-highlight {
            background: yellow;
            padding: 1px 2px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📋 日志查看器</h1>
            <div class="controls">
                <select id="levelFilter">
                    <option value="">🔍 所有级别</option>
                    <option value="error">❌ 错误</option>
                    <option value="warn">⚠️ 警告</option>
                    <option value="info"> 信息</option>
                    <option value="debug">🐛 调试</option>
                </select>
                
                <input type="text" id="searchFilter" placeholder="🔍 搜索关键词..." style="min-width: 200px;">
                <input type="text" id="traceFilter" placeholder="🔗 Trace ID" style="min-width: 150px;">
                
                <select id="lineCount">
                    <option value="50">50 条</option>
                    <option value="100" selected>100 条</option>
                    <option value="200">200 条</option>
                    <option value="500">500 条</option>
                </select>
                
                <button onclick="loadLogs()" id="refreshBtn">🔄 刷新</button>
                <button onclick="toggleAutoRefresh()" id="autoRefreshBtn">⏰ 自动刷新</button>
                <button onclick="clearLogs()">🗑️ 清空</button>
            </div>
        </div>
        
        <div class="stats" id="stats">📊 正在加载统计...</div>
        
        <div class="logs-container">
            <div id="logs" class="loading">📱 正在加载日志...</div>
        </div>
    </div>

    <script>
        let autoRefreshTimer = null;
        let currentLogs = [];
        
        // 模拟日志数据(实际应该从后端获取)
        function generateMockLogs() {
            const levels = ['info', 'warn', 'error', 'debug'];
            const messages = [
                'Photo created successfully',
                'User authentication completed',
                'Database connection established',
                'Failed to process image',
                'Cache miss for key: photos:list',
                'HTTP request completed',
                'Memory usage is high',
                'File upload completed'
            ];
            
            const logs = [];
            for (let i = 0; i < 100; i++) {
                const level = levels[Math.floor(Math.random() * levels.length)];
                const message = messages[Math.floor(Math.random() * messages.length)];
                const traceId = Math.random() > 0.7 ? `trace-${Math.random().toString(36).substr(2, 8)}` : null;
                
                logs.push({
                    timestamp: new Date(Date.now() - Math.random() * 24 * 60 * 60 * 1000).toISOString(),
                    level: level,
                    message: message,
                    trace_id: traceId,
                    user_id: Math.random() > 0.8 ? `user-${Math.floor(Math.random() * 100)}` : null
                });
            }
            
            return logs.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
        }
        
        function loadLogs() {
            const refreshBtn = document.getElementById('refreshBtn');
            refreshBtn.textContent = '⏳ 加载中...';
            refreshBtn.disabled = true;
            
            // 模拟加载延迟
            setTimeout(() => {
                try {
                    // 这里应该是 fetch('/api/logs?...')
                    currentLogs = generateMockLogs();
                    filterAndDisplayLogs();
                    updateStats();
                } catch (error) {
                    document.getElementById('logs').innerHTML = 
                        '<div class="error-msg">❌ 加载失败: ' + error.message + '</div>';
                }
                
                refreshBtn.textContent = '🔄 刷新';
                refreshBtn.disabled = false;
            }, 500);
        }
        
        function filterAndDisplayLogs() {
            const levelFilter = document.getElementById('levelFilter').value;
            const searchFilter = document.getElementById('searchFilter').value.toLowerCase();
            const traceFilter = document.getElementById('traceFilter').value;
            const lineCount = parseInt(document.getElementById('lineCount').value);
            
            let filteredLogs = currentLogs;
            
            // 级别过滤
            if (levelFilter) {
                filteredLogs = filteredLogs.filter(log => log.level === levelFilter);
            }
            
            // 关键词搜索
            if (searchFilter) {
                filteredLogs = filteredLogs.filter(log => 
                    log.message.toLowerCase().includes(searchFilter) ||
                    (log.trace_id && log.trace_id.toLowerCase().includes(searchFilter))
                );
            }
            
            // Trace ID 过滤
            if (traceFilter) {
                filteredLogs = filteredLogs.filter(log => 
                    log.trace_id && log.trace_id.includes(traceFilter)
                );
            }
            
            // 限制条数
            filteredLogs = filteredLogs.slice(0, lineCount);
            
            displayLogs(filteredLogs, searchFilter);
        }
        
        function displayLogs(logs, searchTerm = '') {
            const container = document.getElementById('logs');
            
            if (logs.length === 0) {
                container.innerHTML = '<div class="loading">📭 没有找到匹配的日志</div>';
                return;
            }
            
            container.innerHTML = logs.map(log => {
                const time = new Date(log.timestamp).toLocaleString();
                let message = log.message;
                
                // 高亮搜索关键词
                if (searchTerm) {
                    const regex = new RegExp(`(${searchTerm})`, 'gi');
                    message = message.replace(regex, '<span class="search-highlight">$1</span>');
                }
                
                return `
                    <div class="log-entry ${log.level}">
                        <div>
                            <span class="log-time">${time}</span>
                            <span class="log-level ${log.level}">${log.level.toUpperCase()}</span>
                            ${log.trace_id ? `<span class="log-trace" onclick="filterByTrace('${log.trace_id}')">${log.trace_id}</span>` : ''}
                        </div>
                        <div class="log-message">${message}</div>
                    </div>
                `;
            }).join('');
        }
        
        function updateStats() {
            const stats = {
                total: currentLogs.length,
                error: currentLogs.filter(l => l.level === 'error').length,
                warn: currentLogs.filter(l => l.level === 'warn').length,
                info: currentLogs.filter(l => l.level === 'info').length,
                debug: currentLogs.filter(l => l.level === 'debug').length
            };
            
            document.getElementById('stats').innerHTML = `
                📊 总计: <strong>${stats.total}</strong> 条 | 
                ❌ 错误: <strong>${stats.error}</strong> | 
                ⚠️ 警告: <strong>${stats.warn}</strong> | 
                 信息: <strong>${stats.info}</strong> | 
                🐛 调试: <strong>${stats.debug}</strong>
            `;
        }
        
        function filterByTrace(traceId) {
            document.getElementById('traceFilter').value = traceId;
            filterAndDisplayLogs();
        }
        
        function toggleAutoRefresh() {
            const btn = document.getElementById('autoRefreshBtn');
            
            if (autoRefreshTimer) {
                clearInterval(autoRefreshTimer);
                autoRefreshTimer = null;
                btn.textContent = '⏰ 自动刷新';
                btn.classList.remove('active');
            } else {
                autoRefreshTimer = setInterval(loadLogs, 5000);
                btn.textContent = '⏸️ 停止刷新';
                btn.classList.add('active');
            }
        }
        
        function clearLogs() {
            if (confirm('确定要清空当前显示的日志吗?')) {
                document.getElementById('logs').innerHTML = '<div class="loading">📭 日志已清空</div>';
                currentLogs = [];
                updateStats();
            }
        }
        
        // 事件监听
        document.getElementById('levelFilter').addEventListener('change', filterAndDisplayLogs);
        document.getElementById('searchFilter').addEventListener('input', filterAndDisplayLogs);
        document.getElementById('traceFilter').addEventListener('input', filterAndDisplayLogs);
        document.getElementById('lineCount').addEventListener('change', filterAndDisplayLogs);
        
        // 快捷键
        document.addEventListener('keydown', function(e) {
            if (e.ctrlKey || e.metaKey) {
                if (e.key === 'r') {
                    e.preventDefault();
                    loadLogs();
                }
                if (e.key === 'f') {
                    e.preventDefault();
                    document.getElementById('searchFilter').focus();
                }
            }
        });
        
        // 初始加载
        loadLogs();
    </script>
</body>
</html>

管理后台日志API接口

集成到现有的管理后台后端:

// 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
}

// NewLogHandler 创建日志处理器
func NewLogHandler(logFile string) *LogHandler {
    return &LogHandler{logFile: logFile}
}

// GetLogs 获取日志列表
// @Summary 获取系统日志
// @Description 获取系统日志列表,支持过滤和搜索
// @Tags 日志管理
// @Accept json
// @Produce json
// @Param level query string false "日志级别" Enums(error,warn,info,debug)
// @Param search query string false "搜索关键词"
// @Param trace_id query string false "Trace ID"
// @Param lines query int false "返回行数" default(100)
// @Success 200 {object} LogListResponse
// @Failure 401 {object} ErrorResponse
// @Failure 403 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /admin/api/logs [get]
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 获取日志统计
// @Summary 获取日志统计信息
// @Description 获取各级别日志的统计数据
// @Tags 日志管理
// @Accept json
// @Produce json
// @Success 200 {object} LogStatsResponse
// @Failure 401 {object} ErrorResponse
// @Failure 403 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /admin/api/logs/stats [get]
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,
    })
}

// readLogs 读取日志文件
func (h *LogHandler) readLogs(maxLines int, levelFilter, searchFilter, traceFilter string) ([]LogEntry, error) {
    file, err := os.Open(h.logFile)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    
    var lines []string
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        lines = append(lines, scanner.Text())
    }
    
    // 从后往前读取最新的日志
    start := len(lines) - maxLines*2  // 预读更多行用于过滤
    if start < 0 {
        start = 0
    }
    
    var logs []LogEntry
    for i := start; i < len(lines) && len(logs) < maxLines; i++ {
        line := lines[i]
        if line == "" {
            continue
        }
        
        var entry LogEntry
        if err := json.Unmarshal([]byte(line), &entry); err != nil {
            continue // 跳过无法解析的行
        }
        
        // 应用过滤条件
        if levelFilter != "" && entry.Level != levelFilter {
            continue
        }
        if searchFilter != "" {
            searchLower := strings.ToLower(searchFilter)
            if !strings.Contains(strings.ToLower(entry.Message), searchLower) &&
               !strings.Contains(strings.ToLower(entry.TraceID), searchLower) &&
               !strings.Contains(strings.ToLower(entry.Operation), searchLower) {
                continue
            }
        }
        if traceFilter != "" && !strings.Contains(entry.TraceID, traceFilter) {
            continue
        }
        
        logs = append(logs, entry)
    }
    
    // 反转数组,让最新的日志在前面
    for i, j := 0, len(logs)-1; i < j; i, j = i+1, j-1 {
        logs[i], logs[j] = logs[j], logs[i]
    }
    
    return logs, nil
}

// 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)
    }
}

管理后台前端集成

在管理后台中添加日志管理页面:

// 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';
import { 
  ReloadOutlined, 
  SearchOutlined,
  ExclamationCircleOutlined,
  WarningOutlined,
  InfoCircleOutlined,
  BugOutlined 
} from '@ant-design/icons';
import { adminApi } from '../../services/api';

const { Option } = Select;
const { Search } = Input;

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: <ExclamationCircleOutlined /> },
    warn: { color: 'orange', icon: <WarningOutlined /> },
    info: { color: 'blue', icon: <InfoCircleOutlined /> },
    debug: { color: 'default', icon: <BugOutlined /> },
  };

  // 获取日志数据
  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} icon={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;
      },
    },
    {
      title: '操作',
      dataIndex: 'operation',
      key: 'operation',
      width: 120,
      render: (operation) => operation || '-',
    },
  ];

  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={<ExclamationCircleOutlined />}
            />
          </Card>
        </Col>
        <Col span={6}>
          <Card>
            <Statistic 
              title="警告" 
              value={stats.warn || 0} 
              valueStyle={{ color: '#fa8c16' }}
              prefix={<WarningOutlined />}
            />
          </Card>
        </Col>
        <Col span={6}>
          <Card>
            <Statistic 
              title="信息" 
              value={stats.info || 0} 
              valueStyle={{ color: '#1890ff' }}
              prefix={<InfoCircleOutlined />}
            />
          </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>

          <Search
            placeholder="搜索关键词"
            style={{ width: 200 }}
            value={filters.search}
            onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value }))}
            prefix={<SearchOutlined />}
          />

          <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 
            icon={<ReloadOutlined />} 
            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;

🚀 集成到管理后台

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")
}

添加权限中间件

// pkg/middleware/auth.go
package middleware

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

// RequireAuth 需要登录认证
func RequireAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 检查JWT token或session
        token := c.GetHeader("Authorization")
        if token == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "未授权访问",
            })
            c.Abort()
            return
        }
        
        // 验证token并获取用户信息
        // ... token验证逻辑
        
        c.Next()
    }
}

// RequireAdmin 需要管理员权限
func RequireAdmin() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 检查用户是否是管理员
        userRole := c.GetString("user_role")
        if userRole != "admin" {
            c.JSON(http.StatusForbidden, gin.H{
                "error": "需要管理员权限",
            })
            c.Abort()
            return
        }
        
        c.Next()
    }
}

2. 前端集成步骤

在管理后台路由中添加日志管理

// admin/src/router/index.js
import LogViewer from '../pages/Logs/LogViewer.jsx';

const routes = [
  // 其他路由...
  {
    path: '/admin',
    component: AdminLayout,
    children: [
      {
        path: 'logs',
        component: LogViewer,
        meta: { 
          title: '日志管理',
          requireAuth: true,
          requireAdmin: true 
        }
      }
      // 其他子路由...
    ]
  }
];

在管理后台菜单中添加日志入口

// admin/src/layout/AdminLayout.jsx
const menuItems = [
  {
    key: 'dashboard',
    icon: <DashboardOutlined />,
    label: '仪表盘',
    path: '/admin/dashboard'
  },
  {
    key: 'photos',
    icon: <PictureOutlined />,
    label: '照片管理',
    path: '/admin/photos'
  },
  {
    key: 'logs',
    icon: <FileTextOutlined />,
    label: '日志管理',
    path: '/admin/logs'
  },
  // 其他菜单项...
];

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

🎯 总结

这个集成到管理后台的日志方案提供了:

核心特性

  • 🏢 集成化管理 - 完全集成到管理后台,统一的用户体验
  • 🔐 权限控制 - 基于管理后台的认证和授权体系
  • 📊 专业界面 - 使用Ant Design组件美观且专业
  • 🔍 强大搜索 - 支持关键词、级别、Trace ID多维度过滤
  • 实时监控 - 自动刷新功能,实时观察系统状态
  • 📈 统计分析 - 直观的统计卡片,快速了解系统健康状况

🚀 部署简单

  1. 运行部署脚本: ./deploy-admin-logs.sh
  2. 访问管理后台: http://localhost:8080/admin
  3. 登录管理员账号,进入日志管理页面

💡 使用场景

  • 错误排查: 管理员快速定位和分析错误日志
  • 性能监控: 通过Trace ID追踪完整请求链路
  • 运营监控: 实时观察系统运行状态
  • 历史分析: 搜索和分析历史日志数据
  • 团队协作: 多个管理员可以同时查看和分析日志

🔧 技术优势

  • 集成性: 完全集成到现有管理后台
  • 安全性: 基于管理后台的权限控制
  • 专业性: 使用成熟的UI组件库
  • 可扩展: 易于添加新的日志分析功能
  • 用户友好: 直观的界面,无需学习成本

🔐 权限控制

  • 认证: 需要管理后台登录
  • 授权: 仅管理员可访问日志功能
  • 安全: API接口有完整的权限验证

📋 API接口

  • GET /admin/api/logs - 获取日志列表
  • GET /admin/api/logs/stats - 获取日志统计

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