465 lines
15 KiB
Go
465 lines
15 KiB
Go
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
|
||
}
|