Files
tyapi-server/internal/shared/external_logger/external_logger.go
2025-08-25 15:44:06 +08:00

316 lines
9.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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.

package external_logger
import (
"fmt"
"os"
"path/filepath"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
// ExternalServiceLoggingConfig 外部服务日志配置
type ExternalServiceLoggingConfig struct {
Enabled bool `yaml:"enabled"`
LogDir string `yaml:"log_dir"`
ServiceName string `yaml:"service_name"` // 服务名称,用于区分日志目录
UseDaily bool `yaml:"use_daily"`
EnableLevelSeparation bool `yaml:"enable_level_separation"`
LevelConfigs map[string]ExternalServiceLevelFileConfig `yaml:"level_configs"`
}
// ExternalServiceLevelFileConfig 外部服务级别文件配置
type ExternalServiceLevelFileConfig struct {
MaxSize int `yaml:"max_size"`
MaxBackups int `yaml:"max_backups"`
MaxAge int `yaml:"max_age"`
Compress bool `yaml:"compress"`
}
// ExternalServiceLogger 外部服务日志器
type ExternalServiceLogger struct {
logger *zap.Logger
config ExternalServiceLoggingConfig
serviceName string
}
// NewExternalServiceLogger 创建外部服务日志器
func NewExternalServiceLogger(config ExternalServiceLoggingConfig) (*ExternalServiceLogger, error) {
if !config.Enabled {
return &ExternalServiceLogger{
logger: zap.NewNop(),
serviceName: config.ServiceName,
}, nil
}
// 根据服务名称创建独立的日志目录
serviceLogDir := filepath.Join(config.LogDir, config.ServiceName)
// 确保日志目录存在
if err := os.MkdirAll(serviceLogDir, 0755); err != nil {
return nil, fmt.Errorf("创建日志目录失败: %w", err)
}
// 创建基础配置
zapConfig := zap.NewProductionConfig()
zapConfig.OutputPaths = []string{"stdout"}
zapConfig.ErrorOutputPaths = []string{"stderr"}
// 创建基础logger
baseLogger, err := zapConfig.Build()
if err != nil {
return nil, fmt.Errorf("创建基础logger失败: %w", err)
}
// 如果启用级别分离,创建文件输出
if config.EnableLevelSeparation {
core := createSeparatedCore(serviceLogDir, config)
baseLogger = zap.New(core)
}
// 创建日志器实例
logger := &ExternalServiceLogger{
logger: baseLogger,
config: config,
serviceName: config.ServiceName,
}
// 如果启用按天分隔,启动定时清理任务
if config.UseDaily {
go logger.startCleanupTask()
}
return logger, nil
}
// createSeparatedCore 创建分离的日志核心
func createSeparatedCore(logDir string, config ExternalServiceLoggingConfig) zapcore.Core {
// 创建编码器
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.TimeKey = "timestamp"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
// 创建不同级别的文件输出
infoWriter := createFileWriter(logDir, "info", config.LevelConfigs["info"], config.ServiceName, config.UseDaily)
errorWriter := createFileWriter(logDir, "error", config.LevelConfigs["error"], config.ServiceName, config.UseDaily)
warnWriter := createFileWriter(logDir, "warn", config.LevelConfigs["warn"], config.ServiceName, config.UseDaily)
// 修复:创建真正的级别分离核心
// 使用自定义的LevelEnabler来确保每个Core只处理特定级别的日志
infoCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(infoWriter),
&levelEnabler{minLevel: zapcore.InfoLevel, maxLevel: zapcore.InfoLevel}, // 只接受INFO级别
)
errorCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(errorWriter),
&levelEnabler{minLevel: zapcore.ErrorLevel, maxLevel: zapcore.ErrorLevel}, // 只接受ERROR级别
)
warnCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(warnWriter),
&levelEnabler{minLevel: zapcore.WarnLevel, maxLevel: zapcore.WarnLevel}, // 只接受WARN级别
)
// 使用 zapcore.NewTee 合并核心,现在每个核心只会处理自己级别的日志
return zapcore.NewTee(infoCore, errorCore, warnCore)
}
// levelEnabler 自定义级别过滤器,确保只接受指定级别的日志
type levelEnabler struct {
minLevel zapcore.Level
maxLevel zapcore.Level
}
// Enabled 实现 zapcore.LevelEnabler 接口
func (l *levelEnabler) Enabled(level zapcore.Level) bool {
return level >= l.minLevel && level <= l.maxLevel
}
// createFileWriter 创建文件写入器
func createFileWriter(logDir, level string, config ExternalServiceLevelFileConfig, serviceName string, useDaily bool) *lumberjack.Logger {
// 使用默认配置如果未指定
if config.MaxSize == 0 {
config.MaxSize = 100
}
if config.MaxBackups == 0 {
config.MaxBackups = 3
}
if config.MaxAge == 0 {
config.MaxAge = 28
}
// 构建文件名
var filename string
if useDaily {
// 按天分隔logs/westdex/2024-01-01/westdex_info.log
date := time.Now().Format("2006-01-02")
dateDir := filepath.Join(logDir, date)
// 确保日期目录存在
if err := os.MkdirAll(dateDir, 0755); err != nil {
// 如果创建日期目录失败,回退到根目录
filename = filepath.Join(logDir, fmt.Sprintf("%s_%s.log", serviceName, level))
} else {
filename = filepath.Join(dateDir, fmt.Sprintf("%s_%s.log", serviceName, level))
}
} else {
// 传统方式logs/westdex/westdex_info.log
filename = filepath.Join(logDir, fmt.Sprintf("%s_%s.log", serviceName, level))
}
return &lumberjack.Logger{
Filename: filename,
MaxSize: config.MaxSize,
MaxBackups: config.MaxBackups,
MaxAge: config.MaxAge,
Compress: config.Compress,
}
}
// LogRequest 记录请求日志
func (e *ExternalServiceLogger) LogRequest(requestID, apiCode string, url interface{}, params interface{}) {
e.logger.Info(fmt.Sprintf("%s API请求", e.serviceName),
zap.String("service", e.serviceName),
zap.String("request_id", requestID),
zap.String("api_code", apiCode),
zap.Any("url", url),
zap.Any("params", params),
zap.String("timestamp", time.Now().Format(time.RFC3339)),
)
}
// LogResponse 记录响应日志
func (e *ExternalServiceLogger) LogResponse(requestID, apiCode string, statusCode int, response []byte, duration time.Duration) {
e.logger.Info(fmt.Sprintf("%s API响应", e.serviceName),
zap.String("service", e.serviceName),
zap.String("request_id", requestID),
zap.String("api_code", apiCode),
zap.Int("status_code", statusCode),
zap.String("response", string(response)),
zap.Duration("duration", duration),
zap.String("timestamp", time.Now().Format(time.RFC3339)),
)
}
// LogError 记录错误日志
func (e *ExternalServiceLogger) LogError(requestID, apiCode string, err error, params interface{}) {
e.logger.Error(fmt.Sprintf("%s API错误", e.serviceName),
zap.String("service", e.serviceName),
zap.String("request_id", requestID),
zap.String("api_code", apiCode),
zap.Error(err),
zap.Any("params", params),
zap.String("timestamp", time.Now().Format(time.RFC3339)),
)
}
// LogInfo 记录信息日志
func (e *ExternalServiceLogger) LogInfo(message string, fields ...zap.Field) {
allFields := []zap.Field{zap.String("service", e.serviceName)}
allFields = append(allFields, fields...)
e.logger.Info(message, allFields...)
}
// LogWarn 记录警告日志
func (e *ExternalServiceLogger) LogWarn(message string, fields ...zap.Field) {
allFields := []zap.Field{zap.String("service", e.serviceName)}
allFields = append(allFields, fields...)
e.logger.Warn(message, allFields...)
}
// LogErrorWithFields 记录带字段的错误日志
func (e *ExternalServiceLogger) LogErrorWithFields(message string, fields ...zap.Field) {
allFields := []zap.Field{zap.String("service", e.serviceName)}
allFields = append(allFields, fields...)
e.logger.Error(message, allFields...)
}
// Sync 同步日志
func (e *ExternalServiceLogger) Sync() error {
return e.logger.Sync()
}
// CleanupOldDateDirs 清理过期的日期目录
func (e *ExternalServiceLogger) CleanupOldDateDirs() error {
if !e.config.UseDaily {
return nil
}
logDir := filepath.Join(e.config.LogDir, e.serviceName)
// 读取日志目录
entries, err := os.ReadDir(logDir)
if err != nil {
return fmt.Errorf("读取日志目录失败: %w", err)
}
// 计算过期时间基于配置的MaxAge
maxAge := 28 // 默认28天
if errorConfig, exists := e.config.LevelConfigs["error"]; exists && errorConfig.MaxAge > 0 {
maxAge = errorConfig.MaxAge
}
cutoffTime := time.Now().AddDate(0, 0, -maxAge)
for _, entry := range entries {
if !entry.IsDir() {
continue
}
// 尝试解析目录名为日期
dirName := entry.Name()
dirTime, err := time.Parse("2006-01-02", dirName)
if err != nil {
// 如果不是日期格式的目录,跳过
continue
}
// 检查是否过期
if dirTime.Before(cutoffTime) {
dirPath := filepath.Join(logDir, dirName)
if err := os.RemoveAll(dirPath); err != nil {
return fmt.Errorf("删除过期目录失败 %s: %w", dirPath, err)
}
}
}
return nil
}
// startCleanupTask 启动定时清理任务
func (e *ExternalServiceLogger) startCleanupTask() {
// 每天凌晨2点执行清理
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
// 等待到下一个凌晨2点
now := time.Now()
next := time.Date(now.Year(), now.Month(), now.Day()+1, 2, 0, 0, 0, now.Location())
time.Sleep(time.Until(next))
// 立即执行一次清理
if err := e.CleanupOldDateDirs(); err != nil {
// 记录清理错误这里使用标准输出因为logger可能还未初始化
fmt.Printf("清理过期日志目录失败: %v\n", err)
}
// 定时执行清理
for range ticker.C {
if err := e.CleanupOldDateDirs(); err != nil {
fmt.Printf("清理过期日志目录失败: %v\n", err)
}
}
}
// GetServiceName 获取服务名称
func (e *ExternalServiceLogger) GetServiceName() string {
return e.serviceName
}