38 KiB
38 KiB
摄影作品集网站 - 日志管理方案
🎯 方案概述
这是一个简单实用的日志管理方案,专注于日志收集和问题修复。日志查看功能集成到管理后台,提供友好的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多维度过滤
- ⏰ 实时监控 - 自动刷新功能,实时观察系统状态
- 📈 统计分析 - 直观的统计卡片,快速了解系统健康状况
🚀 部署简单
- 运行部署脚本:
./deploy-admin-logs.sh - 访问管理后台:
http://localhost:8080/admin - 登录管理员账号,进入日志管理页面
💡 使用场景
- 错误排查: 管理员快速定位和分析错误日志
- 性能监控: 通过Trace ID追踪完整请求链路
- 运营监控: 实时观察系统运行状态
- 历史分析: 搜索和分析历史日志数据
- 团队协作: 多个管理员可以同时查看和分析日志
🔧 技术优势
- 集成性: 完全集成到现有管理后台
- 安全性: 基于管理后台的权限控制
- 专业性: 使用成熟的UI组件库
- 可扩展: 易于添加新的日志分析功能
- 用户友好: 直观的界面,无需学习成本
🔐 权限控制
- 认证: 需要管理后台登录
- 授权: 仅管理员可访问日志功能
- 安全: API接口有完整的权限验证
📋 API接口
GET /admin/api/logs- 获取日志列表GET /admin/api/logs/stats- 获取日志统计
这就是最适合生产环境的日志管理方案 - 专业、安全、易用!