diff options
| author | Marc Pervaz Boocha <mboocha@sudomsg.com> | 2026-02-28 00:16:22 +0530 |
|---|---|---|
| committer | Marc Pervaz Boocha <mboocha@sudomsg.com> | 2026-02-28 00:16:22 +0530 |
| commit | 1c75db8c1d1dc6a0a9097016dbdbbb4348f5c835 (patch) | |
| tree | e3aa95caea8ea13b2e50b69babad2ab9eebfa0f2 | |
| parent | Add Syslog and refactored the Sink api (diff) | |
| download | kit-main.tar kit-main.tar.gz kit-main.tar.bz2 kit-main.tar.lz kit-main.tar.xz kit-main.tar.zst kit-main.zip | |
Add Host Loggermain
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | logging/chain.go (renamed from logging/sinks/chain.go) | 2 | ||||
| -rw-r--r-- | logging/config.go | 245 | ||||
| -rw-r--r-- | logging/encoding.go (renamed from logging/sinks/encoding.go) | 2 | ||||
| -rw-r--r-- | logging/encoding_test.go (renamed from logging/sinks/encoding_test.go) | 14 | ||||
| -rw-r--r-- | logging/host.go | 23 | ||||
| -rw-r--r-- | logging/host_linux.go | 211 | ||||
| -rw-r--r-- | logging/host_windows.go | 18 | ||||
| -rw-r--r-- | logging/log.go | 228 | ||||
| -rw-r--r-- | logging/sink.go (renamed from logging/sinks/sink.go) | 2 | ||||
| -rw-r--r-- | logging/syslog.go (renamed from logging/sinks/syslog.go) | 55 | ||||
| -rw-r--r-- | logging/syslog_stub.go (renamed from logging/sinks/syslog_stub.go) | 2 | ||||
| -rw-r--r-- | logging/syslog_unix.go (renamed from logging/sinks/syslog_unix.go) | 2 | ||||
| -rw-r--r-- | logging/test/handler.go | 12 | ||||
| -rw-r--r-- | logging/writer.go (renamed from logging/sinks/writer.go) | 5 | ||||
| -rw-r--r-- | logging/writer_test.go (renamed from logging/sinks/writer_test.go) | 8 |
17 files changed, 561 insertions, 275 deletions
@@ -2,4 +2,7 @@ module go.sudomsg.com/kit go 1.26.0 -require golang.org/x/sync v0.19.0 +require ( + golang.org/x/sync v0.19.0 + golang.org/x/sys v0.41.0 +) @@ -1,2 +1,4 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/logging/sinks/chain.go b/logging/chain.go index c01ce5f..d6ccb8a 100644 --- a/logging/sinks/chain.go +++ b/logging/chain.go @@ -1,4 +1,4 @@ -package sinks +package logging import ( "context" diff --git a/logging/config.go b/logging/config.go new file mode 100644 index 0000000..fdf7a6b --- /dev/null +++ b/logging/config.go @@ -0,0 +1,245 @@ +// Package logging provides helpers for context-aware logging using Go's slog package. +// +// It allows attaching loggers to context.Context for structured logging, +// recovering and logging panics with stack traces, +// and provides a simple package-level logger setup function. +package logging + +import ( + "context" + "fmt" + "log/slog" + "os" + "strings" + + "go.sudomsg.com/kit/net" +) + +type LogFormat int + +const ( + FormatText LogFormat = iota + FormatJSON + FormatMessage +) + +func (s LogFormat) String() string { + switch s { + case FormatJSON: + return "json" + case FormatText: + return "text" + case FormatMessage: + return "message" + default: + return fmt.Sprintf("LogFormat(%d)", s) + } +} + +func (s *LogFormat) Set(str string) error { + switch strings.ToLower(str) { + case "text": + *s = FormatText + case "json": + *s = FormatJSON + case "message": + *s = FormatMessage + default: + return fmt.Errorf("invalid LogFormat %q", str) + } + return nil +} + +func (s LogFormat) AppendText(b []byte) ([]byte, error) { + return append(b, s.String()...), nil +} + +func (s LogFormat) MarshalText() ([]byte, error) { + return s.AppendText(nil) +} + +func (s *LogFormat) UnmarshalText(b []byte) error { + return s.Set(string(b)) +} + +func newEncoder(s LogFormat) (Encoder, error) { + switch s { + case FormatText: + return LogFmtEncoder, nil + case FormatJSON: + return JSONEncoder, nil + case FormatMessage: + return MessageEncoder, nil + default: + return nil, fmt.Errorf("invalid LogFormat: %q", s) + } +} + +// LogSink defines where log output should be written. +// +// It is used to configure the log destination in Setup. +type LogSink int + +const ( + // SinkStderr directs logs to the standard error (os.Stderr). + SinkStderr LogSink = iota + + // SinkStdout directs logs to the standard output (os.Stdout). + SinkStdout + + // SinkFile directs logs to a file specified by the File field in LogConfig. + // + // If configured, the File field must be a valid path for writing logs. + SinkFile + + SinkSyslog //TODO + + SinkHost //TODO +) + +func (s LogSink) String() string { + switch s { + case SinkStdout: + return "stdout" + case SinkStderr: + return "stderr" + case SinkFile: + return "file" + case SinkSyslog: + return "syslog" + default: + return fmt.Sprintf("LogSink(%d)", s) + } +} + +func (s *LogSink) Set(str string) error { + switch strings.ToLower(str) { + case "stdout": + *s = SinkStdout + case "stderr": + *s = SinkStderr + case "file": + *s = SinkFile + case "syslog": + *s = SinkSyslog + default: + return fmt.Errorf("invalid LogSink %q", str) + } + return nil +} + +func (s LogSink) AppendText(b []byte) ([]byte, error) { + return append(b, s.String()...), nil +} + +func (s LogSink) MarshalText() ([]byte, error) { + return s.AppendText(nil) +} + +func (s *LogSink) UnmarshalText(b []byte) error { + return s.Set(string(b)) +} + +// LogConfig holds configuration for the logging setup. +// +// Level specifies the minimum level for logs (e.g., slog.LevelInfo). +// +// Sink specifies the output destination (e.g., SinkStdout, SinkStderr, SinkFile). +// +// Format specifies the log format, either "json" or "text". +// If empty or "json", JSON format is used. +// +// 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 Facility `toml:"facility"` + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr // Programmatic only +} + +func init() { + Setup(context.Background(), LogConfig{}) // Setup Sensible Defaults +} + +// Setup initializes the default slog.Logger based on the provided LogConfig. +// +// It configures the log output sink, log format, and log level. +// +// Returns an error if the configuration is invalid or if opening the log file fails. +// +// Example: +// +// cfg := LogConfig{ +// Level: slog.LevelInfo, +// Sink: SinkFile, +// Format: FormatJSON, +// File: "/var/log/myapp.log", +// } +// if err := Setup(cfg); err != nil { +// log.Fatalf("failed to configure logging: %v", err) +// } +// +// After a successful call, the global slog logger will log accordingly. +func Setup(ctx context.Context, cfg LogConfig) error { + logger, err := New(ctx, cfg) + if err != nil { + return err + } + slog.SetDefault(logger) + return nil +} + +func newSink(ctx context.Context, cfg LogConfig, encoder Encoder) (Sink, error) { + if (cfg.Sink == SinkStdout || cfg.Sink == SinkStderr) && IsDaemonManaged() { + return NewHostSink(ctx, cfg.Level, "", encoder, cfg.ReplaceAttr) + } + switch cfg.Sink { + case SinkStdout: + return NewWriterSink(os.Stdout, encoder, cfg.Level, cfg.ReplaceAttr), nil + case SinkStderr: + return NewWriterSink(os.Stderr, encoder, cfg.Level, cfg.ReplaceAttr), nil + case SinkFile: + if cfg.File == "" { + return nil, fmt.Errorf("log sink set to 'file' but no file path provided") + } + f, err := os.OpenFile(cfg.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return nil, fmt.Errorf("failed to open log file: %w", err) + } + return NewWriterSink(f, encoder, cfg.Level, cfg.ReplaceAttr), nil + case SinkSyslog: + return DialSyslog(ctx, cfg.Network, cfg.Address, encoder, SyslogOptions{ + Facility: cfg.Facility, + Tag: cfg.Tag, + Level: cfg.Level, + ReplaceAttr: cfg.ReplaceAttr, + }) + case SinkHost: + return NewHostSink(ctx, cfg.Level, cfg.Tag, encoder, cfg.ReplaceAttr) + default: + return nil, fmt.Errorf("invalid LogSink %v", cfg.Sink) + } + +} + +// New is same as Setup but returns the logger instead of setting up the global logger +func New(ctx context.Context, cfg LogConfig) (*slog.Logger, error) { + encoder, err := newEncoder(cfg.Format) + if err != nil { + return nil, err + } + + sink, err := newSink(ctx, cfg, encoder) + if err != nil { + return nil, err + } + handler := NewSinkHandler(sink) + logger := slog.New(handler) + return logger, nil +} diff --git a/logging/sinks/encoding.go b/logging/encoding.go index ae58a0e..c4de0c8 100644 --- a/logging/sinks/encoding.go +++ b/logging/encoding.go @@ -1,4 +1,4 @@ -package sinks +package logging import ( "encoding/json" diff --git a/logging/sinks/encoding_test.go b/logging/encoding_test.go index 09a668a..1e47e4a 100644 --- a/logging/sinks/encoding_test.go +++ b/logging/encoding_test.go @@ -1,4 +1,4 @@ -package sinks_test +package logging_test import ( "encoding/json" @@ -8,22 +8,22 @@ import ( "testing" "testing/slogtest" - "go.sudomsg.com/kit/logging/sinks" + "go.sudomsg.com/kit/logging" ) type logRecorder struct { mu sync.Mutex - encoder sinks.Encoder + encoder logging.Encoder records []string } -var _ sinks.Sink = &logRecorder{} +var _ logging.Sink = &logRecorder{} func (h *logRecorder) Append(r slog.Record) error { h.mu.Lock() defer h.mu.Unlock() - it := sinks.RecordAll(r, nil) + it := logging.RecordAll(r, nil) entry, err := h.encoder(it) if err != nil { @@ -50,9 +50,9 @@ func TestJSONEncoder(t *testing.T) { slogtest.Run(t, func(t *testing.T) slog.Handler { lastMock = &logRecorder{ - encoder: sinks.JSONEncoder, + encoder: logging.JSONEncoder, } - return sinks.NewSinkHandler(lastMock) + return logging.NewSinkHandler(lastMock) }, func(t *testing.T) map[string]any { t.Helper() diff --git a/logging/host.go b/logging/host.go new file mode 100644 index 0000000..b58a806 --- /dev/null +++ b/logging/host.go @@ -0,0 +1,23 @@ +//go:build !linux && !windows + +package logging + +import ( + "context" + "log/slog" + + netkit "go.sudomsg.com/kit/net" +) + +func NewHostSink(ctx context.Context, level slog.Level, tag string, encoder Encoder) (Sink, error) { + return DialSyslog(ctx, netkit.NetUnixDatagram, "", encoder, SyslogOptions{ + Tag: tag, + Facility: FacilityUser, + Level: level, + }) +} + + +func IsDaemonManaged() bool { + return false +} diff --git a/logging/host_linux.go b/logging/host_linux.go new file mode 100644 index 0000000..8617cd0 --- /dev/null +++ b/logging/host_linux.go @@ -0,0 +1,211 @@ +//go:build linux + +package logging + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "log/slog" + "net" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + + netkit "go.sudomsg.com/kit/net" + "golang.org/x/sys/unix" +) + +type journalSink struct { + Level slog.Level + Tag string + Conn net.Conn + Encoder Encoder + mu sync.Mutex + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr +} + +var _ Sink = &journalSink{} + +func newJournalSink(ctx context.Context, level slog.Level, tag string, encoder Encoder, replaceAttr func(groups []string, a slog.Attr) slog.Attr) (*journalSink, error) { + conn, err := netkit.Dial(ctx, netkit.NetUnixDatagram, "/run/systemd/journal/socket") + if err != nil { + return nil, fmt.Errorf("failed to connect to journald: %w", err) + } + + if encoder == nil { + encoder = MessageEncoder + } + + if tag == "" { + tag = filepath.Base(os.Args[0]) + } + + if tag == "" { + tag = "kit" + } + + return &journalSink{ + Level: level, + Tag: tag, + Conn: conn, + Encoder: encoder, + ReplaceAttr: replaceAttr, + }, nil +} + +func (s *journalSink) Enabled(level slog.Level) bool { + return s.Level <= level +} + +func (s *journalSink) Append(r slog.Record) error { + fields := map[string]string{ + "SYSLOG_IDENTIFIER": s.Tag, + } + + it := RecordAll(r, func(groups []string, a slog.Attr) slog.Attr { + var key string + switch a.Key { + case slog.LevelKey: + key = "PRIORITY" + val := a.Value.Any().(slog.Leveler).Level() + fields["PRIORTY"] = fmt.Sprintf("%d", mapLevelToSyslog(val)) + return slog.Attr{} + case slog.TimeKey: + fields["SYSLOG_TIMESTAMP"] = a.Value.String() + return slog.Attr{} + case slog.SourceKey: + pc := uintptr(a.Value.Uint64()) + fs := runtime.CallersFrames([]uintptr{pc}) + f, _ := fs.Next() + + fields["CODE_FUNC"] = f.Func.Name() + fields["CODE_FILE"] = f.File + fields["CODE_LINE"] = strconv.Itoa(f.Line) + return slog.Attr{} + default: + key = sanitizeKey(strings.Join(append(groups, a.Key), "_")) + } + + fields[key] = a.Value.String() + return a + }) + + msg, err := s.Encoder(it) + if err != nil { + return fmt.Errorf("encoding failed: %w", err) + } + + fields["MESSAGE"] = msg + return s.write(fields) +} + +func (s *journalSink) write(fields map[string]string) error { + var buf bytes.Buffer + + for k, v := range fields { + if strings.ContainsRune(v, '\n') { + fmt.Fprintln(&buf, k) + binary.Write(&buf, binary.LittleEndian, uint64(len(v))) + fmt.Fprintln(&buf, v) + } else { + fmt.Fprintf(&buf, "%v=%v\n", k, v) + } + } + + s.mu.Lock() + defer s.mu.Unlock() + + if buf.Len() < 65536 { + _, err := s.Conn.Write(buf.Bytes()) + return err + } + + return s.writeMemfd(buf.Bytes()) +} + +func (s *journalSink) writeMemfd(data []byte) error { + fd, err := unix.MemfdCreate("journal_payload", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING) + if err != nil { + return fmt.Errorf("memfd_create failed: %w", err) + } + defer unix.Close(fd) + + _, err = unix.Write(fd, data) + if err != nil { + return err + } + + _, err = unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, + unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL) + if err != nil { + return err + } + + unixConn, ok := s.Conn.(*net.UnixConn) + if !ok { + return fmt.Errorf("connection is not a unix socket") + } + + rights := unix.UnixRights(fd) + + _, _, err = unixConn.WriteMsgUnix(nil, rights, nil) + return err +} + +func sanitizeKey(k string) string { + k = strings.TrimLeft(strings.ToUpper(k), "_") + var res strings.Builder + for _, r := range k { + if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { + res.WriteRune(r) + } else { + res.WriteRune('_') + } + } + return res.String() +} + +func isJournalAvailable() bool { + const socketPath = "/run/systemd/journal/socket" + + fi, err := os.Stat(socketPath) + if err != nil { + return false + } + + return fi.Mode()&os.ModeSocket != 0 +} + +func NewHostSink(ctx context.Context, level slog.Level, tag string, encoder Encoder, replaceAttr func(groups []string, a slog.Attr) slog.Attr) (Sink, error) { + if isJournalAvailable() { + return newJournalSink(ctx, level, tag, encoder, replaceAttr) + } + + return DialSyslog(ctx, netkit.NetUnixDatagram, "", encoder, SyslogOptions{ + Tag: tag, + Facility: FacilityUser, + Level: level, + ReplaceAttr: replaceAttr, + }) +} + +func IsDaemonManaged() bool { + stream := os.Getenv("JOURNAL_STREAM") + if stream == "" { + return false + } + + var stat syscall.Stat_t + if err := syscall.Fstat(2, &stat); err != nil { + return false + } + + expected := fmt.Sprintf("%d:%d", stat.Dev, stat.Ino) + return stream == expected +} diff --git a/logging/host_windows.go b/logging/host_windows.go new file mode 100644 index 0000000..6beaf7c --- /dev/null +++ b/logging/host_windows.go @@ -0,0 +1,18 @@ +//go:build windows + +package logging + +import ( + "context" + "errors" + "log/slog" +) + + +func NewHostSink(ctx context.Context, level slog.Level, tag string, encoder Encoder) (Sink, error) { + return nil, errors.ErrUnsupported +} + +func IsDaemonManaged() bool { + return false +} diff --git a/logging/log.go b/logging/log.go index dc5b90e..a97d44a 100644 --- a/logging/log.go +++ b/logging/log.go @@ -7,15 +7,9 @@ package logging import ( "context" - "fmt" "log/slog" - "os" "runtime" - "strings" "time" - - "go.sudomsg.com/kit/logging/sinks" - "go.sudomsg.com/kit/net" ) type ctxLoggerKeyType struct{} @@ -116,225 +110,3 @@ func RecoverAndLog(ctx context.Context, msg string, err any) { r.Add("error", err, "stack", stack) handler.Handle(ctx, r) } - -type LogFormat int - -const ( - FormatText LogFormat = iota - FormatJSON - FormatMessage -) - -func (s LogFormat) String() string { - switch s { - case FormatJSON: - return "json" - case FormatText: - return "text" - case FormatMessage: - return "message" - default: - return fmt.Sprintf("LogFormat(%d)", s) - } -} - -func (s *LogFormat) Set(str string) error { - switch strings.ToLower(str) { - case "text": - *s = FormatText - case "json": - *s = FormatJSON - case "message": - *s = FormatMessage - default: - return fmt.Errorf("invalid LogFormat %q", str) - } - return nil -} - -func (s LogFormat) AppendText(b []byte) ([]byte, error) { - return append(b, s.String()...), nil -} - -func (s LogFormat) MarshalText() ([]byte, error) { - return s.AppendText(nil) -} - -func (s *LogFormat) UnmarshalText(b []byte) error { - return s.Set(string(b)) -} - -func newEncoder(s LogFormat) (sinks.Encoder, error) { - switch s { - case FormatText: - return sinks.LogFmtEncoder, nil - case FormatJSON: - return sinks.JSONEncoder, nil - case FormatMessage: - return sinks.MessageEncoder, nil - default: - return nil, fmt.Errorf("invalid LogFormat: %q", s) - } -} - -// LogSink defines where log output should be written. -// -// It is used to configure the log destination in Setup. -type LogSink int - -const ( - // SinkStdout directs logs to the standard output (os.Stdout). - SinkStdout LogSink = iota - - // SinkStderr directs logs to the standard error (os.Stderr). - SinkStderr - - // SinkFile directs logs to a file specified by the File field in LogConfig. - // - // If configured, the File field must be a valid path for writing logs. - SinkFile - - SinkSyslog //TODO - - SinkHost //TODO -) - -func (s LogSink) String() string { - switch s { - case SinkStdout: - return "stdout" - case SinkStderr: - return "stderr" - case SinkFile: - return "file" - case SinkSyslog: - return "syslog" - default: - return fmt.Sprintf("LogSink(%d)", s) - } -} - -func (s *LogSink) Set(str string) error { - switch strings.ToLower(str) { - case "stdout": - *s = SinkStdout - case "stderr": - *s = SinkStderr - case "file": - *s = SinkFile - case "syslog": - *s = SinkSyslog - default: - return fmt.Errorf("invalid LogSink %q", str) - } - return nil -} - -func (s LogSink) AppendText(b []byte) ([]byte, error) { - return append(b, s.String()...), nil -} - -func (s LogSink) MarshalText() ([]byte, error) { - return s.AppendText(nil) -} - -func (s *LogSink) UnmarshalText(b []byte) error { - return s.Set(string(b)) -} - -// LogConfig holds configuration for the logging setup. -// -// Level specifies the minimum level for logs (e.g., slog.LevelInfo). -// -// Sink specifies the output destination (e.g., SinkStdout, SinkStderr, SinkFile). -// -// Format specifies the log format, either "json" or "text". -// If empty or "json", JSON format is used. -// -// 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 sinks.Facility `toml:"facility"` -} - -func init() { - Setup(context.Background(), LogConfig{}) // Setup Sensible Defaults -} - -// Setup initializes the default slog.Logger based on the provided LogConfig. -// -// It configures the log output sink, log format, and log level. -// -// Returns an error if the configuration is invalid or if opening the log file fails. -// -// Example: -// -// cfg := LogConfig{ -// Level: slog.LevelInfo, -// Sink: SinkFile, -// Format: FormatJSON, -// File: "/var/log/myapp.log", -// } -// if err := Setup(cfg); err != nil { -// log.Fatalf("failed to configure logging: %v", err) -// } -// -// After a successful call, the global slog logger will log accordingly. -func Setup(ctx context.Context, cfg LogConfig) error { - logger, err := New(ctx, cfg) - if err != nil { - return nil - } - slog.SetDefault(logger) - return nil -} - -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 - case SinkStderr: - return sinks.NewWriterSink(os.Stderr, encoder, cfg.Level), nil - case SinkFile: - if cfg.File == "" { - return nil, fmt.Errorf("log sink set to 'file' but no file path provided") - } - f, err := os.OpenFile(cfg.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - 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) - } - -} - -// New is same as Setup but returns the logger instead of setting up the global logger -func New(ctx context.Context, cfg LogConfig) (*slog.Logger, error) { - encoder, err := newEncoder(cfg.Format) - if err != nil { - return nil, err - } - - sink, err := newSink(ctx, cfg, encoder) - if err != nil { - return nil, err - } - handler := sinks.NewSinkHandler(sink) - logger := slog.New(handler) - return logger, nil -} diff --git a/logging/sinks/sink.go b/logging/sink.go index 058b128..a1e6748 100644 --- a/logging/sinks/sink.go +++ b/logging/sink.go @@ -1,4 +1,4 @@ -package sinks +package logging import ( "context" diff --git a/logging/sinks/syslog.go b/logging/syslog.go index 0c2e429..00e3738 100644 --- a/logging/sinks/syslog.go +++ b/logging/syslog.go @@ -1,4 +1,4 @@ -package sinks +package logging import ( "context" @@ -7,6 +7,7 @@ import ( "log/slog" "os" "path/filepath" + "sync" "time" "go.sudomsg.com/kit/net" @@ -32,22 +33,25 @@ const ( ) type SyslogSink struct { - Writer io.Writer - Facility Facility - Tag string - Hostname string - Level slog.Level - Encoder Encoder + Writer io.Writer + Facility Facility + Tag string + Hostname string + Level slog.Level + Encoder Encoder EmitHostname bool + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr + mu sync.Mutex } var _ Sink = &SyslogSink{} type SyslogOptions struct { - Facility Facility - Tag string - Hostname string - Level slog.Leveler + Facility Facility + Tag string + Hostname string + Level slog.Leveler + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr } // DialSyslog creates a Sink connected to a local or remote syslog daemon. @@ -90,11 +94,12 @@ func NewSyslogSink(w io.Writer, enc Encoder, options SyslogOptions) *SyslogSink } return &SyslogSink{ - Writer: w, - Encoder: enc, - Tag: options.Tag, - Facility: options.Facility, - Hostname: options.Hostname, + Writer: w, + Encoder: enc, + Tag: options.Tag, + Facility: options.Facility, + Hostname: options.Hostname, + ReplaceAttr: options.ReplaceAttr, } } @@ -105,19 +110,22 @@ func (s *SyslogSink) Enabled(level slog.Level) bool { 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 { + it := RecordAll(r, func(groups []string, attr slog.Attr) slog.Attr { + if s.ReplaceAttr != nil { + attr = s.ReplaceAttr(groups, attr) + } if len(groups) == 0 { - if a.Key == slog.TimeKey { - ts = a.Value.Time() + if attr.Key == slog.TimeKey { + ts = attr.Value.Time() return slog.Attr{} } - if a.Key == slog.LevelKey { - level = a.Value.Any().(slog.Leveler) + if attr.Key == slog.LevelKey { + level = attr.Value.Any().(slog.Leveler) return slog.Attr{} } } - return a + return attr }) out, err := s.Encoder(it) if err != nil { @@ -137,6 +145,9 @@ func (s *SyslogSink) Append(r slog.Record) error { } packet := fmt.Sprintf("<%d>%s %s %s: %s", pri, t, hostname, s.Tag, out) + s.mu.Lock() + defer s.mu.Unlock() + _, err = fmt.Fprint(s.Writer, packet) return err } diff --git a/logging/sinks/syslog_stub.go b/logging/syslog_stub.go index 74aac9f..f5b3e77 100644 --- a/logging/sinks/syslog_stub.go +++ b/logging/syslog_stub.go @@ -1,6 +1,6 @@ //go:build !unix -package sinks +package logging import "go.sudomsg.com/kit/net" diff --git a/logging/sinks/syslog_unix.go b/logging/syslog_unix.go index f90a065..e1ddb69 100644 --- a/logging/sinks/syslog_unix.go +++ b/logging/syslog_unix.go @@ -1,5 +1,5 @@ //go:build unix -package sinks +package logging import ( "os" diff --git a/logging/test/handler.go b/logging/test/handler.go index 210893b..a005987 100644 --- a/logging/test/handler.go +++ b/logging/test/handler.go @@ -9,7 +9,7 @@ import ( "sync" "testing" - "go.sudomsg.com/kit/logging/sinks" + "go.sudomsg.com/kit/logging" ) type logRecorder struct { @@ -17,15 +17,15 @@ type logRecorder struct { records []map[string]any } -var _ sinks.Sink = &logRecorder{} +var _ logging.Sink = &logRecorder{} func (h *logRecorder) Append(r slog.Record) error { h.mu.Lock() defer h.mu.Unlock() - it := sinks.RecordAll(r, nil) + it := logging.RecordAll(r, nil) - h.records = append(h.records, sinks.ToMap(it)) + h.records = append(h.records, logging.ToMap(it)) return nil } @@ -46,7 +46,7 @@ func (h *logRecorder) Records() []map[string]any { // // All recorded logs can be retrieved with the Records method. type MockHandler struct { - *sinks.SinkHandler + *logging.SinkHandler recorder *logRecorder } @@ -59,7 +59,7 @@ func NewMockLogHandler(tb testing.TB) *MockHandler { rec := &logRecorder{} return &MockHandler{ - SinkHandler: sinks.NewSinkHandler(rec), + SinkHandler: logging.NewSinkHandler(rec), recorder: rec, } } diff --git a/logging/sinks/writer.go b/logging/writer.go index ed18c97..6f1ee34 100644 --- a/logging/sinks/writer.go +++ b/logging/writer.go @@ -1,4 +1,4 @@ -package sinks +package logging import ( "fmt" @@ -17,11 +17,12 @@ type WriterSink struct { var _ Sink = &WriterSink{} -func NewWriterSink(w io.Writer, encoder Encoder, level slog.Leveler) *WriterSink { +func NewWriterSink(w io.Writer, encoder Encoder, level slog.Leveler, replaceAttr func(groups []string, a slog.Attr) slog.Attr) *WriterSink { return &WriterSink{ Writer: w, Encoder: encoder, Level: level.Level(), + ReplaceAttr: replaceAttr, } } diff --git a/logging/sinks/writer_test.go b/logging/writer_test.go index 0c8f79e..3e2897d 100644 --- a/logging/sinks/writer_test.go +++ b/logging/writer_test.go @@ -1,4 +1,4 @@ -package sinks_test +package logging_test import ( "bytes" @@ -7,7 +7,7 @@ import ( "testing" "testing/slogtest" - "go.sudomsg.com/kit/logging/sinks" + "go.sudomsg.com/kit/logging" ) func TestWriterHandler(t *testing.T) { @@ -16,8 +16,8 @@ func TestWriterHandler(t *testing.T) { slogtest.Run(t, func(t *testing.T) slog.Handler { buf.Reset() - sink := sinks.NewWriterSink(&buf, sinks.JSONEncoder, slog.LevelDebug) - return sinks.NewSinkHandler(sink) + sink := logging.NewWriterSink(&buf, logging.JSONEncoder, slog.LevelDebug, nil) + return logging.NewSinkHandler(sink) }, func(t *testing.T) map[string]any { t.Helper() |
