Files
tyapi-server/internal/shared/external_logger/external_logger.go
2025-08-27 22:19:19 +08:00

465 lines
15 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"`
// 新增:请求和响应日志的独立配置
RequestLogConfig ExternalServiceLevelFileConfig `yaml:"request_log_config"`
ResponseLogConfig ExternalServiceLevelFileConfig `yaml:"response_log_config"`
}
// 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
// 新增:用于区分请求和响应日志的字段
requestLogger *zap.Logger
responseLogger *zap.Logger
}
// 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)
}
// 创建请求和响应日志器
requestLogger, err := createRequestLogger(serviceLogDir, config)
if err != nil {
// 如果创建失败使用基础logger作为备选
requestLogger = baseLogger
fmt.Printf("创建请求日志器失败使用基础logger: %v\n", err)
}
responseLogger, err := createResponseLogger(serviceLogDir, config)
if err != nil {
// 如果创建失败使用基础logger作为备选
responseLogger = baseLogger
fmt.Printf("创建响应日志器失败使用基础logger: %v\n", err)
}
// 如果启用级别分离,创建文件输出
if config.EnableLevelSeparation {
core := createSeparatedCore(serviceLogDir, config)
baseLogger = zap.New(core)
}
// 创建日志器实例
logger := &ExternalServiceLogger{
logger: baseLogger,
config: config,
serviceName: config.ServiceName,
requestLogger: requestLogger,
responseLogger: responseLogger,
}
// 如果启用按天分隔,启动定时清理任务
if config.UseDaily {
go logger.startCleanupTask()
}
return logger, nil
}
// createRequestLogger 创建请求日志器
func createRequestLogger(logDir string, config ExternalServiceLoggingConfig) (*zap.Logger, error) {
// 创建编码器
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.TimeKey = "timestamp"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
// 使用默认配置如果未指定
requestConfig := config.RequestLogConfig
if requestConfig.MaxSize == 0 {
requestConfig.MaxSize = 100
}
if requestConfig.MaxBackups == 0 {
requestConfig.MaxBackups = 5
}
if requestConfig.MaxAge == 0 {
requestConfig.MaxAge = 30
}
// 创建请求日志文件写入器
requestWriter := createFileWriter(logDir, "request", requestConfig, config.ServiceName, config.UseDaily)
// 创建请求日志核心
requestCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(requestWriter),
zapcore.InfoLevel,
)
return zap.New(requestCore), nil
}
// createResponseLogger 创建响应日志器
func createResponseLogger(logDir string, config ExternalServiceLoggingConfig) (*zap.Logger, error) {
// 创建编码器
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.TimeKey = "timestamp"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
// 使用默认配置如果未指定
responseConfig := config.ResponseLogConfig
if responseConfig.MaxSize == 0 {
responseConfig.MaxSize = 100
}
if responseConfig.MaxBackups == 0 {
responseConfig.MaxBackups = 5
}
if responseConfig.MaxAge == 0 {
responseConfig.MaxAge = 30
}
// 创建响应日志文件写入器
responseWriter := createFileWriter(logDir, "response", responseConfig, config.ServiceName, config.UseDaily)
// 创建响应日志核心
responseCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(responseWriter),
zapcore.InfoLevel,
)
return zap.New(responseCore), 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)
// 新增:请求和响应日志的独立文件输出
requestWriter := createFileWriter(logDir, "request", config.RequestLogConfig, config.ServiceName, config.UseDaily)
responseWriter := createFileWriter(logDir, "response", config.ResponseLogConfig, 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级别
)
// 新增:请求和响应日志核心
requestCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(requestWriter),
&requestResponseEnabler{logType: "request"}, // 只接受请求日志
)
responseCore := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(responseWriter),
&requestResponseEnabler{logType: "response"}, // 只接受响应日志
)
// 使用 zapcore.NewTee 合并核心,现在每个核心只会处理自己类型的日志
return zapcore.NewTee(infoCore, errorCore, warnCore, requestCore, responseCore)
}
// 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
}
// requestResponseEnabler 自定义日志类型过滤器,确保只接受特定类型的日志
type requestResponseEnabler struct {
logType string
}
// Enabled 实现 zapcore.LevelEnabler 接口
func (r *requestResponseEnabler) Enabled(level zapcore.Level) bool {
// 请求和响应日志通常是INFO级别
return level == zapcore.InfoLevel
}
// 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, transactionID, apiCode string, url interface{}, params interface{}) {
e.requestLogger.Info(fmt.Sprintf("%s API请求", e.serviceName),
zap.String("service", e.serviceName),
zap.String("request_id", requestID),
zap.String("transaction_id", transactionID),
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, transactionID, apiCode string, statusCode int, response []byte, duration time.Duration) {
e.responseLogger.Info(fmt.Sprintf("%s API响应", e.serviceName),
zap.String("service", e.serviceName),
zap.String("request_id", requestID),
zap.String("transaction_id", transactionID),
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)),
)
}
// LogResponseWithID 记录包含响应ID的响应日志
func (e *ExternalServiceLogger) LogResponseWithID(requestID, transactionID, apiCode string, statusCode int, response []byte, duration time.Duration, responseID string) {
e.responseLogger.Info(fmt.Sprintf("%s API响应", e.serviceName),
zap.String("service", e.serviceName),
zap.String("request_id", requestID),
zap.String("transaction_id", transactionID),
zap.String("api_code", apiCode),
zap.Int("status_code", statusCode),
zap.String("response", string(response)),
zap.Duration("duration", duration),
zap.String("response_id", responseID),
zap.String("timestamp", time.Now().Format(time.RFC3339)),
)
}
// LogError 记录错误日志
func (e *ExternalServiceLogger) LogError(requestID, transactionID, 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("transaction_id", transactionID),
zap.String("api_code", apiCode),
zap.Error(err),
zap.Any("params", params),
zap.String("timestamp", time.Now().Format(time.RFC3339)),
)
}
// LogErrorWithResponseID 记录包含响应ID的错误日志
func (e *ExternalServiceLogger) LogErrorWithResponseID(requestID, transactionID, apiCode string, err error, params interface{}, responseID string) {
e.logger.Error(fmt.Sprintf("%s API错误", e.serviceName),
zap.String("service", e.serviceName),
zap.String("request_id", requestID),
zap.String("transaction_id", transactionID),
zap.String("api_code", apiCode),
zap.Error(err),
zap.Any("params", params),
zap.String("response_id", responseID),
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
}