kra-new/pkg/logging/zap.go

219 lines
6.3 KiB
Go

package logging
import (
"context"
"log/slog"
"os"
"strings"
"sync"
"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 handlerOperation struct {
attrs []slog.Attr
group string
}
type reloadableHandlerState struct {
mu sync.RWMutex
handler slog.Handler
cleanup func()
}
type reloadableHandler struct {
state *reloadableHandlerState
ops []handlerOperation
}
func (h *reloadableHandler) resolved() slog.Handler {
current := h.state.handler
for _, operation := range h.ops {
if operation.group != "" {
current = current.WithGroup(operation.group)
} else {
current = current.WithAttrs(operation.attrs)
}
}
return current
}
func (h *reloadableHandler) Enabled(ctx context.Context, level slog.Level) bool {
h.state.mu.RLock()
defer h.state.mu.RUnlock()
return h.resolved().Enabled(ctx, level)
}
func (h *reloadableHandler) Handle(ctx context.Context, record slog.Record) error {
h.state.mu.RLock()
defer h.state.mu.RUnlock()
return h.resolved().Handle(ctx, record)
}
func (h *reloadableHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
next := append([]handlerOperation(nil), h.ops...)
next = append(next, handlerOperation{attrs: append([]slog.Attr(nil), attrs...)})
return &reloadableHandler{state: h.state, ops: next}
}
func (h *reloadableHandler) WithGroup(name string) slog.Handler {
next := append([]handlerOperation(nil), h.ops...)
next = append(next, handlerOperation{group: name})
return &reloadableHandler{state: h.state, ops: next}
}
type ReloadableLogger struct {
state *reloadableHandlerState
filename string
}
func (l *ReloadableLogger) Reload(root string, options Options) {
handler, cleanup := newZapHandler(root, l.filename, options)
l.state.mu.Lock()
previous := l.state.cleanup
l.state.handler = handler
l.state.cleanup = cleanup
l.state.mu.Unlock()
if previous != nil {
previous()
}
}
func (l *ReloadableLogger) Close() {
l.state.mu.Lock()
cleanup := l.state.cleanup
l.state.cleanup = nil
l.state.mu.Unlock()
if cleanup != nil {
cleanup()
}
}
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 newZapHandler(root, filename string, options Options) (slog.Handler, 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...)
cleanup := func() {
_ = zapLogger.Sync()
_ = file.Close()
}
return handler, cleanup
}
// NewReloadableZapLogger keeps the slog/Kratos adapter stable while replacing
// the underlying Zap core when the runtime configuration changes.
func NewReloadableZapLogger(root, filename string, options Options, attrs ...any) (*slog.Logger, *ReloadableLogger) {
handler, cleanup := newZapHandler(root, filename, options)
state := &reloadableHandlerState{handler: handler, cleanup: cleanup}
control := &ReloadableLogger{state: state, filename: filename}
logger := kratoslog.NewLogger(&reloadableHandler{state: state}, kratoslog.WithExtractor(tracing.TraceAttrs)).With(attrs...)
return logger, control
}
func NewZapLogger(root, filename string, options Options, attrs ...any) (*slog.Logger, func()) {
logger, control := NewReloadableZapLogger(root, filename, options, attrs...)
return logger, control.Close
}