fix(logger): 修复 caller 字段与 OpsSystemLogSink 停止刷盘
修复点: - zap logger 不再强制 AddCallerSkip(1),确保 caller 指向真实调用点 - slog handler 避免重复写 time 字段 - OpsSystemLogSink 优先从字段 component 识别业务组件;停止时 drain 队列并用可用 ctx 刷盘 补充:新增/完善对应单测
This commit is contained in:
@@ -247,7 +247,6 @@ func buildLogger(options InitOptions) (*zap.Logger, zap.AtomicLevel, error) {
|
||||
if stacktraceLevel <= zapcore.FatalLevel {
|
||||
zapOpts = append(zapOpts, zap.AddStacktrace(stacktraceLevel))
|
||||
}
|
||||
zapOpts = append(zapOpts, zap.AddCallerSkip(1))
|
||||
|
||||
logger := zap.New(core, zapOpts...).With(
|
||||
zap.String("service", options.ServiceName),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -127,3 +128,65 @@ func TestInit_FileOutputFailureDowngrade(t *testing.T) {
|
||||
t.Fatalf("stderr should contain fallback warning, got: %s", string(stderrBytes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit_CallerShouldPointToCallsite(t *testing.T) {
|
||||
origStdout := os.Stdout
|
||||
origStderr := os.Stderr
|
||||
stdoutR, stdoutW, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("create stdout pipe: %v", err)
|
||||
}
|
||||
_, stderrW, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("create stderr pipe: %v", err)
|
||||
}
|
||||
os.Stdout = stdoutW
|
||||
os.Stderr = stderrW
|
||||
t.Cleanup(func() {
|
||||
os.Stdout = origStdout
|
||||
os.Stderr = origStderr
|
||||
_ = stdoutR.Close()
|
||||
_ = stdoutW.Close()
|
||||
_ = stderrW.Close()
|
||||
})
|
||||
|
||||
if err := Init(InitOptions{
|
||||
Level: "info",
|
||||
Format: "json",
|
||||
ServiceName: "sub2api",
|
||||
Environment: "test",
|
||||
Caller: true,
|
||||
Output: OutputOptions{
|
||||
ToStdout: true,
|
||||
ToFile: false,
|
||||
},
|
||||
Sampling: SamplingOptions{Enabled: false},
|
||||
}); err != nil {
|
||||
t.Fatalf("Init() error: %v", err)
|
||||
}
|
||||
|
||||
L().Info("caller-check")
|
||||
Sync()
|
||||
_ = stdoutW.Close()
|
||||
logBytes, _ := io.ReadAll(stdoutR)
|
||||
|
||||
var line string
|
||||
for _, item := range strings.Split(string(logBytes), "\n") {
|
||||
if strings.Contains(item, "caller-check") {
|
||||
line = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if line == "" {
|
||||
t.Fatalf("log output missing caller-check: %s", string(logBytes))
|
||||
}
|
||||
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal([]byte(line), &payload); err != nil {
|
||||
t.Fatalf("parse log json failed: %v, line=%s", err, line)
|
||||
}
|
||||
caller, _ := payload["caller"].(string)
|
||||
if !strings.Contains(caller, "logger_test.go:") {
|
||||
t.Fatalf("caller should point to this test file, got: %s", caller)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,8 +41,7 @@ func (h *slogZapHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
}
|
||||
|
||||
func (h *slogZapHandler) Handle(_ context.Context, record slog.Record) error {
|
||||
fields := make([]zap.Field, 0, len(h.attrs)+record.NumAttrs()+4)
|
||||
fields = append(fields, zap.Time("time", record.Time))
|
||||
fields := make([]zap.Field, 0, len(h.attrs)+record.NumAttrs()+3)
|
||||
fields = append(fields, slogAttrsToZapFields(h.groups, h.attrs)...)
|
||||
record.Attrs(func(attr slog.Attr) bool {
|
||||
fields = append(fields, slogAttrToZapField(h.groups, attr))
|
||||
|
||||
90
backend/internal/pkg/logger/slog_handler_test.go
Normal file
90
backend/internal/pkg/logger/slog_handler_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type captureState struct {
|
||||
writes []capturedWrite
|
||||
}
|
||||
|
||||
type capturedWrite struct {
|
||||
entry zapcore.Entry
|
||||
fields []zapcore.Field
|
||||
}
|
||||
|
||||
type captureCore struct {
|
||||
state *captureState
|
||||
withFields []zapcore.Field
|
||||
}
|
||||
|
||||
func newCaptureCore() *captureCore {
|
||||
return &captureCore{state: &captureState{}}
|
||||
}
|
||||
|
||||
func (c *captureCore) Enabled(zapcore.Level) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *captureCore) With(fields []zapcore.Field) zapcore.Core {
|
||||
nextFields := make([]zapcore.Field, 0, len(c.withFields)+len(fields))
|
||||
nextFields = append(nextFields, c.withFields...)
|
||||
nextFields = append(nextFields, fields...)
|
||||
return &captureCore{
|
||||
state: c.state,
|
||||
withFields: nextFields,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *captureCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
return ce.AddCore(entry, c)
|
||||
}
|
||||
|
||||
func (c *captureCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
||||
allFields := make([]zapcore.Field, 0, len(c.withFields)+len(fields))
|
||||
allFields = append(allFields, c.withFields...)
|
||||
allFields = append(allFields, fields...)
|
||||
c.state.writes = append(c.state.writes, capturedWrite{
|
||||
entry: entry,
|
||||
fields: allFields,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *captureCore) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSlogZapHandler_Handle_DoesNotAppendTimeField(t *testing.T) {
|
||||
core := newCaptureCore()
|
||||
handler := newSlogZapHandler(zap.New(core))
|
||||
|
||||
record := slog.NewRecord(time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC), slog.LevelInfo, "hello", 0)
|
||||
record.AddAttrs(slog.String("component", "http.access"))
|
||||
|
||||
if err := handler.Handle(context.Background(), record); err != nil {
|
||||
t.Fatalf("handle slog record: %v", err)
|
||||
}
|
||||
if len(core.state.writes) != 1 {
|
||||
t.Fatalf("write calls = %d, want 1", len(core.state.writes))
|
||||
}
|
||||
|
||||
var hasComponent bool
|
||||
for _, field := range core.state.writes[0].fields {
|
||||
if field.Key == "time" {
|
||||
t.Fatalf("unexpected duplicate time field in slog adapter output")
|
||||
}
|
||||
if field.Key == "component" {
|
||||
hasComponent = true
|
||||
}
|
||||
}
|
||||
if !hasComponent {
|
||||
t.Fatalf("component field should be preserved")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user