aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--go.mod5
-rw-r--r--go.sum2
-rw-r--r--logging/chain.go (renamed from logging/sinks/chain.go)2
-rw-r--r--logging/config.go245
-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.go23
-rw-r--r--logging/host_linux.go211
-rw-r--r--logging/host_windows.go18
-rw-r--r--logging/log.go228
-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.go12
-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
diff --git a/go.mod b/go.mod
index 0cc7e2a..09e8bc6 100644
--- a/go.mod
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
index 159532a..7edb209 100644
--- a/go.sum
+++ b/go.sum
@@ -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()