kra-new/pkg/logging/zap.go

470 lines
14 KiB
Go

package logging
import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"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
}
// ErrorEntry is the storage-neutral representation of an Error-level log.
// Keeping it in pkg/logging lets the log core report failures without taking a
// dependency on the application service or persistence layers.
type ErrorEntry struct {
Form, Info, Level, RequestID, TraceID string
}
// ErrorSink receives Error-level log entries. The sink must not log failures
// through the same logger, otherwise a storage failure could recurse forever.
type ErrorSink interface {
RecordLogError(context.Context, ErrorEntry) error
}
type ErrorSinkFunc func(context.Context, ErrorEntry) error
func (f ErrorSinkFunc) RecordLogError(ctx context.Context, entry ErrorEntry) error {
return f(ctx, entry)
}
type errorSinkState struct {
mu sync.RWMutex
sink ErrorSink
}
func (s *errorSinkState) record(entry ErrorEntry) {
if s == nil {
return
}
s.mu.RLock()
sink := s.sink
s.mu.RUnlock()
if sink != nil {
_ = sink.RecordLogError(context.Background(), entry)
}
}
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
errorSink *errorSinkState
}
func (l *ReloadableLogger) Reload(root string, options Options) {
handler, cleanup := newZapHandler(root, l.filename, options, l.errorSink)
l.state.mu.Lock()
previous := l.state.cleanup
l.state.handler = handler
l.state.cleanup = cleanup
l.state.mu.Unlock()
if previous != nil {
previous()
}
}
// SetErrorSink changes the database/audit target while keeping the current
// logger and its hot-reload state intact.
func (l *ReloadableLogger) SetErrorSink(sink ErrorSink) {
if l == nil || l.errorSink == nil {
return
}
l.errorSink.mu.Lock()
l.errorSink.sink = sink
l.errorSink.mu.Unlock()
}
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" {
if field.String != "" {
return field.String
}
if field.Interface != nil {
return fmt.Sprint(field.Interface)
}
}
}
return ""
}
type routedFileState struct {
mu sync.Mutex
writers map[string]*DailyWriter
}
type routedFileCore struct {
base zapcore.Core
encoder zapcore.Encoder
level zapcore.LevelEnabler
root string
retentionDay int
state *routedFileState
fields []zapcore.Field
errorSink *errorSinkState
}
func (c *routedFileCore) Enabled(level zapcore.Level) bool { return c.level.Enabled(level) }
func (c *routedFileCore) With(fields []zapcore.Field) zapcore.Core {
inherited := append([]zapcore.Field(nil), c.fields...)
inherited = append(inherited, fields...)
return &routedFileCore{base: c.base.With(fields), encoder: c.encoder, level: c.level, root: c.root, retentionDay: c.retentionDay, state: c.state, fields: inherited, errorSink: c.errorSink}
}
func (c *routedFileCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if c.Enabled(entry.Level) {
return checked.AddCore(entry, c)
}
return checked
}
func (c *routedFileCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
baseErr := c.base.Write(entry, fields)
allFields := append([]zapcore.Field(nil), c.fields...)
allFields = append(allFields, fields...)
if entry.Level >= zapcore.ErrorLevel && !isGORMLoggerEntry(entry.Caller.File, allFields) {
c.errorSink.record(errorEntryFromZap(entry, allFields))
}
paths := routedLogPaths(moduleField(allFields), entry.Level)
if len(paths) == 0 {
return baseErr
}
buffer, err := c.encoder.Clone().EncodeEntry(entry, allFields)
if err != nil {
if baseErr != nil {
return baseErr
}
return err
}
defer buffer.Free()
for _, name := range paths {
writer := c.writer(name)
if _, writeErr := writer.Write(buffer.Bytes()); writeErr != nil && baseErr == nil {
baseErr = writeErr
}
}
return baseErr
}
func isGORMLoggerEntry(filename string, fields []zapcore.Field) bool {
for _, field := range fields {
if field.Key == "gorm_logger" {
return true
}
}
normalized := strings.ReplaceAll(filename, "\\", "/")
return strings.HasSuffix(normalized, "/gorm_logger_writer.go") ||
strings.HasSuffix(normalized, "/pkg/database/gormkit/logger.go")
}
func errorEntryFromZap(entry zapcore.Entry, fields []zapcore.Field) ErrorEntry {
requestID, traceID, errorText := "", "", ""
for _, field := range fields {
switch field.Key {
case "request_id":
if requestID == "" {
requestID = zapFieldString(field)
}
case "trace_id":
if traceID == "" {
traceID = zapFieldString(field)
}
case "error", "err":
if errorText == "" {
errorText = zapFieldString(field)
}
}
}
info := entry.Message
if errorText != "" {
info += " | 错误: " + errorText
}
if entry.Caller.File != "" {
info += fmt.Sprintf(" \n 源文件:%s:%d", entry.Caller.File, entry.Caller.Line)
}
if entry.Stack != "" {
info += " \n 调用栈:" + entry.Stack
if frame, ok := finalApplicationCaller(entry.Stack); ok {
functionName, source, startLine, endLine, err := functionSourceAt(frame.File, frame.Line)
if err == nil {
info += fmt.Sprintf(" \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", frame.File, frame.Line, functionName, startLine, endLine, source)
} else {
info += fmt.Sprintf(" \n 最终调用方法:%s:%d (%s) | extract_err=%v", frame.File, frame.Line, functionName, err)
}
}
}
return ErrorEntry{Form: "后端", Info: info, Level: entry.Level.String(), RequestID: requestID, TraceID: traceID}
}
func zapFieldString(field zapcore.Field) string {
if field.String != "" {
return field.String
}
if field.Interface != nil {
return fmt.Sprint(field.Interface)
}
return ""
}
func (c *routedFileCore) Sync() error {
result := c.base.Sync()
c.state.mu.Lock()
defer c.state.mu.Unlock()
for _, writer := range c.state.writers {
if err := writer.Sync(); err != nil && result == nil {
result = err
}
}
return result
}
func (c *routedFileCore) writer(name string) *DailyWriter {
c.state.mu.Lock()
defer c.state.mu.Unlock()
writer := c.state.writers[name]
if writer == nil {
writer = NewDailyWriter(c.root, name, c.retentionDay)
c.state.writers[name] = writer
}
return writer
}
func (c *routedFileCore) Close() {
c.state.mu.Lock()
defer c.state.mu.Unlock()
for name, writer := range c.state.writers {
_ = writer.Close()
delete(c.state.writers, name)
}
}
func routedLogPaths(module string, level zapcore.Level) []string {
paths := make([]string, 0, 2)
if module = safeModuleName(module); module != "" {
switch module {
case "http":
paths = append(paths, filepath.Join("http", "access.log"))
case "timedTask":
paths = append(paths, filepath.Join("timedTask", "task.log"))
case "error":
paths = append(paths, filepath.Join("error", "error.log"))
default:
paths = append(paths, filepath.Join(module, "application.log"))
}
}
if level >= zapcore.ErrorLevel {
errorPath := filepath.Join("error", "error.log")
if len(paths) == 0 || paths[len(paths)-1] != errorPath {
paths = append(paths, errorPath)
}
}
return paths
}
func safeModuleName(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
return strings.Map(func(r rune) rune {
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' {
return r
}
return -1
}, value)
}
// 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, errorSink *errorSinkState) (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.DebugLevel
// zapcore.Level.Set reports an error but does not make the intended
// configuration handling obvious here. Parse the configured value directly
// so debug/warn/error are actually applied after startup and hot reload.
if value := strings.TrimSpace(strings.ToLower(options.Level)); value != "" {
if err := level.UnmarshalText([]byte(value)); err != 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})
}
routed := &routedFileCore{base: core, encoder: outputEncoder.Clone(), level: levelEnabler, root: root, retentionDay: options.RetentionDay, state: &routedFileState{writers: map[string]*DailyWriter{}}, errorSink: errorSink}
zapLogger := zap.New(routed)
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()
routed.Close()
_ = 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) {
errorSink := &errorSinkState{}
baseHandler, cleanup := newZapHandler(root, filename, options, errorSink)
state := &reloadableHandlerState{handler: baseHandler, cleanup: cleanup}
control := &ReloadableLogger{state: state, filename: filename, errorSink: errorSink}
handler := &contextHandler{handler: &reloadableHandler{state: state}}
logger := kratoslog.NewLogger(handler, 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
}