package logging import ( "log/slog" "os" "strings" "time" "github.com/go-kratos/kratos/contrib/otel/v3/tracing" kratoslog "github.com/go-kratos/kratos/v3/log" "go.uber.org/zap" "go.uber.org/zap/exp/zapslog" "go.uber.org/zap/zapcore" ) type Options struct { Level, Format, EncodeLevel, Prefix, StacktraceKey string LogInConsole, ShowLine bool RetentionDay int FileOnlyModules []string } type moduleFilterCore struct { zapcore.Core fileOnly map[string]struct{} mod string } func (c *moduleFilterCore) With(fields []zapcore.Field) zapcore.Core { mod := c.mod if value := moduleField(fields); value != "" { mod = value } return &moduleFilterCore{Core: c.Core.With(fields), fileOnly: c.fileOnly, mod: mod} } func (c *moduleFilterCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry { if c.Enabled(entry.Level) { return checked.AddCore(entry, c) } return checked } func (c *moduleFilterCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { mod := c.mod if value := moduleField(fields); value != "" { mod = value } if _, excluded := c.fileOnly[mod]; excluded { return nil } return c.Core.Write(entry, fields) } func moduleField(fields []zapcore.Field) string { for _, field := range fields { if field.Key == "mod" { return field.String } } return "" } // NewZapLogger adapts a Zap core to the slog logger used by Kratos v3. // The file layout remains compatible with the administration log viewer. func NewZapLogger(root, filename string, options Options, attrs ...any) (*slog.Logger, func()) { file := NewDailyWriter(root, filename, options.RetentionDay) encoder := zap.NewProductionEncoderConfig() encoder.EncodeTime = zapcore.RFC3339NanoTimeEncoder if options.StacktraceKey != "" { encoder.StacktraceKey = options.StacktraceKey } switch options.EncodeLevel { case "CapitalLevelEncoder": encoder.EncodeLevel = zapcore.CapitalLevelEncoder case "CapitalColorLevelEncoder": encoder.EncodeLevel = zapcore.CapitalColorLevelEncoder case "LowercaseColorLevelEncoder": encoder.EncodeLevel = zapcore.LowercaseColorLevelEncoder default: encoder.EncodeLevel = zapcore.LowercaseLevelEncoder } var outputEncoder zapcore.Encoder = zapcore.NewJSONEncoder(encoder) if options.Format != "" && options.Format != "json" { if options.Prefix != "" { encoder.EncodeTime = func(value time.Time, output zapcore.PrimitiveArrayEncoder) { output.AppendString(options.Prefix + value.Format("2006-01-02 15:04:05.000")) } } outputEncoder = zapcore.NewConsoleEncoder(encoder) } level := zap.InfoLevel if parsed := level.Set(strings.ToLower(options.Level)); parsed != nil { level = zap.DebugLevel } levelEnabler := zap.NewAtomicLevelAt(level) fileCore := zapcore.NewCore( outputEncoder.Clone(), zapcore.AddSync(file), levelEnabler, ) core := zapcore.Core(fileCore) if options.LogInConsole { fileOnly := make(map[string]struct{}, len(options.FileOnlyModules)) for _, module := range options.FileOnlyModules { fileOnly[module] = struct{}{} } consoleCore := zapcore.NewCore(outputEncoder.Clone(), zapcore.AddSync(os.Stdout), levelEnabler) core = zapcore.NewTee(fileCore, &moduleFilterCore{Core: consoleCore, fileOnly: fileOnly}) } zapLogger := zap.New(core) handlerOptions := []zapslog.HandlerOption{zapslog.AddStacktraceAt(slog.LevelError)} if options.ShowLine { handlerOptions = append(handlerOptions, zapslog.WithCaller(true)) } handler := zapslog.NewHandler(zapLogger.Core(), handlerOptions...) logger := kratoslog.NewLogger(handler, kratoslog.WithExtractor(tracing.TraceAttrs)).With(attrs...) cleanup := func() { _ = zapLogger.Sync() _ = file.Close() } return logger, cleanup }