diff --git a/app.go b/app.go index 3ec32d3..d2233ff 100644 --- a/app.go +++ b/app.go @@ -181,7 +181,7 @@ func Init(options ...Option) *AppConfig { var c config.Logger err = viper.UnmarshalKey(acf.LogPathKey, &c) if err != nil { - color.Red("failed to init app log") + color.Red("failed to init app log: " + err.Error()) os.Exit(1) } diff --git a/internal/config/config_logger.go b/internal/config/config_logger.go index 06a6714..cd7441f 100644 --- a/internal/config/config_logger.go +++ b/internal/config/config_logger.go @@ -10,12 +10,13 @@ type Logger struct { LogFile string RuntimeLogFile string - LogPath string `yaml:"logPath"` // Path of the log file - MaxSize int `yaml:"maxSize"` // Maximum log file size in MB - MaxBackups int `yaml:"maxBackups"` // Maximum number of log file backups - MaxAge int `yaml:"maxAge"` // Maximum number of days to retain log files - Compress bool `yaml:"compress"` // Whether to enable gzip compression - UseCaller bool `yaml:"useCaller"` // Whether to enable Zap Caller + LogPath string `yaml:"logPath"` // Path of the log file + LogFilter map[string]string `yaml:"logFilter"` // Filter + MaxSize int `yaml:"maxSize"` // Maximum log file size in MB + MaxBackups int `yaml:"maxBackups"` // Maximum number of log file backups + MaxAge int `yaml:"maxAge"` // Maximum number of days to retain log files + Compress bool `yaml:"compress"` // Whether to enable gzip compression + UseCaller bool `yaml:"useCaller"` // Whether to enable Zap Caller } // GetEncoder 根据模式获取编码器 diff --git a/logger/zap.go b/logger/zap.go index 492e368..f99590f 100644 --- a/logger/zap.go +++ b/logger/zap.go @@ -2,16 +2,19 @@ package logger import ( "fmt" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "unicode/utf8" + "github.com/fatih/color" "github.com/wonli/aqi/internal/config" "go.uber.org/zap" "go.uber.org/zap/buffer" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" - "os" - "path/filepath" - "regexp" - "strings" ) var ZapLog *zap.Logger @@ -68,7 +71,7 @@ func Init(c config.Logger) { stdEncoder := newLimitLengthEncoder(c.GetEncoder(""), 300) stdLog := zapcore.NewCore(stdEncoder, zapcore.AddSync(os.Stdout), zap.InfoLevel) - fileEncoder := getFileStyleEncoder() + fileEncoder := getFileStyleEncoder(c) fileLog := zapcore.NewCore(fileEncoder, zapcore.AddSync(&hook), zap.InfoLevel) rFileLog := zapcore.NewCore(fileEncoder, zapcore.AddSync(&runtimeHook), zap.InfoLevel) @@ -106,7 +109,7 @@ func newLimitLengthEncoder(encoder zapcore.Encoder, limit int) zapcore.Encoder { } // getFileStyleEncoder 获取文件风格的日志编码器 -func getFileStyleEncoder() zapcore.Encoder { +func getFileStyleEncoder(c config.Logger) zapcore.Encoder { encoderConfig := zapcore.EncoderConfig{ TimeKey: "", // 这些字段会在自定义格式中处理 LevelKey: "", @@ -120,22 +123,69 @@ func getFileStyleEncoder() zapcore.Encoder { EncodeName: zapcore.FullNameEncoder, } - return &fileStyleEncoder{ - Encoder: zapcore.NewConsoleEncoder(encoderConfig), + filters := make([]FilterRule, 0, len(c.LogFilter)) + if c.LogFilter != nil { + for actionPattern, fieldRule := range c.LogFilter { + // 解析字段路径和长度(格式:field.path:maxLen) + ruleParts := strings.SplitN(fieldRule, ":", 2) + if len(ruleParts) != 2 { + continue + } + + fieldPath := strings.TrimSpace(ruleParts[0]) + maxLen, _ := strconv.Atoi(strings.TrimSpace(ruleParts[1])) + if maxLen <= 0 { + continue + } + + patternStr := fmt.Sprintf(`"action"\s*:\s*"%s"`, regexp.QuoteMeta(actionPattern)) + pattern := regexp.MustCompile(patternStr) + + fieldPattern := fmt.Sprintf(`("%s":\s*)(.*)`, regexp.QuoteMeta(fieldPath)) + fieldRegex := regexp.MustCompile(fieldPattern) + + filters = append(filters, FilterRule{ + Action: actionPattern, + Field: fieldPath, + MaxLen: maxLen, + Pattern: pattern, + FieldRegex: fieldRegex, + }) + } } + + return &fileStyleEncoder{ + Encoder: zapcore.NewConsoleEncoder(encoderConfig), + base64Filter: regexp.MustCompile(`("data:[^"]*;base64,)([^"]*)`), + logFilters: filters, + } +} + +type FilterRule struct { + Action string + Field string + MaxLen int + Pattern *regexp.Regexp // 匹配action的正则 + FieldRegex *regexp.Regexp // 匹配字段的正则 } // fileStyleEncoder 自定义编码风格输出 type fileStyleEncoder struct { zapcore.Encoder + + base64Filter *regexp.Regexp + logFilters []FilterRule } func (e *fileStyleEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { + // 处理自定义过滤规则 + if e.logFilters != nil { + entry.Message = e.processMessage(entry.Message, e.logFilters) + } + // 正则表达式过滤base64的主体 - filterRegex := regexp.MustCompile(`("data:[^"]*;base64,)([^"]*)`) - if filterRegex.MatchString(entry.Message) { - // 替换中间部分,保留前后部分 - entry.Message = filterRegex.ReplaceAllString(entry.Message, `$1..(replace)..`) + if e.base64Filter.MatchString(entry.Message) { + entry.Message = e.base64Filter.ReplaceAllString(entry.Message, `$1..(replace)..`) } // 创建输出缓冲区 @@ -203,3 +253,28 @@ func (e *fileStyleEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Fie return buf, nil } + +func (e *fileStyleEncoder) processMessage(msg string, filters []FilterRule) string { + for _, rule := range filters { + if strings.Contains(msg, rule.Action) && rule.Pattern.MatchString(msg) { + msg = rule.FieldRegex.ReplaceAllStringFunc(msg, func(match string) string { + parts := rule.FieldRegex.FindStringSubmatch(match) + if len(parts) >= 3 { + value := parts[2] + if len(value) > rule.MaxLen { + prefix := parts[1] + // 确保不会从中文字符中间截断 + safeIndex := rule.MaxLen + for safeIndex > 0 && !utf8.RuneStart(value[safeIndex]) { + safeIndex-- + } + return prefix + value[:safeIndex] + "..." + } + return match + } + return match + }) + } + } + return msg +} diff --git a/logger/zap_test.go b/logger/zap_test.go new file mode 100644 index 0000000..8b74475 --- /dev/null +++ b/logger/zap_test.go @@ -0,0 +1,60 @@ +package logger + +import ( + "fmt" + "log" + "regexp" + "strconv" + "strings" + "testing" +) + +func TestZap(t *testing.T) { + + logFilters := map[string]string{ + "article.list": "data:16", + "res.uploadImg": "params:64", + } + + filters := make([]FilterRule, 0, len(logFilters)) + for actionPattern, fieldRule := range logFilters { + // 解析字段路径和长度(格式:field.path:maxLen) + ruleParts := strings.SplitN(fieldRule, ":", 2) + if len(ruleParts) != 2 { + continue + } + + fieldPath := strings.TrimSpace(ruleParts[0]) + maxLen, _ := strconv.Atoi(strings.TrimSpace(ruleParts[1])) + if maxLen <= 0 { + maxLen = 0 + } + + patternStr := fmt.Sprintf(`"action":"%s"`, regexp.QuoteMeta(actionPattern)) + pattern := regexp.MustCompile(patternStr) + + fieldPattern := fmt.Sprintf(`("%s":\s*)(.*)`, regexp.QuoteMeta(fieldPath)) + fieldRegex := regexp.MustCompile(fieldPattern) + + filters = append(filters, FilterRule{ + Action: actionPattern, + Field: fieldPath, + MaxLen: maxLen, + Pattern: pattern, + FieldRegex: fieldRegex, + }) + } + + // 测试消息 + msg1 := `{"action":"res.uploadImg","params":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYoAAAGICAYAAABftLn"}` + msg2 := `{"code":0,"action":"article.list","data":{"page":{"current":1,"pageSize":10,"total":5}}}` + + fs := fileStyleEncoder{} + + // 处理消息 + processedMsg1 := fs.processMessage(msg1, filters) + log.Println("处理后的消息1:", processedMsg1) + + processedMsg2 := fs.processMessage(msg2, filters) + log.Println("处理后的消息2:", processedMsg2) +}