持久化数据使用固定路径
This commit is contained in:
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"ai-gateway/internal/billing"
|
||||
"ai-gateway/internal/models"
|
||||
"ai-gateway/internal/scheduler"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -15,7 +16,8 @@ import (
|
||||
|
||||
// APIHandler 持有数据库连接并处理API请求
|
||||
type APIHandler struct {
|
||||
DB *gorm.DB
|
||||
DB *gorm.DB
|
||||
LogCleaner *scheduler.LogCleaner
|
||||
}
|
||||
|
||||
// RequestLogListItem 精简的请求日志列表项(不包含RequestBody和ResponseBody)
|
||||
@@ -398,6 +400,14 @@ func (h *APIHandler) ClearRequestLogsHandler(c *gin.Context) {
|
||||
// 获取查询参数
|
||||
olderThan := c.Query("older_than") // 清空多少天前的日志
|
||||
|
||||
// 先查询当前总记录数(包括软删除的记录)
|
||||
var totalCountBefore int64
|
||||
h.DB.Unscoped().Model(&models.RequestLog{}).Count(&totalCountBefore)
|
||||
|
||||
// 查询活跃记录数(不包括软删除的记录)
|
||||
var activeCountBefore int64
|
||||
h.DB.Model(&models.RequestLog{}).Count(&activeCountBefore)
|
||||
|
||||
var startTime time.Time
|
||||
if olderThan != "" {
|
||||
// 解析天数
|
||||
@@ -414,19 +424,40 @@ func (h *APIHandler) ClearRequestLogsHandler(c *gin.Context) {
|
||||
|
||||
// 如果是0天,要删除所有日志,不需要时间限制
|
||||
if days == 0 {
|
||||
// 执行删除所有日志的操作
|
||||
result := h.DB.Where("1 = 1").Delete(&models.RequestLog{})
|
||||
// 使用Unscoped进行硬删除,真正删除记录而非软删除
|
||||
result := h.DB.Unscoped().Where("1 = 1").Delete(&models.RequestLog{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear logs"})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回删除的记录数
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "All logs cleared successfully",
|
||||
// 执行VACUUM来回收数据库空间
|
||||
vacuumError := ""
|
||||
if err := h.DB.Exec("VACUUM").Error; err != nil {
|
||||
vacuumError = fmt.Sprintf("VACUUM failed: %v", err)
|
||||
fmt.Printf("Warning: %s\n", vacuumError)
|
||||
}
|
||||
|
||||
// 查询删除后的记录数
|
||||
var totalCountAfter int64
|
||||
h.DB.Unscoped().Model(&models.RequestLog{}).Count(&totalCountAfter)
|
||||
|
||||
// 返回详细的删除信息
|
||||
response := gin.H{
|
||||
"message": "All logs cleared successfully with hard delete and vacuum",
|
||||
"deleted_count": result.RowsAffected,
|
||||
"older_than_days": olderThan,
|
||||
})
|
||||
"total_count_before": totalCountBefore,
|
||||
"active_count_before": activeCountBefore,
|
||||
"total_count_after": totalCountAfter,
|
||||
"hard_delete": true,
|
||||
"vacuum_executed": vacuumError == "",
|
||||
}
|
||||
if vacuumError != "" {
|
||||
response["vacuum_error"] = vacuumError
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -436,17 +467,91 @@ func (h *APIHandler) ClearRequestLogsHandler(c *gin.Context) {
|
||||
startTime = time.Now().AddDate(0, 0, -30)
|
||||
}
|
||||
|
||||
// 执行删除操作
|
||||
result := h.DB.Where("created_at < ?", startTime).Delete(&models.RequestLog{})
|
||||
// 使用Unscoped执行硬删除操作,真正删除记录而非软删除
|
||||
result := h.DB.Unscoped().Where("created_at < ?", startTime).Delete(&models.RequestLog{})
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear logs"})
|
||||
return
|
||||
}
|
||||
|
||||
// 返回删除的记录数
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Logs cleared successfully",
|
||||
// 执行VACUUM来回收数据库空间
|
||||
vacuumError := ""
|
||||
if err := h.DB.Exec("VACUUM").Error; err != nil {
|
||||
vacuumError = fmt.Sprintf("VACUUM failed: %v", err)
|
||||
fmt.Printf("Warning: %s\n", vacuumError)
|
||||
}
|
||||
|
||||
// 查询删除后的记录数
|
||||
var totalCountAfter int64
|
||||
h.DB.Unscoped().Model(&models.RequestLog{}).Count(&totalCountAfter)
|
||||
|
||||
// 返回详细的删除信息
|
||||
response := gin.H{
|
||||
"message": "Logs cleared successfully with hard delete and vacuum",
|
||||
"deleted_count": result.RowsAffected,
|
||||
"older_than_days": olderThan,
|
||||
"cutoff_time": startTime,
|
||||
"total_count_before": totalCountBefore,
|
||||
"active_count_before": activeCountBefore,
|
||||
"total_count_after": totalCountAfter,
|
||||
"hard_delete": true,
|
||||
"vacuum_executed": vacuumError == "",
|
||||
}
|
||||
if vacuumError != "" {
|
||||
response["vacuum_error"] = vacuumError
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
// GetLogCleanerStatusHandler 获取日志清理器状态
|
||||
func (h *APIHandler) GetLogCleanerStatusHandler(c *gin.Context) {
|
||||
if h.LogCleaner == nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"enabled": false,
|
||||
"message": "Log cleaner is not initialized",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
config := h.LogCleaner.GetConfig()
|
||||
nextExecuteTime := h.LogCleaner.GetNextExecuteTime()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"enabled": config.Enabled,
|
||||
"execute_time": config.ExecuteTime,
|
||||
"retention_days": config.RetentionDays,
|
||||
"check_interval": config.CheckInterval,
|
||||
"next_execute_time": nextExecuteTime.Format("2006-01-02 15:04:05"),
|
||||
"time_until_next": time.Until(nextExecuteTime).String(),
|
||||
})
|
||||
}
|
||||
|
||||
// ForceLogCleanupHandler 手动触发日志清理
|
||||
func (h *APIHandler) ForceLogCleanupHandler(c *gin.Context) {
|
||||
if h.LogCleaner == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Log cleaner is not initialized",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行清理
|
||||
report := h.LogCleaner.ForceCleanup()
|
||||
|
||||
// 返回清理报告
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Manual log cleanup completed",
|
||||
"execute_time": report.ExecuteTime.Format("2006-01-02 15:04:05"),
|
||||
"duration": report.Duration.String(),
|
||||
"deleted_count": report.DeletedCount,
|
||||
"total_count_before": report.TotalCountBefore,
|
||||
"active_count_before": report.ActiveCountBefore,
|
||||
"total_count_after": report.TotalCountAfter,
|
||||
"cutoff_time": report.CutoffTime.Format("2006-01-02 15:04:05"),
|
||||
"vacuum_duration": report.VacuumDuration.String(),
|
||||
"vacuum_error": report.VacuumError,
|
||||
"success": report.Success,
|
||||
})
|
||||
}
|
||||
|
||||
168
backend/internal/config/config.go
Normal file
168
backend/internal/config/config.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"ai-gateway/internal/scheduler"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 固定路径常量 - 用于 Docker 挂载
|
||||
const (
|
||||
// DataDir 数据目录,用于存放配置和数据库文件(固定路径,便于 Docker 挂载)
|
||||
DataDir = "/data"
|
||||
// ConfigPath 配置文件固定路径
|
||||
ConfigPath = "/data/config.json"
|
||||
// DatabasePath 数据库文件固定路径
|
||||
DatabasePath = "/data/gateway.db"
|
||||
)
|
||||
|
||||
// Config 应用程序配置结构
|
||||
type Config struct {
|
||||
// 服务器配置
|
||||
Server ServerConfig `json:"server"`
|
||||
// 日志清理配置
|
||||
LogCleaner scheduler.LogCleanerConfig `json:"log_cleaner"`
|
||||
// 应用配置
|
||||
App AppConfig `json:"app"`
|
||||
}
|
||||
|
||||
// ServerConfig 服务器配置
|
||||
type ServerConfig struct {
|
||||
Port string `json:"port"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
// AppConfig 应用配置
|
||||
type AppConfig struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Environment string `json:"environment"`
|
||||
LogLevel string `json:"log_level"`
|
||||
}
|
||||
|
||||
// DefaultConfig 默认配置
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Port: "8080",
|
||||
Host: "0.0.0.0",
|
||||
},
|
||||
LogCleaner: scheduler.LogCleanerConfig{
|
||||
Enabled: true, // 默认启用自动清理
|
||||
ExecuteTime: "02:00", // 凌晨2点执行
|
||||
RetentionDays: 7, // 保留7天的日志
|
||||
CheckInterval: 5, // 每5分钟检查一次
|
||||
},
|
||||
App: AppConfig{
|
||||
Name: "AI Gateway",
|
||||
Version: "1.0.0",
|
||||
Environment: "production",
|
||||
LogLevel: "info",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfig 从文件加载配置
|
||||
func LoadConfig(filepath string) (*Config, error) {
|
||||
// 确保数据目录存在
|
||||
if err := os.MkdirAll(DataDir, 0755); err != nil {
|
||||
log.Printf("⚠️ 创建数据目录失败: %v", err)
|
||||
}
|
||||
// 如果配置文件不存在,返回默认配置并创建配置文件
|
||||
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||
log.Printf("⚠️ 配置文件不存在,使用默认配置并创建: %s", filepath)
|
||||
config := DefaultConfig()
|
||||
if err := config.Save(filepath); err != nil {
|
||||
log.Printf("❌ 保存默认配置失败: %v", err)
|
||||
// 即使保存失败,仍然返回默认配置
|
||||
return config, nil
|
||||
}
|
||||
log.Printf("✅ 已创建默认配置文件: %s", filepath)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// 读取配置文件
|
||||
data, err := ioutil.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取配置文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析JSON配置
|
||||
var config Config
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("解析配置文件失败: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("✅ 配置文件加载成功: %s", filepath)
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// LoadConfigOrDefault 加载配置或使用默认配置(不会失败)
|
||||
func LoadConfigOrDefault(filepath string) *Config {
|
||||
config, err := LoadConfig(filepath)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 加载配置失败,使用默认配置: %v", err)
|
||||
return DefaultConfig()
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// Save 保存配置到文件
|
||||
func (c *Config) Save(filepath string) error {
|
||||
// 将配置序列化为格式化的JSON
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
if err := ioutil.WriteFile(filepath, data, 0644); err != nil {
|
||||
return fmt.Errorf("写入配置文件失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证配置的有效性
|
||||
func (c *Config) Validate() error {
|
||||
// 验证服务器端口
|
||||
if c.Server.Port == "" {
|
||||
return fmt.Errorf("服务器端口不能为空")
|
||||
}
|
||||
|
||||
// 验证日志清理配置
|
||||
if c.LogCleaner.Enabled {
|
||||
if c.LogCleaner.ExecuteTime == "" {
|
||||
return fmt.Errorf("日志清理执行时间不能为空")
|
||||
}
|
||||
if c.LogCleaner.RetentionDays < 0 {
|
||||
return fmt.Errorf("日志保留天数不能为负数")
|
||||
}
|
||||
if c.LogCleaner.CheckInterval <= 0 {
|
||||
c.LogCleaner.CheckInterval = 5 // 设置默认值
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Print 打印配置信息(用于调试)
|
||||
func (c *Config) Print() {
|
||||
log.Println("📋 当前配置:")
|
||||
log.Printf(" 📁 数据目录: %s", DataDir)
|
||||
log.Printf(" 📄 配置文件: %s", ConfigPath)
|
||||
log.Printf(" 🗄️ 数据库路径: %s (固定)", DatabasePath)
|
||||
log.Printf(" 🌐 服务器地址: %s:%s", c.Server.Host, c.Server.Port)
|
||||
log.Printf(" 🧹 日志自动清理: %v", c.LogCleaner.Enabled)
|
||||
if c.LogCleaner.Enabled {
|
||||
log.Printf(" ⏰ 执行时间: %s", c.LogCleaner.ExecuteTime)
|
||||
log.Printf(" 📅 保留天数: %d天", c.LogCleaner.RetentionDays)
|
||||
log.Printf(" 🔍 检查间隔: %d分钟", c.LogCleaner.CheckInterval)
|
||||
}
|
||||
log.Printf(" 📱 应用名称: %s v%s", c.App.Name, c.App.Version)
|
||||
log.Printf(" 🌍 运行环境: %s", c.App.Environment)
|
||||
log.Printf(" 📝 日志级别: %s", c.App.LogLevel)
|
||||
}
|
||||
@@ -3,20 +3,14 @@ package db
|
||||
import (
|
||||
"ai-gateway/internal/models"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// InitDB 初始化数据库连接并执行自动迁移
|
||||
func InitDB() (*gorm.DB, error) {
|
||||
// 从环境变量读取数据库路径,如果未设置则使用默认路径
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
if dbPath == "" {
|
||||
dbPath = "gateway.db"
|
||||
}
|
||||
|
||||
// 参数 dbPath: 数据库文件路径
|
||||
func InitDB(dbPath string) (*gorm.DB, error) {
|
||||
// 使用SQLite驱动连接到数据库文件
|
||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
|
||||
279
backend/internal/scheduler/log_cleaner.go
Normal file
279
backend/internal/scheduler/log_cleaner.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"ai-gateway/internal/models"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// LogCleanerConfig 日志清理器配置
|
||||
type LogCleanerConfig struct {
|
||||
// 启用状态
|
||||
Enabled bool `json:"enabled"`
|
||||
// 执行时间(24小时制,如:"02:00" 表示凌晨2点)
|
||||
ExecuteTime string `json:"execute_time"`
|
||||
// 保留天数(清理多少天之前的日志)
|
||||
RetentionDays int `json:"retention_days"`
|
||||
// 检查间隔(分钟)
|
||||
CheckInterval int `json:"check_interval"`
|
||||
}
|
||||
|
||||
// LogCleaner 日志清理器
|
||||
type LogCleaner struct {
|
||||
db *gorm.DB
|
||||
config LogCleanerConfig
|
||||
ticker *time.Ticker
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
// NewLogCleaner 创建日志清理器实例
|
||||
func NewLogCleaner(db *gorm.DB, config LogCleanerConfig) *LogCleaner {
|
||||
return &LogCleaner{
|
||||
db: db,
|
||||
config: config,
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动定时清理任务
|
||||
func (lc *LogCleaner) Start() {
|
||||
if !lc.config.Enabled {
|
||||
log.Println("📅 日志自动清理功能已禁用")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("🚀 启动日志自动清理器 - 执行时间: %s, 保留天数: %d天",
|
||||
lc.config.ExecuteTime, lc.config.RetentionDays)
|
||||
|
||||
// 设置检查间隔,默认5分钟检查一次
|
||||
checkInterval := time.Duration(lc.config.CheckInterval) * time.Minute
|
||||
if checkInterval == 0 {
|
||||
checkInterval = 5 * time.Minute
|
||||
}
|
||||
|
||||
lc.ticker = time.NewTicker(checkInterval)
|
||||
|
||||
go func() {
|
||||
// 启动时先检查一次是否需要立即执行
|
||||
lc.checkAndExecuteCleanup()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-lc.ticker.C:
|
||||
lc.checkAndExecuteCleanup()
|
||||
case <-lc.quit:
|
||||
lc.ticker.Stop()
|
||||
log.Println("🛑 日志自动清理器已停止")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop 停止定时清理任务
|
||||
func (lc *LogCleaner) Stop() {
|
||||
if lc.ticker != nil {
|
||||
close(lc.quit)
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndExecuteCleanup 检查并执行清理任务
|
||||
func (lc *LogCleaner) checkAndExecuteCleanup() {
|
||||
now := time.Now()
|
||||
|
||||
// 解析配置的执行时间
|
||||
targetTime, err := time.Parse("15:04", lc.config.ExecuteTime)
|
||||
if err != nil {
|
||||
log.Printf("❌ 执行时间格式错误: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构造今天的目标执行时间
|
||||
todayTarget := time.Date(now.Year(), now.Month(), now.Day(),
|
||||
targetTime.Hour(), targetTime.Minute(), 0, 0, now.Location())
|
||||
|
||||
// 如果当前时间已经超过今天的目标时间,则计算明天的目标时间
|
||||
if now.After(todayTarget) {
|
||||
todayTarget = todayTarget.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
// 检查是否到了执行时间(允许5分钟的误差)
|
||||
timeDiff := todayTarget.Sub(now)
|
||||
if timeDiff > 0 && timeDiff <= 5*time.Minute {
|
||||
log.Printf("⏰ 开始执行定时日志清理任务 - 当前时间: %s", now.Format("2006-01-02 15:04:05"))
|
||||
lc.executeCleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// executeCleanup 执行日志清理
|
||||
func (lc *LogCleaner) executeCleanup() {
|
||||
startTime := time.Now()
|
||||
|
||||
// 计算清理的截止时间
|
||||
cutoffTime := startTime.AddDate(0, 0, -lc.config.RetentionDays)
|
||||
|
||||
log.Printf("🧹 开始自动清理日志 - 删除 %s 之前的日志", cutoffTime.Format("2006-01-02 15:04:05"))
|
||||
|
||||
// 查询删除前的统计信息
|
||||
var totalCountBefore, activeCountBefore int64
|
||||
lc.db.Unscoped().Model(&models.RequestLog{}).Count(&totalCountBefore)
|
||||
lc.db.Model(&models.RequestLog{}).Count(&activeCountBefore)
|
||||
|
||||
// 执行硬删除
|
||||
result := lc.db.Unscoped().Where("created_at < ?", cutoffTime).Delete(&models.RequestLog{})
|
||||
if result.Error != nil {
|
||||
log.Printf("❌ 自动清理日志失败: %v", result.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 执行VACUUM操作回收空间
|
||||
vacuumStart := time.Now()
|
||||
vacuumError := ""
|
||||
if err := lc.db.Exec("VACUUM").Error; err != nil {
|
||||
vacuumError = fmt.Sprintf("VACUUM failed: %v", err)
|
||||
log.Printf("⚠️ 警告: %s", vacuumError)
|
||||
}
|
||||
vacuumDuration := time.Since(vacuumStart)
|
||||
|
||||
// 查询删除后的统计信息
|
||||
var totalCountAfter int64
|
||||
lc.db.Unscoped().Model(&models.RequestLog{}).Count(&totalCountAfter)
|
||||
|
||||
// 计算执行时间
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// 记录清理结果
|
||||
log.Printf("✅ 自动日志清理完成:")
|
||||
log.Printf(" 📊 删除记录数: %d", result.RowsAffected)
|
||||
log.Printf(" 📈 删除前总数: %d (活跃: %d)", totalCountBefore, activeCountBefore)
|
||||
log.Printf(" 📉 删除后总数: %d", totalCountAfter)
|
||||
log.Printf(" 🗜️ VACUUM耗时: %v", vacuumDuration)
|
||||
log.Printf(" ⏱️ 总耗时: %v", duration)
|
||||
log.Printf(" 📅 清理截止时间: %s", cutoffTime.Format("2006-01-02 15:04:05"))
|
||||
|
||||
if vacuumError != "" {
|
||||
log.Printf(" ⚠️ VACUUM警告: %s", vacuumError)
|
||||
}
|
||||
|
||||
// 可选:发送清理报告到监控系统或通知系统
|
||||
lc.sendCleanupReport(CleanupReport{
|
||||
ExecuteTime: startTime,
|
||||
Duration: duration,
|
||||
DeletedCount: result.RowsAffected,
|
||||
TotalCountBefore: totalCountBefore,
|
||||
ActiveCountBefore: activeCountBefore,
|
||||
TotalCountAfter: totalCountAfter,
|
||||
CutoffTime: cutoffTime,
|
||||
VacuumDuration: vacuumDuration,
|
||||
VacuumError: vacuumError,
|
||||
Success: result.Error == nil,
|
||||
})
|
||||
}
|
||||
|
||||
// CleanupReport 清理报告结构
|
||||
type CleanupReport struct {
|
||||
ExecuteTime time.Time `json:"execute_time"`
|
||||
Duration time.Duration `json:"duration"`
|
||||
DeletedCount int64 `json:"deleted_count"`
|
||||
TotalCountBefore int64 `json:"total_count_before"`
|
||||
ActiveCountBefore int64 `json:"active_count_before"`
|
||||
TotalCountAfter int64 `json:"total_count_after"`
|
||||
CutoffTime time.Time `json:"cutoff_time"`
|
||||
VacuumDuration time.Duration `json:"vacuum_duration"`
|
||||
VacuumError string `json:"vacuum_error,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// sendCleanupReport 发送清理报告(可扩展为发送到监控系统)
|
||||
func (lc *LogCleaner) sendCleanupReport(report CleanupReport) {
|
||||
// 这里可以扩展为发送报告到:
|
||||
// 1. 监控系统(如Prometheus)
|
||||
// 2. 通知系统(如邮件、企业微信等)
|
||||
// 3. 日志聚合系统(如ELK)
|
||||
// 4. 数据库记录清理历史
|
||||
|
||||
// 目前只记录到应用日志
|
||||
if report.Success {
|
||||
log.Printf("📋 清理报告已生成 - 删除 %d 条记录,耗时 %v",
|
||||
report.DeletedCount, report.Duration)
|
||||
} else {
|
||||
log.Printf("📋 清理报告已生成 - 执行失败,耗时 %v", report.Duration)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取当前配置
|
||||
func (lc *LogCleaner) GetConfig() LogCleanerConfig {
|
||||
return lc.config
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置(运行时动态调整)
|
||||
func (lc *LogCleaner) UpdateConfig(newConfig LogCleanerConfig) {
|
||||
lc.config = newConfig
|
||||
log.Printf("🔄 日志清理器配置已更新 - 启用: %v, 执行时间: %s, 保留天数: %d",
|
||||
newConfig.Enabled, newConfig.ExecuteTime, newConfig.RetentionDays)
|
||||
}
|
||||
|
||||
// GetNextExecuteTime 获取下次执行时间
|
||||
func (lc *LogCleaner) GetNextExecuteTime() time.Time {
|
||||
now := time.Now()
|
||||
targetTime, err := time.Parse("15:04", lc.config.ExecuteTime)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// 构造今天的目标执行时间
|
||||
todayTarget := time.Date(now.Year(), now.Month(), now.Day(),
|
||||
targetTime.Hour(), targetTime.Minute(), 0, 0, now.Location())
|
||||
|
||||
// 如果当前时间已经超过今天的目标时间,则返回明天的目标时间
|
||||
if now.After(todayTarget) {
|
||||
return todayTarget.Add(24 * time.Hour)
|
||||
}
|
||||
|
||||
return todayTarget
|
||||
}
|
||||
|
||||
// ForceCleanup 手动触发清理(用于测试或紧急清理)
|
||||
func (lc *LogCleaner) ForceCleanup() CleanupReport {
|
||||
log.Printf("🔧 手动触发日志清理")
|
||||
|
||||
startTime := time.Now()
|
||||
cutoffTime := startTime.AddDate(0, 0, -lc.config.RetentionDays)
|
||||
|
||||
var totalCountBefore, activeCountBefore int64
|
||||
lc.db.Unscoped().Model(&models.RequestLog{}).Count(&totalCountBefore)
|
||||
lc.db.Model(&models.RequestLog{}).Count(&activeCountBefore)
|
||||
|
||||
result := lc.db.Unscoped().Where("created_at < ?", cutoffTime).Delete(&models.RequestLog{})
|
||||
|
||||
vacuumStart := time.Now()
|
||||
vacuumError := ""
|
||||
if err := lc.db.Exec("VACUUM").Error; err != nil {
|
||||
vacuumError = fmt.Sprintf("VACUUM failed: %v", err)
|
||||
}
|
||||
vacuumDuration := time.Since(vacuumStart)
|
||||
|
||||
var totalCountAfter int64
|
||||
lc.db.Unscoped().Model(&models.RequestLog{}).Count(&totalCountAfter)
|
||||
|
||||
duration := time.Since(startTime)
|
||||
|
||||
report := CleanupReport{
|
||||
ExecuteTime: startTime,
|
||||
Duration: duration,
|
||||
DeletedCount: result.RowsAffected,
|
||||
TotalCountBefore: totalCountBefore,
|
||||
ActiveCountBefore: activeCountBefore,
|
||||
TotalCountAfter: totalCountAfter,
|
||||
CutoffTime: cutoffTime,
|
||||
VacuumDuration: vacuumDuration,
|
||||
VacuumError: vacuumError,
|
||||
Success: result.Error == nil,
|
||||
}
|
||||
|
||||
lc.sendCleanupReport(report)
|
||||
return report
|
||||
}
|
||||
@@ -2,21 +2,51 @@ package main
|
||||
|
||||
import (
|
||||
"ai-gateway/api"
|
||||
"ai-gateway/internal/config"
|
||||
"ai-gateway/internal/db"
|
||||
"ai-gateway/internal/middleware"
|
||||
"ai-gateway/internal/scheduler"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 初始化数据库连接
|
||||
database, err := db.InitDB()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize database: %v", err)
|
||||
log.Println("🚀 启动 AI Gateway 服务...")
|
||||
|
||||
// 加载配置文件(使用固定路径,便于 Docker 挂载)
|
||||
cfg := config.LoadConfigOrDefault(config.ConfigPath)
|
||||
cfg.Print()
|
||||
|
||||
// 验证配置
|
||||
if err := cfg.Validate(); err != nil {
|
||||
log.Printf("⚠️ 配置验证失败: %v,使用默认配置", err)
|
||||
cfg = config.DefaultConfig()
|
||||
}
|
||||
|
||||
// 初始化数据库连接(使用固定数据库路径)
|
||||
database, err := db.InitDB(config.DatabasePath)
|
||||
if err != nil {
|
||||
log.Fatalf("❌ 数据库初始化失败: %v", err)
|
||||
}
|
||||
log.Printf("✅ 数据库连接成功: %s", config.DatabasePath)
|
||||
|
||||
// 初始化并启动日志清理器
|
||||
logCleaner := scheduler.NewLogCleaner(database, cfg.LogCleaner)
|
||||
logCleaner.Start()
|
||||
log.Println("✅ 日志自动清理器已启动")
|
||||
|
||||
// 设置优雅关闭
|
||||
defer func() {
|
||||
log.Println("🛑 正在关闭服务...")
|
||||
logCleaner.Stop()
|
||||
log.Println("✅ 日志自动清理器已停止")
|
||||
}()
|
||||
|
||||
// 创建Gin路由器实例
|
||||
router := gin.Default()
|
||||
|
||||
@@ -36,7 +66,8 @@ func main() {
|
||||
|
||||
// 创建API处理器
|
||||
handler := &api.APIHandler{
|
||||
DB: database,
|
||||
DB: database,
|
||||
LogCleaner: logCleaner,
|
||||
}
|
||||
|
||||
// 添加健康检查端点(无需认证)
|
||||
@@ -83,11 +114,28 @@ func main() {
|
||||
api_.GET("/logs/stats", handler.GetRequestLogStatsHandler)
|
||||
api_.GET("/logs/:id", handler.GetRequestLogDetailHandler)
|
||||
api_.DELETE("/logs", handler.ClearRequestLogsHandler)
|
||||
|
||||
// Log Cleaner Management
|
||||
api_.GET("/log-cleaner/status", handler.GetLogCleanerStatusHandler)
|
||||
api_.POST("/log-cleaner/force-cleanup", handler.ForceLogCleanupHandler)
|
||||
}
|
||||
|
||||
// 启动HTTP服务器
|
||||
log.Println("Starting AI Gateway server on :8080")
|
||||
if err := router.Run(":8080"); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
// 设置优雅关闭信号处理
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// 在goroutine中启动HTTP服务器
|
||||
go func() {
|
||||
serverAddr := cfg.Server.Host + ":" + cfg.Server.Port
|
||||
log.Printf("🌐 HTTP服务器启动在 %s", serverAddr)
|
||||
if err := router.Run(serverAddr); err != nil {
|
||||
log.Fatalf("❌ HTTP服务器启动失败: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待退出信号
|
||||
<-quit
|
||||
log.Println("🛑 收到退出信号,正在优雅关闭...")
|
||||
logCleaner.Stop()
|
||||
log.Println("✅ AI Gateway 服务已关闭")
|
||||
}
|
||||
|
||||
18
config.example.json
Normal file
18
config.example.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"server": {
|
||||
"port": "8080",
|
||||
"host": "0.0.0.0"
|
||||
},
|
||||
"log_cleaner": {
|
||||
"enabled": true,
|
||||
"execute_time": "02:00",
|
||||
"retention_days": 7,
|
||||
"check_interval": 5
|
||||
},
|
||||
"app": {
|
||||
"name": "AI Gateway",
|
||||
"version": "1.0.0",
|
||||
"environment": "production",
|
||||
"log_level": "info"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user