优化
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type contextFieldsKey struct{}
|
||||
|
||||
// ContextFields contains request metadata attached to logs emitted with a
|
||||
// request context.
|
||||
type ContextFields struct {
|
||||
RequestID string
|
||||
TraceID string
|
||||
SpanID string
|
||||
ParentSpanID string
|
||||
DeviceID string
|
||||
ClientIP string
|
||||
HTTPMethod string
|
||||
HTTPPath string
|
||||
}
|
||||
|
||||
func WithContextFields(ctx context.Context, fields *ContextFields) context.Context {
|
||||
return context.WithValue(ctx, contextFieldsKey{}, fields)
|
||||
}
|
||||
|
||||
func ContextFieldsFrom(ctx context.Context) *ContextFields {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
fields, _ := ctx.Value(contextFieldsKey{}).(*ContextFields)
|
||||
return fields
|
||||
}
|
||||
|
||||
type contextHandler struct{ handler slog.Handler }
|
||||
|
||||
func (h *contextHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
return h.handler.Enabled(ctx, level)
|
||||
}
|
||||
|
||||
func (h *contextHandler) Handle(ctx context.Context, record slog.Record) error {
|
||||
if fields := ContextFieldsFrom(ctx); fields != nil {
|
||||
record.AddAttrs(
|
||||
slog.String("request_id", fields.RequestID),
|
||||
slog.String("trace_id", fields.TraceID),
|
||||
slog.String("device_id", fields.DeviceID),
|
||||
slog.String("client_ip", fields.ClientIP),
|
||||
slog.String("http_method", fields.HTTPMethod),
|
||||
slog.String("http_path", fields.HTTPPath),
|
||||
)
|
||||
if fields.SpanID != "" {
|
||||
record.AddAttrs(slog.String("span_id", fields.SpanID))
|
||||
}
|
||||
if fields.ParentSpanID != "" {
|
||||
record.AddAttrs(slog.String("parent_span_id", fields.ParentSpanID))
|
||||
}
|
||||
}
|
||||
return h.handler.Handle(ctx, record)
|
||||
}
|
||||
|
||||
func (h *contextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &contextHandler{handler: h.handler.WithAttrs(attrs)}
|
||||
}
|
||||
|
||||
func (h *contextHandler) WithGroup(name string) slog.Handler {
|
||||
return &contextHandler{handler: h.handler.WithGroup(name)}
|
||||
}
|
||||
+12
-2
@@ -53,11 +53,12 @@ func (w *DailyWriter) Write(value []byte) (int, error) {
|
||||
if w.file != nil {
|
||||
_ = w.file.Close()
|
||||
}
|
||||
directory := filepath.Join(w.root, date)
|
||||
filePath := filepath.Join(w.root, date, filepath.Clean(w.name))
|
||||
directory := filepath.Dir(filePath)
|
||||
if err := os.MkdirAll(directory, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
file, err := os.OpenFile(filepath.Join(directory, w.name), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -66,6 +67,15 @@ func (w *DailyWriter) Write(value []byte) (int, error) {
|
||||
return w.file.Write(value)
|
||||
}
|
||||
|
||||
func (w *DailyWriter) Sync() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if w.file == nil {
|
||||
return nil
|
||||
}
|
||||
return w.file.Sync()
|
||||
}
|
||||
|
||||
func (w *DailyWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
+146
-8
@@ -2,8 +2,10 @@ package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -136,12 +138,140 @@ func (c *moduleFilterCore) Write(entry zapcore.Entry, fields []zapcore.Field) er
|
||||
func moduleField(fields []zapcore.Field) string {
|
||||
for _, field := range fields {
|
||||
if field.Key == "mod" {
|
||||
return field.String
|
||||
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
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
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...)
|
||||
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 (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) (slog.Handler, func()) {
|
||||
@@ -170,9 +300,14 @@ func newZapHandler(root, filename string, options Options) (slog.Handler, func()
|
||||
}
|
||||
outputEncoder = zapcore.NewConsoleEncoder(encoder)
|
||||
}
|
||||
level := zap.InfoLevel
|
||||
if parsed := level.Set(strings.ToLower(options.Level)); parsed != nil {
|
||||
level = zap.DebugLevel
|
||||
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(
|
||||
@@ -189,7 +324,8 @@ func newZapHandler(root, filename string, options Options) (slog.Handler, func()
|
||||
consoleCore := zapcore.NewCore(outputEncoder.Clone(), zapcore.AddSync(os.Stdout), levelEnabler)
|
||||
core = zapcore.NewTee(fileCore, &moduleFilterCore{Core: consoleCore, fileOnly: fileOnly})
|
||||
}
|
||||
zapLogger := zap.New(core)
|
||||
routed := &routedFileCore{base: core, encoder: outputEncoder.Clone(), level: levelEnabler, root: root, retentionDay: options.RetentionDay, state: &routedFileState{writers: map[string]*DailyWriter{}}}
|
||||
zapLogger := zap.New(routed)
|
||||
handlerOptions := []zapslog.HandlerOption{zapslog.AddStacktraceAt(slog.LevelError)}
|
||||
if options.ShowLine {
|
||||
handlerOptions = append(handlerOptions, zapslog.WithCaller(true))
|
||||
@@ -197,6 +333,7 @@ func newZapHandler(root, filename string, options Options) (slog.Handler, func()
|
||||
handler := zapslog.NewHandler(zapLogger.Core(), handlerOptions...)
|
||||
cleanup := func() {
|
||||
_ = zapLogger.Sync()
|
||||
routed.Close()
|
||||
_ = file.Close()
|
||||
}
|
||||
return handler, cleanup
|
||||
@@ -205,10 +342,11 @@ func newZapHandler(root, filename string, options Options) (slog.Handler, func()
|
||||
// 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}
|
||||
baseHandler, cleanup := newZapHandler(root, filename, options)
|
||||
state := &reloadableHandlerState{handler: baseHandler, cleanup: cleanup}
|
||||
control := &ReloadableLogger{state: state, filename: filename}
|
||||
logger := kratoslog.NewLogger(&reloadableHandler{state: state}, kratoslog.WithExtractor(tracing.TraceAttrs)).With(attrs...)
|
||||
handler := &contextHandler{handler: &reloadableHandler{state: state}}
|
||||
logger := kratoslog.NewLogger(handler, kratoslog.WithExtractor(tracing.TraceAttrs)).With(attrs...)
|
||||
return logger, control
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestZapHandlerHonorsConfiguredLevel(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
handler, cleanup := newZapHandler(root, "application.log", Options{Level: "error", Format: "json"})
|
||||
defer cleanup()
|
||||
|
||||
if handler.Enabled(context.Background(), slog.LevelInfo) {
|
||||
t.Fatal("info should be disabled when level is error")
|
||||
}
|
||||
if !handler.Enabled(context.Background(), slog.LevelError) {
|
||||
t.Fatal("error should be enabled when level is error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZapHandlerUsesDebugFallbackForInvalidLevel(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
handler, cleanup := newZapHandler(root, "application.log", Options{Level: "not-a-level", Format: "json"})
|
||||
defer cleanup()
|
||||
|
||||
if !handler.Enabled(context.Background(), slog.LevelDebug) {
|
||||
t.Fatal("invalid levels should use the reference debug fallback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZapHandlerRoutesHTTPAndErrorLogs(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
handler, cleanup := newZapHandler(root, "application.log", Options{Level: "info", Format: "json"})
|
||||
logger := slog.New(handler)
|
||||
logger.Info("request", "mod", "http", "request_id", "req-1")
|
||||
logger.Error("failed", "mod", "users")
|
||||
cleanup()
|
||||
|
||||
dateEntries, err := os.ReadDir(root)
|
||||
if err != nil || len(dateEntries) != 1 {
|
||||
t.Fatalf("expected one daily log directory, entries=%v err=%v", len(dateEntries), err)
|
||||
}
|
||||
date := dateEntries[0].Name()
|
||||
paths := []string{
|
||||
filepath.Join(root, date, "application.log"),
|
||||
filepath.Join(root, date, "http", "access.log"),
|
||||
filepath.Join(root, date, "users", "application.log"),
|
||||
filepath.Join(root, date, "error", "error.log"),
|
||||
}
|
||||
for _, path := range paths {
|
||||
if info, err := os.Stat(path); err != nil || info.Size() == 0 {
|
||||
t.Fatalf("expected non-empty routed log %s: info=%v err=%v", path, info, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user