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

1320 lines
38 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. 命令行工具
#### 快速查看错误
```bash
#!/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查找日志
```bash
#!/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'
```
#### 实时监控错误
```bash
#!/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)
```html
<!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接口
集成到现有的管理后台后端:
```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
}
// 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)
}
}
```
#### 管理后台前端集成
在管理后台中添加日志管理页面:
```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';
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中注册日志路由
```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
// 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. 前端集成步骤
#### 在管理后台路由中添加日志管理
```javascript
// 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
}
}
// 其他子路由...
]
}
];
```
#### 在管理后台菜单中添加日志入口
```javascript
// 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. 配置更新
#### 更新应用配置文件
```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
```
## 🎯 总结
这个集成到管理后台的日志方案提供了:
### ✨ 核心特性
- **🏢 集成化管理** - 完全集成到管理后台,统一的用户体验
- **🔐 权限控制** - 基于管理后台的认证和授权体系
- **📊 专业界面** - 使用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` - 获取日志统计
这就是最适合生产环境的日志管理方案 - **专业、安全、易用**