diff options
| author | Marc Pervaz Boocha <mboocha@sudomsg.com> | 2026-02-27 21:18:28 +0530 |
|---|---|---|
| committer | Marc Pervaz Boocha <mboocha@sudomsg.com> | 2026-02-27 21:18:28 +0530 |
| commit | a451b3008e17167198da38ec627bb28d8af084de (patch) | |
| tree | b1e30744d58bf7f0a770d8da6fdb949d74fdd88b /logging | |
| parent | Misc Fixes (diff) | |
| download | kit-a451b3008e17167198da38ec627bb28d8af084de.tar kit-a451b3008e17167198da38ec627bb28d8af084de.tar.gz kit-a451b3008e17167198da38ec627bb28d8af084de.tar.bz2 kit-a451b3008e17167198da38ec627bb28d8af084de.tar.lz kit-a451b3008e17167198da38ec627bb28d8af084de.tar.xz kit-a451b3008e17167198da38ec627bb28d8af084de.tar.zst kit-a451b3008e17167198da38ec627bb28d8af084de.zip | |
Add Syslog and refactored the Sink api
Diffstat (limited to 'logging')
| -rw-r--r-- | logging/http/http_test.go | 16 | ||||
| -rw-r--r-- | logging/log.go | 34 | ||||
| -rw-r--r-- | logging/log_test.go | 59 | ||||
| -rw-r--r-- | logging/sinks/encoding.go | 147 | ||||
| -rw-r--r-- | logging/sinks/encoding_test.go | 50 | ||||
| -rw-r--r-- | logging/sinks/sink.go | 4 | ||||
| -rw-r--r-- | logging/sinks/syslog.go | 155 | ||||
| -rw-r--r-- | logging/sinks/syslog_stub.go | 9 | ||||
| -rw-r--r-- | logging/sinks/syslog_unix.go | 20 | ||||
| -rw-r--r-- | logging/sinks/writer.go | 24 | ||||
| -rw-r--r-- | logging/test/handler.go | 10 | ||||
| -rw-r--r-- | logging/test/handler_test.go | 36 |
12 files changed, 363 insertions, 201 deletions
diff --git a/logging/http/http_test.go b/logging/http/http_test.go index 832b49e..e12b9b2 100644 --- a/logging/http/http_test.go +++ b/logging/http/http_test.go @@ -80,20 +80,14 @@ func TestNew(t *testing.T) { t.Fatalf("expected 1 log record, got %d", len(records)) } - record := records[0] - - attrs := map[string]slog.Value{} - record.Attrs(func(a slog.Attr) bool { - attrs[a.Key] = a.Value - return true - }) + attrs := records[0] method := attrs["method"] - if got := method.String(); got != tt.method { + if got := method.(string); got != tt.method { t.Fatalf("expected %v log record, got %v", tt.method, got) } status := attrs["status"] - if got := int(status.Int64()); got != tt.status { + if got := int(status.(int64)); got != tt.status { t.Fatalf("expected %v log record, got %v", tt.status, got) } @@ -103,11 +97,11 @@ func TestNew(t *testing.T) { } host := attrs["host"] - if got := host.String(); got != url.Host { + if got := host.(string); got != url.Host { t.Fatalf("expected %v log record, got %v", url.Host, got) } path := attrs["path"] - if got := path.String(); got != url.RequestURI() { + if got := path.(string); got != url.RequestURI() { t.Fatalf("expected %v log record, got %v", url.RequestURI(), got) } }) diff --git a/logging/log.go b/logging/log.go index a9d4bf4..dc5b90e 100644 --- a/logging/log.go +++ b/logging/log.go @@ -254,18 +254,18 @@ func (s *LogSink) UnmarshalText(b []byte) error { // File specifies the output file path when SinkFile is selected. // It is required if SinkFile is used. type LogConfig struct { - Level slog.Level `toml:"level"` - Sink LogSink `toml:"sink"` - Format LogFormat `toml:"format"` - File string `toml:"file"` - Tag string `toml:"tag"` - Network net.NetType `toml:"network"` - Address string `toml:"address"` - Facility int `toml:"facility"` + Level slog.Level `toml:"level"` + Sink LogSink `toml:"sink"` + Format LogFormat `toml:"format"` + File string `toml:"file"` + Tag string `toml:"tag"` + Network net.NetType `toml:"network"` + Address string `toml:"address"` + Facility sinks.Facility `toml:"facility"` } func init() { - Setup(LogConfig{}) // Setup Sensible Defaults + Setup(context.Background(), LogConfig{}) // Setup Sensible Defaults } // Setup initializes the default slog.Logger based on the provided LogConfig. @@ -287,8 +287,8 @@ func init() { // } // // After a successful call, the global slog logger will log accordingly. -func Setup(cfg LogConfig) error { - logger, err := New(cfg) +func Setup(ctx context.Context, cfg LogConfig) error { + logger, err := New(ctx, cfg) if err != nil { return nil } @@ -296,7 +296,7 @@ func Setup(cfg LogConfig) error { return nil } -func newSink(cfg LogConfig, encoder sinks.Encoder) (sinks.Sink, error) { +func newSink(ctx context.Context, cfg LogConfig, encoder sinks.Encoder) (sinks.Sink, error) { switch cfg.Sink { case SinkStdout: return sinks.NewWriterSink(os.Stdout, encoder, cfg.Level), nil @@ -311,6 +311,12 @@ func newSink(cfg LogConfig, encoder sinks.Encoder) (sinks.Sink, error) { return nil, fmt.Errorf("failed to open log file: %w", err) } return sinks.NewWriterSink(f, encoder, cfg.Level), nil + case SinkSyslog: + return sinks.DialSyslog(ctx, cfg.Network, cfg.Address, encoder, sinks.SyslogOptions{ + Facility: cfg.Facility, + Tag: cfg.Tag, + Level: cfg.Level, + }) default: return nil, fmt.Errorf("invalid LogSink %v", cfg.Sink) } @@ -318,13 +324,13 @@ func newSink(cfg LogConfig, encoder sinks.Encoder) (sinks.Sink, error) { } // New is same as Setup but returns the logger instead of setting up the global logger -func New(cfg LogConfig) (*slog.Logger, error) { +func New(ctx context.Context, cfg LogConfig) (*slog.Logger, error) { encoder, err := newEncoder(cfg.Format) if err != nil { return nil, err } - sink, err := newSink(cfg, encoder) + sink, err := newSink(ctx, cfg, encoder) if err != nil { return nil, err } diff --git a/logging/log_test.go b/logging/log_test.go index 5efed19..2852939 100644 --- a/logging/log_test.go +++ b/logging/log_test.go @@ -50,19 +50,11 @@ func TestRecoverAndLog(t *testing.T) { } record := records[0] - if record.Message != "panic recovered" { - t.Errorf("expected message 'panic recAvered', got %q", record.Message) + if record[slog.MessageKey] != "panic recovered" { + t.Errorf("expected message 'panic recAvered', got %q", record[slog.MessageKey]) } - foundStack := false - record.Attrs(func(a slog.Attr) bool { - if a.Key == "stack" { - foundStack = true - } - return true - }) - - if !foundStack { + if _, ok := record["stack"]; !ok { t.Errorf("expected 'stack' attribute in log") } } @@ -87,21 +79,21 @@ func TestWith(t *testing.T) { } record := records[0] - foundUser := false - foundID := false - record.Attrs(func(attr slog.Attr) bool { - switch attr.Key { - case "user": - foundUser = attr.Value.String() == user - case "id": - foundID = attr.Value.String() == id + if v, ok := record["user"]; ok { + if v.(string) != user { + t.Errorf("expected %v, got %v in 'user' attribute in log record, ", user, v) } - return true - }) + } else { + t.Errorf("expected 'user' attributes in log record, %v", records) + } - if !foundUser || !foundID { - t.Errorf("expected 'user' and 'id' attributes in log record, %v", records) + if v, ok := record["id"]; ok { + if v.(string) != id { + t.Errorf("expected %v, got %v in 'id' attribute in log record, ", id, v) + } + } else { + t.Errorf("expected 'id' attributes in log record, %v", records) } // Test context carries logger with same attributes @@ -130,16 +122,13 @@ func TestWithGroup(t *testing.T) { } record := records[0] - var foundGroup bool - - record.Attrs(func(attr slog.Attr) bool { - foundGroup = attr.Value.Kind() == slog.KindGroup - return false - }) - - if !foundGroup { - t.Errorf("expected 'user' and 'id' attributes in log record, %v", records) + if v, ok := record["foo"]; ok { + if _, ok := v.(map[string]any); !ok { + t.Errorf("expected 'foo' to be a group attr in log record, %v", records) + } + } else { + t.Errorf("expected 'foo' attributes in log record, %v", records) } } @@ -220,9 +209,9 @@ func TestLogSink(t *testing.T) { t.Parallel() EnumTester(t, []logging.LogSink{ - logging.SinkStdout, - logging.SinkStderr, - logging.SinkFile, + logging.SinkStdout, + logging.SinkStderr, + logging.SinkFile, }) } diff --git a/logging/sinks/encoding.go b/logging/sinks/encoding.go index 42848ec..ae58a0e 100644 --- a/logging/sinks/encoding.go +++ b/logging/sinks/encoding.go @@ -6,117 +6,103 @@ import ( "iter" "log/slog" "runtime" + "slices" "strings" ) -func RecordAll(r slog.Record) iter.Seq2[string, slog.Value] { - return func(yield func(string, slog.Value) bool) { +func RecordAll(r slog.Record, replaceAttr func(groups []string, a slog.Attr) slog.Attr) iter.Seq2[[]string, slog.Attr] { + return func(yield func([]string, slog.Attr) bool) { + var walk func([]string, slog.Attr) bool + walk = func(groups []string, a slog.Attr) bool { + if replaceAttr != nil { + a = replaceAttr(groups, a) + } + + if a.Key == "" { + return true + } + + a.Value = a.Value.Resolve() + + if a.Value.Kind() == slog.KindGroup { + newGroups := append(slices.Clone(groups), a.Key) + for _, child := range a.Value.Group() { + if !walk(newGroups, child) { + return false + } + } + return true + } + + return yield(groups, a) + } + if !r.Time.IsZero() { - if !yield(slog.TimeKey, slog.TimeValue(r.Time)) { + if !walk([]string{}, slog.Time(slog.TimeKey, r.Time)) { return } } - if !yield(slog.LevelKey, slog.AnyValue(r.Level)) { + if !walk([]string{}, slog.Any(slog.LevelKey, r.Level)) { return } - if !yield(slog.MessageKey, slog.StringValue(r.Message)) { + if !walk([]string{}, slog.String(slog.MessageKey, r.Message)) { return } if r.PC != 0 { - if !yield(slog.SourceKey, slog.Uint64Value(uint64(r.PC))) { + if !walk([]string{}, slog.Uint64(slog.SourceKey, uint64(r.PC))) { return } } - var walkAttrs func(attrs []slog.Attr, prefix string) bool - - walkAttrs = func(attrs []slog.Attr, prefix string) bool { - for _, attr := range attrs { - key := attr.Key - if prefix != "" { - key = prefix + "." + key - } - - if attr.Value.Kind() == slog.KindGroup { - if !walkAttrs(attr.Value.Group(), key) { - return false - } - } else { - if !yield(key, attr.Value) { - return false - } - } - } - return true - } r.Attrs(func(attr slog.Attr) bool { - return walkAttrs([]slog.Attr{attr}, "") + return walk([]string{}, attr) }) } } -type Encoder func(rec slog.Record, ReplaceAttr func(string, slog.Value) (slog.Value, bool)) (string, error) +type Encoder func(iter.Seq2[[]string, slog.Attr]) (string, error) -func LogFmtEncoder(rec slog.Record, ReplaceAttr func(string, slog.Value) (slog.Value, bool)) (string, error) { +func LogFmtEncoder(attrs iter.Seq2[[]string, slog.Attr]) (string, error) { str := []string{} - for key, value := range RecordAll(rec) { - if ReplaceAttr != nil { - ok := true - value, ok = ReplaceAttr(key, value) - if !ok { - continue - } - } - if key == slog.SourceKey { - fs := runtime.CallersFrames([]uintptr{rec.PC}) + for groups, attr := range attrs { + if len(groups) == 0 && attr.Key == slog.SourceKey { + pc := uintptr(attr.Value.Uint64()) + fs := runtime.CallersFrames([]uintptr{pc}) f, _ := fs.Next() - value = slog.StringValue(fmt.Sprintf("%s:%d", f.File, f.Line)) + attr.Value = slog.StringValue(fmt.Sprintf("%s:%d", f.File, f.Line)) } - str = append(str, fmt.Sprintf("%s=%q", key, value)) + str = append(str, fmt.Sprintf("%s=%q", strings.Join(append(groups, attr.Key), "."), attr.Value)) } return strings.Join(str, " "), nil } -func insertIntoMap(m map[string]any, dottedKey string, value slog.Value) { - parts := strings.Split(dottedKey, ".") - key := parts[len(parts)-1] - parts = parts[:len(parts)-1] - currentMap := m - - for _, part := range parts { - if _, ok := currentMap[part]; !ok { - currentMap[part] = make(map[string]any) - } - - if nested, ok := currentMap[part].(map[string]any); ok { - currentMap = nested - } else { - currentMap[part] = make(map[string]any) - currentMap = currentMap[part].(map[string]any) - } - } - currentMap[key] = value.Any() -} - -func JSONEncoder(rec slog.Record, ReplaceAttr func(string, slog.Value) (slog.Value, bool)) (string, error) { - m := make(map[string]any) - for key, value := range RecordAll(rec) { - if ReplaceAttr != nil { - ok := true - value, ok = ReplaceAttr(key, value) - if !ok { - continue +func ToMap(seq iter.Seq2[[]string, slog.Attr]) map[string]any { + out := make(map[string]any) + for groups, attr := range seq { + current := out + for _, group := range groups { + if next, ok := current[group].(map[string]any); ok { + current = next + } else { + newMap := make(map[string]any) + current[group] = newMap + current = newMap } } - insertIntoMap(m, key, value) + current[attr.Key] = attr.Value.Any() } + return out +} - if _, ok := m[slog.SourceKey]; ok { - fs := runtime.CallersFrames([]uintptr{rec.PC}) +func JSONEncoder(attrs iter.Seq2[[]string, slog.Attr]) (string, error) { + m := ToMap(attrs) + if v, ok := m[slog.SourceKey]; ok { + pc := v.(uint64) + fs := runtime.CallersFrames([]uintptr{uintptr(pc)}) f, _ := fs.Next() m[slog.SourceKey] = fmt.Sprintf("%s:%d", f.File, f.Line) } @@ -129,14 +115,11 @@ func JSONEncoder(rec slog.Record, ReplaceAttr func(string, slog.Value) (slog.Val return string(b), nil } -func MessageEncoder(rec slog.Record, ReplaceAttr func(string, slog.Value) (slog.Value, bool)) (string, error) { - if ReplaceAttr != nil { - value, ok := ReplaceAttr(slog.MessageKey, slog.StringValue(rec.Message)) - if !ok { - return "", nil +func MessageEncoder(attrs iter.Seq2[[]string, slog.Attr]) (string, error) { + for groups, attr := range attrs { + if len(groups) == 0 && attr.Key == slog.MessageKey { + return attr.Value.String(), nil } - return value.String(), nil } - - return rec.Message, nil + return "", nil } diff --git a/logging/sinks/encoding_test.go b/logging/sinks/encoding_test.go index 2b2839f..09a668a 100644 --- a/logging/sinks/encoding_test.go +++ b/logging/sinks/encoding_test.go @@ -3,20 +3,56 @@ package sinks_test import ( "encoding/json" "log/slog" + "slices" + "sync" "testing" "testing/slogtest" "go.sudomsg.com/kit/logging/sinks" - "go.sudomsg.com/kit/logging/test" ) +type logRecorder struct { + mu sync.Mutex + encoder sinks.Encoder + records []string +} + +var _ sinks.Sink = &logRecorder{} + +func (h *logRecorder) Append(r slog.Record) error { + h.mu.Lock() + defer h.mu.Unlock() + + it := sinks.RecordAll(r, nil) + + entry, err := h.encoder(it) + if err != nil { + return err + } + + h.records = append(h.records, entry) + return nil +} + +func (h *logRecorder) Enabled(level slog.Level) bool { + return true // Capture all logs +} + +func (h *logRecorder) Records() []string { + h.mu.Lock() + defer h.mu.Unlock() + return slices.Clone(h.records) +} + func TestJSONEncoder(t *testing.T) { - var lastMock *test.MockHandler + var lastMock *logRecorder slogtest.Run(t, func(t *testing.T) slog.Handler { - lastMock = test.NewMockLogHandler(t) - return lastMock + lastMock = &logRecorder{ + encoder: sinks.JSONEncoder, + } + return sinks.NewSinkHandler(lastMock) }, func(t *testing.T) map[string]any { t.Helper() @@ -29,11 +65,7 @@ func TestJSONEncoder(t *testing.T) { rec := recs[len(recs)-1] m := make(map[string]any) - s, err := sinks.JSONEncoder(rec, nil) - if err != nil { - t.Fatalf("%v", err) - } - if err := json.Unmarshal([]byte(s), &m); err != nil { + if err := json.Unmarshal([]byte(rec), &m); err != nil { t.Fatalf("%v", err) } diff --git a/logging/sinks/sink.go b/logging/sinks/sink.go index acb437f..058b128 100644 --- a/logging/sinks/sink.go +++ b/logging/sinks/sink.go @@ -35,9 +35,7 @@ func (h *SinkHandler) Handle(ctx context.Context, r slog.Record) error { attrs := GetNormalizedAttrs(r) newRecord.AddAttrs(attrs...) - h.Sink.Append(newRecord) - - return nil + return h.Sink.Append(newRecord) } func (h *SinkHandler) WithAttrs(attrs []slog.Attr) slog.Handler { diff --git a/logging/sinks/syslog.go b/logging/sinks/syslog.go new file mode 100644 index 0000000..0c2e429 --- /dev/null +++ b/logging/sinks/syslog.go @@ -0,0 +1,155 @@ +package sinks + +import ( + "context" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "time" + + "go.sudomsg.com/kit/net" +) + +type Facility int + +const ( + FacilityKern Facility = 0 // kernel messages + FacilityUser Facility = 1 // user-level messages + FacilityMail Facility = 2 // mail system + FacilityDaemon Facility = 3 // system daemons + FacilityAuth Facility = 4 // security/authorization messages + FacilitySyslog Facility = 5 // messages generated internally by syslogd + FacilityLPR Facility = 6 // line printer subsystem + FacilityNews Facility = 7 // network news subsystem + FacilityUUCP Facility = 8 // UUCP subsystem + FacilityCron Facility = 9 // clock daemon + FacilityAuthPriv Facility = 10 // security/authorization messages (private) + FacilityFTP Facility = 11 // FTP daemon + FacilityLocal0 Facility = 16 + FacilityLocal7 Facility = 23 +) + +type SyslogSink struct { + Writer io.Writer + Facility Facility + Tag string + Hostname string + Level slog.Level + Encoder Encoder + EmitHostname bool +} + +var _ Sink = &SyslogSink{} + +type SyslogOptions struct { + Facility Facility + Tag string + Hostname string + Level slog.Leveler +} + +// DialSyslog creates a Sink connected to a local or remote syslog daemon. +// raddr: "/dev/log", "localhost:514", etc. +func DialSyslog(ctx context.Context, network net.NetType, raddr string, enc Encoder, options SyslogOptions) (*SyslogSink, error) { + if raddr == "" { + network, raddr = resolveLocalEndpoint() + } + + conn, err := net.Dial(ctx, network, raddr) + if err != nil { + return nil, fmt.Errorf("syslog dial failed: %w", err) + } + + isLocal := (network == net.NetUnixDatagram || network == net.NetUnix) + + sink := NewSyslogSink(conn, enc, options) + sink.EmitHostname = !isLocal + return sink, nil +} + +func NewSyslogSink(w io.Writer, enc Encoder, options SyslogOptions) *SyslogSink { + if options.Hostname == "" { + options.Hostname, _ = os.Hostname() + } + if options.Hostname == "" { + options.Hostname = "localhost" + } + + if options.Tag == "" { + options.Tag = filepath.Base(os.Args[0]) + } + + if options.Tag == "" { + options.Tag = "kit" + } + + if len(options.Tag) > 32 { + options.Tag = options.Tag[:32] + } + + return &SyslogSink{ + Writer: w, + Encoder: enc, + Tag: options.Tag, + Facility: options.Facility, + Hostname: options.Hostname, + } +} + +func (s *SyslogSink) Enabled(level slog.Level) bool { + return s.Level <= level +} + +func (s *SyslogSink) Append(r slog.Record) error { + level := slog.Leveler(s.Level) + var ts time.Time + it := RecordAll(r, func(groups []string, a slog.Attr) slog.Attr { + if len(groups) == 0 { + if a.Key == slog.TimeKey { + ts = a.Value.Time() + return slog.Attr{} + } + + if a.Key == slog.LevelKey { + level = a.Value.Any().(slog.Leveler) + return slog.Attr{} + } + } + return a + }) + out, err := s.Encoder(it) + if err != nil { + return fmt.Errorf("failed to encode: %w", err) + } + + pri := int(s.Facility)*8 + mapLevelToSyslog(level.Level()) + + // Note: RFC 3164 uses a space for leading zeros in days (e.g., "Jan 2") + t := "" + if !ts.IsZero() { + t = r.Time.Format("Jan _2 15:04:05") + } + hostname := "" + if s.EmitHostname { + hostname = s.Hostname + } + packet := fmt.Sprintf("<%d>%s %s %s: %s", pri, t, hostname, s.Tag, out) + + _, err = fmt.Fprint(s.Writer, packet) + return err +} + +func mapLevelToSyslog(l slog.Level) int { + switch { + case l >= slog.LevelError: + return 3 + case l >= slog.LevelWarn: + return 4 + case l >= slog.LevelInfo: + return 6 + default: + return 7 + } +} diff --git a/logging/sinks/syslog_stub.go b/logging/sinks/syslog_stub.go new file mode 100644 index 0000000..74aac9f --- /dev/null +++ b/logging/sinks/syslog_stub.go @@ -0,0 +1,9 @@ +//go:build !unix + +package sinks + +import "go.sudomsg.com/kit/net" + +func resolveLocalEndpoint() (net.NetType, string) { + return net.NetUDP, "localhost:514" +} diff --git a/logging/sinks/syslog_unix.go b/logging/sinks/syslog_unix.go new file mode 100644 index 0000000..f90a065 --- /dev/null +++ b/logging/sinks/syslog_unix.go @@ -0,0 +1,20 @@ +//go:build unix +package sinks + +import ( + "os" + + "go.sudomsg.com/kit/net" +) + +var defaultSyslogPaths = []string{"/dev/log", "/var/run/log", "/var/run/syslog"} + +func resolveLocalEndpoint() (net.NetType, string) { + for _, path := range defaultSyslogPaths { + if _, err := os.Stat(path); err == nil { + return net.NetUnixDatagram, path + } + } + // Fallback if no local socket is found + return net.NetUDP, "localhost:514" +} diff --git a/logging/sinks/writer.go b/logging/sinks/writer.go index 93f3423..ed18c97 100644 --- a/logging/sinks/writer.go +++ b/logging/sinks/writer.go @@ -8,35 +8,37 @@ import ( ) type WriterSink struct { - writer io.Writer - mu sync.Mutex - encoder Encoder - level slog.Level + Writer io.Writer + mu sync.Mutex + Encoder Encoder + Level slog.Level + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr } var _ Sink = &WriterSink{} func NewWriterSink(w io.Writer, encoder Encoder, level slog.Leveler) *WriterSink { return &WriterSink{ - writer: w, - encoder: encoder, - level: level.Level(), + Writer: w, + Encoder: encoder, + Level: level.Level(), } } func (s *WriterSink) Enabled(level slog.Level) bool { - return s.level < level + return s.Level <= level } func (s *WriterSink) Append(r slog.Record) error { - out, err := s.encoder(r, nil) + it := RecordAll(r, s.ReplaceAttr) + out, err := s.Encoder(it) if err != nil { - return err + return fmt.Errorf("failed to encode: %w", err) } s.mu.Lock() defer s.mu.Unlock() - _, err = fmt.Fprintln(s.writer, out) + _, err = fmt.Fprintln(s.Writer, out) return err } diff --git a/logging/test/handler.go b/logging/test/handler.go index e26916c..210893b 100644 --- a/logging/test/handler.go +++ b/logging/test/handler.go @@ -14,7 +14,7 @@ import ( type logRecorder struct { mu sync.Mutex - records []slog.Record + records []map[string]any } var _ sinks.Sink = &logRecorder{} @@ -23,7 +23,9 @@ func (h *logRecorder) Append(r slog.Record) error { h.mu.Lock() defer h.mu.Unlock() - h.records = append(h.records, r.Clone()) + it := sinks.RecordAll(r, nil) + + h.records = append(h.records, sinks.ToMap(it)) return nil } @@ -31,7 +33,7 @@ func (h *logRecorder) Enabled(level slog.Level) bool { return true // Capture all logs } -func (h *logRecorder) Records() []slog.Record { +func (h *logRecorder) Records() []map[string]any { h.mu.Lock() defer h.mu.Unlock() return slices.Clone(h.records) @@ -65,6 +67,6 @@ func NewMockLogHandler(tb testing.TB) *MockHandler { // Records returns a copy of all slog.Records captured by this handler so far. // // It is safe to call from multiple goroutines. -func (h *MockHandler) Records() []slog.Record { +func (h *MockHandler) Records() []map[string]any { return h.recorder.Records() } diff --git a/logging/test/handler_test.go b/logging/test/handler_test.go index ff1ad01..e78192e 100644 --- a/logging/test/handler_test.go +++ b/logging/test/handler_test.go @@ -4,11 +4,9 @@ import ( "fmt" "log/slog" "runtime" - "strings" "testing" "testing/slogtest" - "go.sudomsg.com/kit/logging/sinks" "go.sudomsg.com/kit/logging/test" ) @@ -28,16 +26,11 @@ func TestMockHandlerCompliance(t *testing.T) { t.Fatalf("no records captured") } - rec := recs[len(recs)-1] + m := recs[len(recs)-1] - // Convert slog.Record to map[string]any - m := make(map[string]any) - for key, value := range sinks.RecordAll(rec) { - insertIntoMap(m, key, value) - } - - if _, ok := m[slog.SourceKey]; ok { - fs := runtime.CallersFrames([]uintptr{rec.PC}) + if v, ok := m[slog.SourceKey]; ok { + pc := uintptr(v.(uint64)) + fs := runtime.CallersFrames([]uintptr{pc}) f, _ := fs.Next() m[slog.SourceKey] = fmt.Sprintf("%s:%d", f.File, f.Line) } @@ -46,24 +39,3 @@ func TestMockHandlerCompliance(t *testing.T) { }, ) } - -func insertIntoMap(m map[string]any, dottedKey string, value slog.Value) { - parts := strings.Split(dottedKey, ".") - key := parts[len(parts)-1] - parts = parts[:len(parts)-1] - currentMap := m - - for _, part := range parts { - if _, ok := currentMap[part]; !ok { - currentMap[part] = make(map[string]any) - } - - if nested, ok := currentMap[part].(map[string]any); ok { - currentMap = nested - } else { - currentMap[part] = make(map[string]any) - currentMap = currentMap[part].(map[string]any) - } - } - currentMap[key] = value.Any() -} |
